Phase 4: Remove deprecated edge fields from Issue struct (Decision 004)
This is the final phase of the Edge Schema Consolidation. It removes the deprecated edge fields (RepliesTo, RelatesTo, DuplicateOf, SupersededBy) from the Issue struct and all related code. Changes: - Remove edge fields from types.Issue struct - Remove edge field scanning from queries.go and transaction.go - Update graph_links_test.go to use dependency API exclusively - Update relate.go to use AddDependency/RemoveDependency - Update show.go with helper functions for thread traversal via deps - Update mail_test.go to verify thread links via dependencies - Add migration 022 to drop columns from issues table - Fix cycle detection to allow bidirectional relates-to links - Fix migration 022 to disable foreign keys before table recreation All edge relationships now use the dependencies table exclusively. The old Issue fields are fully removed. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -10,6 +10,7 @@
|
||||
{"id":"bd-1tw","title":"Fix G104 errors unhandled in internal/storage/sqlite/queries.go:1186","description":"Linting issue: G104: Errors unhandled (gosec) at internal/storage/sqlite/queries.go:1186:2. Error: rows.Close()","status":"closed","priority":0,"issue_type":"bug","created_at":"2025-12-07T15:35:13.051671889-07:00","updated_at":"2025-12-17T23:13:40.53486-08:00","closed_at":"2025-12-17T16:46:11.0289-08:00"}
|
||||
{"id":"bd-20j","title":"sync branch not match config","description":"./bd sync\n→ Exporting pending changes to JSONL...\n→ No changes to commit\n→ Pulling from sync branch 'gh-386'...\nError pulling from sync branch: failed to create worktree: failed to create worktree parent directory: mkdir /var/home/matt/dev/beads/worktree-db-fail/.git: not a directory\nmatt@blufin-framation ~/d/b/worktree-db-fail (worktree-db-fail) [1]\u003e bd config list\n\nConfiguration:\n auto_compact_enabled = false\n compact_batch_size = 50\n compact_model = claude-3-5-haiku-20241022\n compact_parallel_workers = 5\n compact_tier1_days = 30\n compact_tier1_dep_levels = 2\n compact_tier2_commits = 100\n compact_tier2_days = 90\n compact_tier2_dep_levels = 5\n compaction_enabled = false\n issue_prefix = worktree-db-fail\n sync.branch = worktree-db-fail","status":"open","priority":2,"issue_type":"task","created_at":"2025-12-08T06:49:04.449094018-07:00","updated_at":"2025-12-08T06:49:04.449094018-07:00"}
|
||||
{"id":"bd-28db","title":"Add 'bd status' command for issue database overview","description":"Implement a bd status command that provides a quick snapshot of the issue database state, similar to how git status shows working tree state.\n\nExpected output: Show summary including counts by state (open, in-progress, blocked, closed), recent activity (last 7 days), and quick overview without needing multiple queries.\n\nExample output showing issue counts, recent activity stats, and pointer to bd list for details.\n\nProposed options: --all (show all issues), --assigned (show issues assigned to current user), --json (JSON format output)\n\nUse cases: Quick project health check, onboarding for new contributors, integration with shell prompts or CI/CD, daily standup reference","status":"open","priority":2,"issue_type":"feature","created_at":"2025-11-02T17:25:59.203549-08:00","updated_at":"2025-11-02T17:25:59.203549-08:00"}
|
||||
{"id":"bd-2oo","title":"Edge Schema Consolidation: Unify all edges in dependencies table","description":"Consolidate all edge types into the dependency table per decision 004.\n\n## Changes\n- Add metadata column to dependencies table\n- Add thread_id column for conversation grouping\n- Remove redundant Issue fields: replies_to, relates_to, duplicate_of, superseded_by\n- Update all code to use dependencies API\n- Migration script for existing data\n- JSONL format change (breaking)\n\nReference: ~/gt/hop/decisions/004-edge-schema-consolidation.md","status":"open","priority":0,"issue_type":"epic","created_at":"2025-12-18T02:01:48.785558-08:00","updated_at":"2025-12-18T02:01:48.785558-08:00"}
|
||||
{"id":"bd-2q6d","title":"Beads commands operate on stale database without warning","description":"All beads read operations should validate database is in sync with JSONL before proceeding.\n\n**Current Behavior:**\n- Commands can query/read from stale database\n- Only mutation operations (like 'bd sync') check if JSONL is newer\n- User gets incorrect results without realizing database is out of sync\n\n**Expected Behavior:**\n- All beads commands should have pre-flight check for database freshness\n- If JSONL is newer than database, refuse to operate with error: \"Database out of sync. Run 'bd import' first.\"\n- Same safety check that exists for 'bd sync' should apply to ALL operations\n\n**Impact:**\n- Users make decisions based on incomplete/outdated data\n- Silent failures lead to confusion (e.g., thinking issues don't exist when they do)\n- Similar to running git commands on stale repo without being warned to pull\n\n**Example:**\n- Searched for bd-g9eu issue file: not found\n- Issue exists in .beads/issues.jsonl (in git)\n- Database was stale, but no warning was given\n- Led to incorrect conclusion that issue was already closed/deleted","notes":"## Implementation Complete\n\n**Phase 1: Created staleness check (cmd/bd/staleness.go)**\n- ensureDatabaseFresh() function checks JSONL mtime vs last_import_time\n- Returns error with helpful message when database is stale\n- Auto-skips in daemon mode (daemon has auto-import)\n\n**Phase 2: Added to all read commands**\n- list, show, ready, status, stale, info, duplicates, validate\n- Check runs before database queries in direct mode\n- Daemon mode already protected via checkAndAutoImportIfStale()\n\n**Phase 3: Code Review Findings**\nSee follow-up issues:\n- bd-XXXX: Add warning when staleness check errors\n- bd-YYYY: Improve CheckStaleness error handling\n- bd-ZZZZ: Refactor redundant daemon checks (low priority)\n\n**Testing:**\n- Build successful: go build ./cmd/bd\n- Binary works: ./bd --version\n- Ready for manual testing\n\n**Next Steps:**\n1. Test with stale database scenario\n2. Implement review improvements\n3. Close issue when tests pass","status":"closed","priority":1,"issue_type":"bug","created_at":"2025-11-20T19:33:40.019297-05:00","updated_at":"2025-12-17T23:13:40.535149-08:00","closed_at":"2025-12-17T19:11:12.982639-08:00"}
|
||||
{"id":"bd-379","title":"Implement `bd setup cursor` for Cursor IDE integration","description":"Create a `bd setup cursor` command that integrates Beads workflow into Cursor IDE via .cursorrules file. Unlike Claude Code (which has hooks), Cursor uses a static rules file to provide context to its AI.","status":"open","priority":3,"issue_type":"feature","created_at":"2025-11-11T23:32:22.170083-08:00","updated_at":"2025-11-11T23:32:22.170083-08:00"}
|
||||
{"id":"bd-3852","title":"Add orphan detection migration","description":"Create migration to detect orphaned children in existing databases. Query: SELECT id FROM issues WHERE id LIKE '%.%' AND substr(id, 1, instr(id || '.', '.') - 1) NOT IN (SELECT id FROM issues). Log results, let user decide action (delete orphans or convert to top-level).","status":"open","priority":2,"issue_type":"task","created_at":"2025-11-04T12:32:30.727044-08:00","updated_at":"2025-11-04T12:32:30.727044-08:00"}
|
||||
@@ -80,18 +81,18 @@
|
||||
{"id":"bd-bxha","title":"Default to YES for git hooks and merge driver installation","description":"Currently bd init prompts user to install git hooks and merge driver, but setup is incomplete if user declines. Change to install by default unless --skip-hooks or --skip-merge-driver flags are passed. Better safe defaults. If installation fails, warn user and suggest bd doctor --fix.","status":"open","priority":1,"issue_type":"feature","created_at":"2025-11-21T23:16:10.172238-08:00","updated_at":"2025-11-21T23:16:28.369137-08:00","dependencies":[{"issue_id":"bd-bxha","depends_on_id":"bd-tbz3","type":"parent-child","created_at":"2025-11-21T23:16:10.173034-08:00","created_by":"daemon"}]}
|
||||
{"id":"bd-c3u","title":"Review PR #512: clarify bd ready docs","description":"Review and merge PR #512 from aspiers. This PR clarifies what bd ready does after git pull in README.md. Simple 1-line change. URL: https://github.com/anthropics/beads/pull/512","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-13T08:15:13.405161+11:00","updated_at":"2025-12-13T07:07:29.641265-08:00","closed_at":"2025-12-13T07:07:29.641265-08:00"}
|
||||
{"id":"bd-c83r","title":"Prevent multiple daemons from running on the same repo","description":"Multiple bd daemons running on the same repo clone causes race conditions and data corruption risks.\n\n**Problem:**\n- Nothing prevents spawning multiple daemons for the same repository\n- Multiple daemons watching the same files can conflict during sync operations\n- Observed: 4 daemons running simultaneously caused sync race condition\n\n**Solution:**\nImplement daemon singleton enforcement per repo:\n1. Use a lock file (e.g., .beads/.daemon.lock) with PID\n2. On daemon start, check if lock exists and process is alive\n3. If stale lock (dead PID), clean up and acquire lock\n4. If active daemon exists, either:\n - Exit with message 'daemon already running (PID xxx)'\n - Or offer --replace flag to kill existing and take over\n5. Release lock on graceful shutdown\n\n**Edge cases to handle:**\n- Daemon crashes without releasing lock (stale PID detection)\n- Multiple repos in same directory tree (each repo gets own lock)\n- Race between two daemons starting simultaneously (atomic lock acquisition)","status":"closed","priority":2,"issue_type":"bug","created_at":"2025-12-13T06:37:23.377131-08:00","updated_at":"2025-12-16T01:14:49.50347-08:00","closed_at":"2025-12-14T17:34:14.990077-08:00"}
|
||||
{"id":"bd-cb64c226.1","title":"Performance Validation","description":"Confirm no performance regression from cache removal","status":"closed","priority":1,"issue_type":"task","created_at":"2025-10-28T10:50:15.126019-07:00","updated_at":"2025-12-17T22:59:13.637153-08:00","closed_at":"2025-12-17T22:59:13.637153-08:00","close_reason":"Closed"}
|
||||
{"id":"bd-cb64c226.10","title":"Delete server_cache_storage.go","description":"Remove the entire cache implementation file (~286 lines)","status":"closed","priority":1,"issue_type":"task","created_at":"2025-10-27T22:55:38.729299-07:00","updated_at":"2025-12-17T22:59:13.638098-08:00","closed_at":"2025-12-17T22:59:13.638098-08:00","close_reason":"Closed"}
|
||||
{"id":"bd-cb64c226.12","title":"Remove Storage Cache from Server Struct","description":"Eliminate cache fields and use s.storage directly","status":"closed","priority":1,"issue_type":"task","created_at":"2025-10-27T22:55:25.474412-07:00","updated_at":"2025-12-17T22:59:13.638663-08:00","closed_at":"2025-12-17T22:59:13.638663-08:00","close_reason":"Closed"}
|
||||
{"id":"bd-cb64c226.13","title":"Audit Current Cache Usage","description":"**Summary:** Comprehensive audit of storage cache usage revealed minimal dependency across server components, with most calls following a consistent pattern. Investigation confirmed cache was largely unnecessary in single-repository daemon architecture.\n\n**Key Decisions:** \n- Remove all cache-related environment variables\n- Delete server struct cache management fields\n- Eliminate cache-specific test files\n- Deprecate req.Cwd routing logic\n\n**Resolution:** Cache system will be completely removed, simplifying server storage access and reducing unnecessary complexity with negligible performance impact.","notes":"AUDIT COMPLETE\n\ngetStorageForRequest() callers: 17 production + 11 test\n- server_issues_epics.go: 8 calls\n- server_labels_deps_comments.go: 4 calls \n- server_export_import_auto.go: 2 calls\n- server_compact.go: 2 calls\n- server_routing_validation_diagnostics.go: 1 call\n- server_eviction_test.go: 11 calls (DELETE entire file)\n\nPattern everywhere: store, err := s.getStorageForRequest(req) → store := s.storage\n\nreq.Cwd usage: Only for multi-repo routing. Local daemon always serves 1 repo, so routing is unused.\n\nMCP server: Uses separate daemons per repo (no req.Cwd usage found). NOT affected by cache removal.\n\nCache env vars to deprecate:\n- BEADS_DAEMON_MAX_CACHE_SIZE (used in server_core.go:63)\n- BEADS_DAEMON_CACHE_TTL (used in server_core.go:72)\n- BEADS_DAEMON_MEMORY_THRESHOLD_MB (used in server_cache_storage.go:47)\n\nServer struct fields to remove:\n- storageCache, cacheMu, maxCacheSize, cacheTTL, cleanupTicker, cacheHits, cacheMisses\n\nTests to delete:\n- server_eviction_test.go (entire file - 9 tests)\n- limits_test.go cache assertions\n\nSpecial consideration: ValidateDatabase endpoint uses findDatabaseForCwd() outside cache. Verify if used, then remove or inline.\n\nSafe to proceed with removal - cache always had 1 entry in local daemon model.","status":"closed","priority":1,"issue_type":"task","created_at":"2025-10-27T22:55:19.3723-07:00","updated_at":"2025-12-17T22:59:13.639153-08:00","closed_at":"2025-12-17T22:59:13.639153-08:00","close_reason":"Closed"}
|
||||
{"id":"bd-cb64c226.6","title":"Verify MCP Server Compatibility","description":"Ensure MCP server works with cache-free daemon","status":"closed","priority":1,"issue_type":"task","created_at":"2025-10-27T22:56:03.241615-07:00","updated_at":"2025-12-17T22:59:13.63959-08:00","closed_at":"2025-12-17T22:59:13.63959-08:00","close_reason":"Closed"}
|
||||
{"id":"bd-cb64c226.8","title":"Update Metrics and Health Endpoints","description":"Remove cache-related metrics from health/metrics endpoints","status":"closed","priority":1,"issue_type":"task","created_at":"2025-10-27T22:55:49.212047-07:00","updated_at":"2025-12-17T22:59:13.640049-08:00","closed_at":"2025-12-17T22:59:13.640049-08:00","close_reason":"Closed"}
|
||||
{"id":"bd-cb64c226.9","title":"Remove Cache-Related Tests","description":"Delete or update tests that assume multi-repo caching","status":"closed","priority":1,"issue_type":"task","created_at":"2025-10-27T22:55:44.511897-07:00","updated_at":"2025-12-17T22:59:13.640441-08:00","closed_at":"2025-12-17T22:59:13.640441-08:00","close_reason":"Closed"}
|
||||
{"id":"bd-cbed9619.1","title":"Fix multi-round convergence for N-way collisions","description":"**Summary:** Multi-round collision resolution was identified as a critical issue preventing complete synchronization across distributed clones. The problem stemmed from incomplete final pulls that didn't fully propagate all changes between system instances.\n\n**Key Decisions:**\n- Implement multi-round sync mechanism\n- Ensure bounded convergence (≤N rounds)\n- Guarantee idempotent import without data loss\n\n**Resolution:** Developed a sync strategy that ensures all clones converge to the same complete set of issues, unblocking the bd-cbed9619 epic and improving distributed system reliability.","status":"closed","priority":1,"issue_type":"task","created_at":"2025-10-28T21:22:21.486109-07:00","updated_at":"2025-12-17T22:59:13.640844-08:00","closed_at":"2025-12-17T22:59:13.640844-08:00","close_reason":"Closed"}
|
||||
{"id":"bd-cbed9619.2","title":"Implement content-first idempotent import","description":"**Summary:** Refactored issue import to be content-first and idempotent, ensuring consistent data synchronization across multiple import rounds by prioritizing content hash matching over ID-based updates.\n\n**Key Decisions:** \n- Implement content hash as primary matching mechanism\n- Create global collision resolution algorithm\n- Ensure importing same data multiple times results in no-op\n\n**Resolution:** The new import strategy guarantees predictable convergence across distributed systems, solving rename detection and collision handling while maintaining data integrity during multi-stage imports.","status":"closed","priority":1,"issue_type":"task","created_at":"2025-10-28T18:38:25.671302-07:00","updated_at":"2025-12-17T22:59:13.641263-08:00","closed_at":"2025-12-17T22:59:13.641263-08:00","close_reason":"Closed","dependencies":[{"issue_id":"bd-cbed9619.2","depends_on_id":"bd-cbed9619.5","type":"blocks","created_at":"2025-10-28T18:39:28.360026-07:00","created_by":"daemon"},{"issue_id":"bd-cbed9619.2","depends_on_id":"bd-cbed9619.4","type":"blocks","created_at":"2025-10-28T18:39:28.383624-07:00","created_by":"daemon"},{"issue_id":"bd-cbed9619.2","depends_on_id":"bd-cbed9619.3","type":"blocks","created_at":"2025-10-28T18:39:28.407157-07:00","created_by":"daemon"}]}
|
||||
{"id":"bd-cbed9619.3","title":"Implement global N-way collision resolution algorithm","description":"**Summary:** Replaced pairwise collision resolution with a global N-way algorithm that deterministically resolves issue ID conflicts across multiple clones. The new approach groups collisions, deduplicates by content hash, and assigns sequential IDs to ensure consistent synchronization.\n\n**Key Decisions:**\n- Use content hash for global, stable sorting\n- Group collisions by base ID\n- Assign sequential IDs based on sorted unique versions\n- Eliminate order-dependent remapping logic\n\n**Resolution:** Implemented ResolveNWayCollisions function that guarantees deterministic issue ID assignment across multiple synchronization scenarios, solving the core challenge of maintaining consistency in distributed systems with potential conflicts.","status":"closed","priority":1,"issue_type":"task","created_at":"2025-10-28T18:37:42.85616-07:00","updated_at":"2025-12-17T22:59:13.64168-08:00","closed_at":"2025-12-17T22:59:13.64168-08:00","close_reason":"Closed","dependencies":[{"issue_id":"bd-cbed9619.3","depends_on_id":"bd-cbed9619.5","type":"blocks","created_at":"2025-10-28T18:39:28.30886-07:00","created_by":"daemon"},{"issue_id":"bd-cbed9619.3","depends_on_id":"bd-cbed9619.4","type":"blocks","created_at":"2025-10-28T18:39:28.336312-07:00","created_by":"daemon"}]}
|
||||
{"id":"bd-cbed9619.4","title":"Make DetectCollisions read-only (separate detection from modification)","description":"**Summary:** The project restructured the collision detection process in the database to separate read-only detection from state modification, eliminating race conditions and improving system reliability. This was achieved by introducing a two-phase approach: first detecting potential collisions, then applying resolution separately.\n\n**Key Decisions:**\n- Create read-only DetectCollisions method\n- Add RenameDetail to track potential issue renames\n- Implement atomic ApplyCollisionResolution function\n- Separate detection logic from database modification\n\n**Resolution:** The refactoring creates a more robust, composable collision handling mechanism that prevents partial failures and maintains database consistency during complex issue import scenarios.","status":"closed","priority":1,"issue_type":"task","created_at":"2025-10-28T18:37:09.652326-07:00","updated_at":"2025-12-17T22:59:13.642212-08:00","closed_at":"2025-12-17T22:59:13.642212-08:00","close_reason":"Closed","dependencies":[{"issue_id":"bd-cbed9619.4","depends_on_id":"bd-cbed9619.5","type":"blocks","created_at":"2025-10-28T18:39:28.285653-07:00","created_by":"daemon"}]}
|
||||
{"id":"bd-cbed9619.5","title":"Add content-addressable identity to Issue type","description":"**Summary:** Added content-addressable identity to Issue type by implementing a ContentHash field that generates a unique SHA256 fingerprint based on semantic issue content. This resolves issue identification challenges when multiple system instances create issues with identical IDs but different contents.\n\n**Key Decisions:**\n- Use SHA256 for content hashing\n- Hash excludes ID and timestamps\n- Compute hash automatically at creation/import time\n- Add database column for hash storage\n\n**Resolution:** Successfully implemented a deterministic content hashing mechanism that enables reliable issue identification across distributed systems, improving data integrity and collision detection.","status":"closed","priority":1,"issue_type":"task","created_at":"2025-10-28T18:36:44.914967-07:00","updated_at":"2025-12-17T22:59:13.642625-08:00","closed_at":"2025-12-17T22:59:13.642625-08:00","close_reason":"Closed"}
|
||||
{"id":"bd-cb64c226.1","title":"Performance Validation","description":"Confirm no performance regression from cache removal","status":"tombstone","priority":1,"issue_type":"task","created_at":"2025-10-28T10:50:15.126019-07:00","updated_at":"2025-12-17T23:18:29.108883-08:00","close_reason":"Closed","deleted_at":"2025-12-17T23:18:29.108883-08:00","deleted_by":"daemon","delete_reason":"delete","original_type":"task"}
|
||||
{"id":"bd-cb64c226.10","title":"Delete server_cache_storage.go","description":"Remove the entire cache implementation file (~286 lines)","status":"tombstone","priority":1,"issue_type":"task","created_at":"2025-10-27T22:55:38.729299-07:00","updated_at":"2025-12-17T23:18:29.110716-08:00","close_reason":"Closed","deleted_at":"2025-12-17T23:18:29.110716-08:00","deleted_by":"daemon","delete_reason":"delete","original_type":"task"}
|
||||
{"id":"bd-cb64c226.12","title":"Remove Storage Cache from Server Struct","description":"Eliminate cache fields and use s.storage directly","status":"tombstone","priority":1,"issue_type":"task","created_at":"2025-10-27T22:55:25.474412-07:00","updated_at":"2025-12-17T23:18:29.111039-08:00","close_reason":"Closed","deleted_at":"2025-12-17T23:18:29.111039-08:00","deleted_by":"daemon","delete_reason":"delete","original_type":"task"}
|
||||
{"id":"bd-cb64c226.13","title":"Audit Current Cache Usage","description":"**Summary:** Comprehensive audit of storage cache usage revealed minimal dependency across server components, with most calls following a consistent pattern. Investigation confirmed cache was largely unnecessary in single-repository daemon architecture.\n\n**Key Decisions:** \n- Remove all cache-related environment variables\n- Delete server struct cache management fields\n- Eliminate cache-specific test files\n- Deprecate req.Cwd routing logic\n\n**Resolution:** Cache system will be completely removed, simplifying server storage access and reducing unnecessary complexity with negligible performance impact.","notes":"AUDIT COMPLETE\n\ngetStorageForRequest() callers: 17 production + 11 test\n- server_issues_epics.go: 8 calls\n- server_labels_deps_comments.go: 4 calls \n- server_export_import_auto.go: 2 calls\n- server_compact.go: 2 calls\n- server_routing_validation_diagnostics.go: 1 call\n- server_eviction_test.go: 11 calls (DELETE entire file)\n\nPattern everywhere: store, err := s.getStorageForRequest(req) → store := s.storage\n\nreq.Cwd usage: Only for multi-repo routing. Local daemon always serves 1 repo, so routing is unused.\n\nMCP server: Uses separate daemons per repo (no req.Cwd usage found). NOT affected by cache removal.\n\nCache env vars to deprecate:\n- BEADS_DAEMON_MAX_CACHE_SIZE (used in server_core.go:63)\n- BEADS_DAEMON_CACHE_TTL (used in server_core.go:72)\n- BEADS_DAEMON_MEMORY_THRESHOLD_MB (used in server_cache_storage.go:47)\n\nServer struct fields to remove:\n- storageCache, cacheMu, maxCacheSize, cacheTTL, cleanupTicker, cacheHits, cacheMisses\n\nTests to delete:\n- server_eviction_test.go (entire file - 9 tests)\n- limits_test.go cache assertions\n\nSpecial consideration: ValidateDatabase endpoint uses findDatabaseForCwd() outside cache. Verify if used, then remove or inline.\n\nSafe to proceed with removal - cache always had 1 entry in local daemon model.","status":"tombstone","priority":1,"issue_type":"task","created_at":"2025-10-27T22:55:19.3723-07:00","updated_at":"2025-12-17T23:18:29.111369-08:00","close_reason":"Closed","deleted_at":"2025-12-17T23:18:29.111369-08:00","deleted_by":"daemon","delete_reason":"delete","original_type":"task"}
|
||||
{"id":"bd-cb64c226.6","title":"Verify MCP Server Compatibility","description":"Ensure MCP server works with cache-free daemon","status":"tombstone","priority":1,"issue_type":"task","created_at":"2025-10-27T22:56:03.241615-07:00","updated_at":"2025-12-17T23:18:29.109644-08:00","close_reason":"Closed","deleted_at":"2025-12-17T23:18:29.109644-08:00","deleted_by":"daemon","delete_reason":"delete","original_type":"task"}
|
||||
{"id":"bd-cb64c226.8","title":"Update Metrics and Health Endpoints","description":"Remove cache-related metrics from health/metrics endpoints","status":"tombstone","priority":1,"issue_type":"task","created_at":"2025-10-27T22:55:49.212047-07:00","updated_at":"2025-12-17T23:18:29.110022-08:00","close_reason":"Closed","deleted_at":"2025-12-17T23:18:29.110022-08:00","deleted_by":"daemon","delete_reason":"delete","original_type":"task"}
|
||||
{"id":"bd-cb64c226.9","title":"Remove Cache-Related Tests","description":"Delete or update tests that assume multi-repo caching","status":"tombstone","priority":1,"issue_type":"task","created_at":"2025-10-27T22:55:44.511897-07:00","updated_at":"2025-12-17T23:18:29.110385-08:00","close_reason":"Closed","deleted_at":"2025-12-17T23:18:29.110385-08:00","deleted_by":"daemon","delete_reason":"delete","original_type":"task"}
|
||||
{"id":"bd-cbed9619.1","title":"Fix multi-round convergence for N-way collisions","description":"**Summary:** Multi-round collision resolution was identified as a critical issue preventing complete synchronization across distributed clones. The problem stemmed from incomplete final pulls that didn't fully propagate all changes between system instances.\n\n**Key Decisions:**\n- Implement multi-round sync mechanism\n- Ensure bounded convergence (≤N rounds)\n- Guarantee idempotent import without data loss\n\n**Resolution:** Developed a sync strategy that ensures all clones converge to the same complete set of issues, unblocking the bd-cbed9619 epic and improving distributed system reliability.","status":"tombstone","priority":1,"issue_type":"task","created_at":"2025-10-28T21:22:21.486109-07:00","updated_at":"2025-12-17T23:18:29.111713-08:00","close_reason":"Closed","deleted_at":"2025-12-17T23:18:29.111713-08:00","deleted_by":"daemon","delete_reason":"delete","original_type":"task"}
|
||||
{"id":"bd-cbed9619.2","title":"Implement content-first idempotent import","description":"**Summary:** Refactored issue import to be content-first and idempotent, ensuring consistent data synchronization across multiple import rounds by prioritizing content hash matching over ID-based updates.\n\n**Key Decisions:** \n- Implement content hash as primary matching mechanism\n- Create global collision resolution algorithm\n- Ensure importing same data multiple times results in no-op\n\n**Resolution:** The new import strategy guarantees predictable convergence across distributed systems, solving rename detection and collision handling while maintaining data integrity during multi-stage imports.","status":"tombstone","priority":1,"issue_type":"task","created_at":"2025-10-28T18:38:25.671302-07:00","updated_at":"2025-12-17T23:18:29.112032-08:00","close_reason":"Closed","dependencies":[{"issue_id":"bd-cbed9619.2","depends_on_id":"bd-cbed9619.5","type":"blocks","created_at":"2025-10-28T18:39:28.360026-07:00","created_by":"daemon"},{"issue_id":"bd-cbed9619.2","depends_on_id":"bd-cbed9619.4","type":"blocks","created_at":"2025-10-28T18:39:28.383624-07:00","created_by":"daemon"},{"issue_id":"bd-cbed9619.2","depends_on_id":"bd-cbed9619.3","type":"blocks","created_at":"2025-10-28T18:39:28.407157-07:00","created_by":"daemon"}],"deleted_at":"2025-12-17T23:18:29.112032-08:00","deleted_by":"daemon","delete_reason":"delete","original_type":"task"}
|
||||
{"id":"bd-cbed9619.3","title":"Implement global N-way collision resolution algorithm","description":"**Summary:** Replaced pairwise collision resolution with a global N-way algorithm that deterministically resolves issue ID conflicts across multiple clones. The new approach groups collisions, deduplicates by content hash, and assigns sequential IDs to ensure consistent synchronization.\n\n**Key Decisions:**\n- Use content hash for global, stable sorting\n- Group collisions by base ID\n- Assign sequential IDs based on sorted unique versions\n- Eliminate order-dependent remapping logic\n\n**Resolution:** Implemented ResolveNWayCollisions function that guarantees deterministic issue ID assignment across multiple synchronization scenarios, solving the core challenge of maintaining consistency in distributed systems with potential conflicts.","status":"tombstone","priority":1,"issue_type":"task","created_at":"2025-10-28T18:37:42.85616-07:00","updated_at":"2025-12-17T23:18:29.112335-08:00","close_reason":"Closed","dependencies":[{"issue_id":"bd-cbed9619.3","depends_on_id":"bd-cbed9619.5","type":"blocks","created_at":"2025-10-28T18:39:28.30886-07:00","created_by":"daemon"},{"issue_id":"bd-cbed9619.3","depends_on_id":"bd-cbed9619.4","type":"blocks","created_at":"2025-10-28T18:39:28.336312-07:00","created_by":"daemon"}],"deleted_at":"2025-12-17T23:18:29.112335-08:00","deleted_by":"daemon","delete_reason":"delete","original_type":"task"}
|
||||
{"id":"bd-cbed9619.4","title":"Make DetectCollisions read-only (separate detection from modification)","description":"**Summary:** The project restructured the collision detection process in the database to separate read-only detection from state modification, eliminating race conditions and improving system reliability. This was achieved by introducing a two-phase approach: first detecting potential collisions, then applying resolution separately.\n\n**Key Decisions:**\n- Create read-only DetectCollisions method\n- Add RenameDetail to track potential issue renames\n- Implement atomic ApplyCollisionResolution function\n- Separate detection logic from database modification\n\n**Resolution:** The refactoring creates a more robust, composable collision handling mechanism that prevents partial failures and maintains database consistency during complex issue import scenarios.","status":"tombstone","priority":1,"issue_type":"task","created_at":"2025-10-28T18:37:09.652326-07:00","updated_at":"2025-12-17T23:18:29.112637-08:00","close_reason":"Closed","dependencies":[{"issue_id":"bd-cbed9619.4","depends_on_id":"bd-cbed9619.5","type":"blocks","created_at":"2025-10-28T18:39:28.285653-07:00","created_by":"daemon"}],"deleted_at":"2025-12-17T23:18:29.112637-08:00","deleted_by":"daemon","delete_reason":"delete","original_type":"task"}
|
||||
{"id":"bd-cbed9619.5","title":"Add content-addressable identity to Issue type","description":"**Summary:** Added content-addressable identity to Issue type by implementing a ContentHash field that generates a unique SHA256 fingerprint based on semantic issue content. This resolves issue identification challenges when multiple system instances create issues with identical IDs but different contents.\n\n**Key Decisions:**\n- Use SHA256 for content hashing\n- Hash excludes ID and timestamps\n- Compute hash automatically at creation/import time\n- Add database column for hash storage\n\n**Resolution:** Successfully implemented a deterministic content hashing mechanism that enables reliable issue identification across distributed systems, improving data integrity and collision detection.","status":"tombstone","priority":1,"issue_type":"task","created_at":"2025-10-28T18:36:44.914967-07:00","updated_at":"2025-12-17T23:18:29.112933-08:00","close_reason":"Closed","deleted_at":"2025-12-17T23:18:29.112933-08:00","deleted_by":"daemon","delete_reason":"delete","original_type":"task"}
|
||||
{"id":"bd-crgr","title":"GH#517: Claude sets priority wrong on new install","description":"Claude uses 'medium/high/low' for priority instead of P0-P4. Update bd prime/onboard output to be clearer about priority syntax. See GitHub issue #517.","status":"tombstone","priority":2,"issue_type":"bug","created_at":"2025-12-16T01:03:34.803084-08:00","updated_at":"2025-12-17T16:11:17.070763-08:00","deleted_at":"2025-12-17T16:11:17.070763-08:00","deleted_by":"batch delete","delete_reason":"batch delete","original_type":"bug"}
|
||||
{"id":"bd-d148","title":"GH#483: Pre-commit hook fails unnecessarily when .beads removed","description":"Pre-commit hook fails on bd sync when .beads directory exists but user is on branch without beads. Should exit gracefully. See GitHub issue #483.","status":"tombstone","priority":2,"issue_type":"bug","created_at":"2025-12-16T01:03:40.049785-08:00","updated_at":"2025-12-17T16:11:17.070763-08:00","deleted_at":"2025-12-17T16:11:17.070763-08:00","deleted_by":"batch delete","delete_reason":"batch delete","original_type":"bug"}
|
||||
{"id":"bd-d3e5","title":"Test issue 2","description":"","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-14T11:21:13.878680387-07:00","updated_at":"2025-12-14T11:21:13.878680387-07:00","closed_at":"2025-12-14T00:32:13.890274-08:00"}
|
||||
@@ -119,12 +120,14 @@
|
||||
{"id":"bd-hlsw.4","title":"Sync branch integrity guards","description":"Track sync branch parent commit. If sync branch was force-pushed, warn user and require confirmation before proceeding. Add option to reset to remote if user accepts rebase. Prevents silent corruption from forced pushes.","status":"open","priority":3,"issue_type":"task","created_at":"2025-12-14T10:40:20.645402352-07:00","updated_at":"2025-12-14T10:40:20.645402352-07:00","dependencies":[{"issue_id":"bd-hlsw.4","depends_on_id":"bd-hlsw","type":"parent-child","created_at":"2025-12-14T10:40:20.646425761-07:00","created_by":"daemon"}]}
|
||||
{"id":"bd-hnkg","title":"GH#540: Add silent quick-capture mode (bd q)","description":"Add bd q alias for quick capture that outputs only issue ID. Useful for piping/scripting. See GitHub issue #540.","status":"tombstone","priority":2,"issue_type":"feature","created_at":"2025-12-16T01:03:38.260135-08:00","updated_at":"2025-12-17T16:11:17.070763-08:00","deleted_at":"2025-12-17T16:11:17.070763-08:00","deleted_by":"batch delete","delete_reason":"batch delete","original_type":"feature"}
|
||||
{"id":"bd-hy9p","title":"Add --body-file flag to bd create for reading descriptions from files","description":"## Problem\n\nCreating issues with long/complex descriptions via CLI requires shell escaping gymnastics:\n\n```bash\n# Current workaround - awkward heredoc quoting\nbd create --title=\"...\" --description=\"$(cat \u003c\u003c'EOF'\n...markdown...\nEOF\n)\"\n\n# Often fails with quote escaping errors in eval context\n# Agents resort to writing temp files then reading them\n```\n\n## Proposed Solution\n\nAdd `--body-file` and `--description-file` flags to read description from a file, matching `gh` CLI pattern.\n\n```bash\n# Natural pattern that aligns with training data\ncat \u003e /tmp/desc.md \u003c\u003c 'EOF'\n...markdown content...\nEOF\n\nbd create --title=\"...\" --body-file=/tmp/desc.md\n```\n\n## Implementation\n\n### 1. Add new flags to `bd create`\n\n```go\ncreateCmd.Flags().String(\"body-file\", \"\", \"Read description from file (use - for stdin)\")\ncreateCmd.Flags().String(\"description-file\", \"\", \"Alias for --body-file\")\n```\n\n### 2. Flag precedence\n\n- If `--body-file` or `--description-file` is provided, read from file\n- If value is `-`, read from stdin\n- Otherwise fall back to `--body` or `--description` flag\n- If neither provided, description is empty (current behavior)\n\n### 3. Error handling\n\n- File doesn't exist → clear error message\n- File not readable → clear error message\n- stdin specified but not available → clear error message\n\n## Benefits\n\n✅ **Matches training data**: `gh issue create --body-file file.txt` is a common pattern\n✅ **No shell escaping issues**: File content is read directly\n✅ **Works with any content**: Markdown, special characters, quotes, etc.\n✅ **Agent-friendly**: Agents already write complex content to temp files\n✅ **User-friendly**: Easier for humans too when pasting long descriptions\n\n## Related Commands\n\nConsider adding similar support to:\n- `bd update --body-file` (for updating descriptions)\n- `bd comment --body-file` (if/when we add comments)\n\n## Examples\n\n```bash\n# From file\nbd create --title=\"Add new feature\" --body-file=feature.md\n\n# From stdin\necho \"Quick description\" | bd create --title=\"Bug fix\" --body-file=-\n\n# With other flags\nbd create \\\n --title=\"Security issue\" \\\n --type=bug \\\n --priority=0 \\\n --body-file=security-report.md \\\n --label=security\n```\n\n## Testing\n\n- Test with normal files\n- Test with stdin (`-`)\n- Test with non-existent files (error handling)\n- Test with binary files (should handle gracefully)\n- Test with empty files (valid - empty description)\n- Test that `--description-file` and `--body-file` are equivalent aliases","status":"closed","priority":1,"issue_type":"feature","created_at":"2025-11-22T00:02:08.762684-08:00","updated_at":"2025-12-17T23:13:40.536024-08:00","closed_at":"2025-12-17T17:28:52.505239-08:00"}
|
||||
{"id":"bd-in7","title":"Test message","description":"Hello world","status":"closed","priority":2,"issue_type":"message","assignee":"test-agent","created_at":"2025-12-17T23:16:13.184946-08:00","updated_at":"2025-12-18T00:35:08.403989-08:00","closed_at":"2025-12-17T23:37:38.563369-08:00"}
|
||||
{"id":"bd-io8c","title":"Improve test coverage for internal/syncbranch (33.0% → 70%)","description":"The syncbranch package has only 33.0% test coverage. This package handles git sync operations and is critical for data integrity.\n\nCurrent coverage: 33.0%\nTarget coverage: 70%","status":"open","priority":1,"issue_type":"task","created_at":"2025-12-13T20:43:02.079145-08:00","updated_at":"2025-12-13T21:01:14.972533-08:00"}
|
||||
{"id":"bd-iq7n","title":"Audit and fix JSONL filename mismatches across all repo clones","description":"## Problem\n\nMultiple clones of repos are configured with different JSONL filenames (issues.jsonl vs beads.jsonl), causing:\n1. JSONL files to be resurrected after deletion (one clone pushes issues.jsonl, another pushes beads.jsonl)\n2. Agents unable to see issues filed by other agents after sync\n3. Merge conflicts and data inconsistencies\n\n## Root Cause\n\nWhen repos were \"bd doctored\" or initialized at different times, some got issues.jsonl (old default) and others got beads.jsonl (Beads repo specific). These clones push their respective files, creating duplicates.\n\n## Task\n\nScan all repo clones under ~/src/ (1-2 levels deep) and standardize their JSONL configuration.\n\n### Step 1: Find all beads-enabled repos\n\n```bash\n# Find all directories named 'beads' at levels 1-2 under ~/src/\nfind ~/src -maxdepth 2 -type d -name beads\n```\n\n### Step 2: For each repo found, check configuration\n\nFor each directory from Step 1, check:\n- Does `.beads/metadata.json` exist?\n- What is the `jsonl_export` value?\n- What JSONL files actually exist in `.beads/`?\n- Are there multiple JSONL files (problem!)?\n\n### Step 3: Create audit report\n\nGenerate a report showing:\n```\nRepo Path | Config | Actual Files | Status\n----------------------------------- | ------------- | ---------------------- | --------\n~/src/beads | beads.jsonl | beads.jsonl | OK\n~/src/dave/beads | issues.jsonl | issues.jsonl | MISMATCH\n~/src/emma/beads | issues.jsonl | issues.jsonl, beads.jsonl | DUPLICATE!\n```\n\n### Step 4: Determine canonical name for each repo\n\nFor repos that are the SAME git repository (check `git remote -v`):\n- Group them together\n- Determine which JSONL filename should be canonical (majority wins, or beads.jsonl for the beads repo itself)\n- List which clones need to be updated\n\n### Step 5: Generate fix script\n\nCreate a script that for each mismatched clone:\n1. Updates `.beads/metadata.json` to use the canonical name\n2. If JSONL file needs renaming: `git mv .beads/old.jsonl .beads/new.jsonl`\n3. Removes any duplicate JSONL files: `git rm .beads/duplicate.jsonl`\n4. Commits the change\n5. Syncs: `bd sync`\n\n### Expected Output\n\n1. Audit report showing all repos and their config status\n2. List of repos grouped by git remote (same repository)\n3. Fix script or manual instructions for standardizing each repo\n4. Verification that after fixes, all clones of the same repo use the same JSONL filename\n\n### Edge Cases\n\n- Handle repos without metadata.json (use default discovery)\n- Handle repos with no git remote (standalone/local)\n- Handle repos that are not git repositories\n- Don't modify repos with uncommitted changes (warn instead)\n\n### Success Criteria\n\n- All clones of the same git repository use the same JSONL filename\n- No duplicate JSONL files in any repo\n- All configurations documented in metadata.json\n- bd doctor passes on all repos","status":"closed","priority":0,"issue_type":"task","created_at":"2025-11-21T23:58:35.044762-08:00","updated_at":"2025-12-17T23:13:40.531403-08:00","closed_at":"2025-12-17T16:50:59.510972-08:00"}
|
||||
{"id":"bd-j3il","title":"Add bd reset command for clean slate restart","description":"Implement a command to reset beads to a clean starting state.\n\n**Context:** GitHub issue #479 - users sometimes get beads into an invalid state after updates, and there's no clean way to start fresh. The git backup/restore mechanism that protects against accidental deletion also makes it hard to intentionally reset.\n\n**Current workaround** (from maphew):\n```bash\nbd daemons killall\ngit rm .beads/*.jsonl\ngit commit -m 'remove old issues'\nrm .beads/*\nbd init\nbd onboard\n```\n\n**Desired:** A proper `bd reset` command that handles this cleanly and safely.","status":"closed","priority":2,"issue_type":"feature","created_at":"2025-12-13T08:41:34.956552+11:00","updated_at":"2025-12-13T08:43:49.970591+11:00","closed_at":"2025-12-13T08:43:49.970591+11:00"}
|
||||
{"id":"bd-j6lr","title":"GH#402: Add --parent flag documentation to bd onboard","description":"bd onboard output is missing --parent flag for epic subtasks. Agents guess wrong syntax (--deps parent:). See GitHub issue #402.","status":"tombstone","priority":2,"issue_type":"task","created_at":"2025-12-16T01:03:56.594829-08:00","updated_at":"2025-12-17T16:11:17.070763-08:00","deleted_at":"2025-12-17T16:11:17.070763-08:00","deleted_by":"batch delete","delete_reason":"batch delete","original_type":"task"}
|
||||
{"id":"bd-jgxi","title":"Auto-migrate database on CLI version bump","description":"When CLI is upgraded (e.g., 0.24.0 → 0.24.1), database version becomes stale. Add auto-migration in PersistentPreRun or daemon startup. Check dbVersion != CLIVersion and run bd migrate automatically. Fixes recurring UX issue where bd doctor shows version mismatch after every CLI upgrade.","status":"closed","priority":0,"issue_type":"feature","created_at":"2025-11-21T23:16:09.004619-08:00","updated_at":"2025-12-17T23:13:40.535453-08:00","closed_at":"2025-12-17T17:15:43.605762-08:00","dependencies":[{"issue_id":"bd-jgxi","depends_on_id":"bd-tbz3","type":"parent-child","created_at":"2025-11-21T23:16:09.005513-08:00","created_by":"daemon"}]}
|
||||
{"id":"bd-jvu","title":"Add bd update --parent flag to change issue parent","description":"Allow changing an issue's parent with bd update --parent \u003cnew-parent-id\u003e. Useful for reorganizing tasks under different epics or moving issues between hierarchies. Should update the parent-child dependency relationship.","status":"closed","priority":3,"issue_type":"feature","created_at":"2025-12-17T22:24:07.274485-08:00","updated_at":"2025-12-17T22:34:07.318938-08:00","closed_at":"2025-12-17T22:34:07.318938-08:00"}
|
||||
{"id":"bd-kpy","title":"Sync race: rebase-based divergence recovery resurrects tombstones","description":"## Problem\nWhen two repos sync simultaneously, tombstones can be resurrected:\n\n1. Repo A deletes issue (creates tombstone), pushes to sync branch\n2. Repo B (with 'closed' status) exports and tries to push\n3. Push fails (non-fast-forward)\n4. fetchAndRebaseInWorktree does git rebase\n5. Git rebase applies B's 'closed' patch on top of A's 'tombstone'\n6. TEXT-level rebase doesn't invoke beads merge driver\n7. 'closed' overwrites 'tombstone' = resurrection\n\n## Root Cause\nCommitToSyncBranch uses git rebase for divergence recovery, but rebase is text-level, not content-level. The proper content-level merge in PullFromSyncBranch handles tombstones correctly, but it runs AFTER the problematic push.\n\n## Proposed Fix\nOption 1: Don't push in CommitToSyncBranch - let PullFromSyncBranch handle merge+push\nOption 2: Replace git rebase with content-level merge in fetchAndRebaseInWorktree\nOption 3: Reorder sync steps: Export → Pull/Merge → Commit → Push\n\n## Workaround Applied\nExcluded tombstones from orphan detection warnings (commit 1e97d9cc).\n\nSee also: bd-3852 (Add orphan detection migration)","status":"open","priority":2,"issue_type":"bug","created_at":"2025-12-17T23:29:33.049272-08:00","updated_at":"2025-12-17T23:29:33.049272-08:00"}
|
||||
{"id":"bd-kwro","title":"Beads Messaging \u0026 Knowledge Graph (v0.30.2)","description":"Add messaging semantics and extended graph links to Beads, enabling it to serve as\nthe universal substrate for knowledge work - issues, messages, documents, and threads\nas nodes in a queryable graph.\n\n## Motivation\n\nGas Town (GGT) needs inter-agent communication. Rather than a separate mail system,\ncollapse messaging into Beads - one system, one sync, one query interface, all in git.\n\nThis also positions Beads as a foundation for:\n- Company-wide issue tracking (like Notion)\n- Threaded conversations (like Reddit/Slack)\n- Knowledge graphs with loose associations\n- Arbitrary workflow UIs built on top\n\n## New Issue Type\n\n**message** - ephemeral communication between workers\n- sender: who sent it\n- assignee: recipient\n- priority: P0 (urgent) to P4 (routine)\n- status: open (unread) -\u003e closed (read)\n- ephemeral: true = can be bulk-deleted after swarm\n\n## New Graph Links\n\n**replies_to** - conversation threading\n- Messages reply to messages\n- Enables Reddit-style nested threads\n- Different from parent_id (not hierarchy, its conversation flow)\n\n**relates_to** - loose see also associations\n- Bidirectional knowledge graph edges\n- Not blocking, not hierarchical, just related\n- Enables discovery and traversal\n\n**duplicates** - deduplication at scale\n- Mark issue B as duplicate of canonical issue A\n- Close B, link to A\n- Essential for large issue databases\n\n**supersedes** - version chains\n- Design Doc v2 supersedes Design Doc v1\n- Track evolution of artifacts\n\n## New Fields (optional, any issue type)\n\n- sender (string) - who created this (for messages)\n- ephemeral (boolean) - can be bulk-deleted when closed\n\n## New Commands\n\nMessaging:\n- bd mail send \u003crecipient\u003e -s Subject -m Body\n- bd mail inbox (list open messages for me)\n- bd mail read \u003cid\u003e (show message content)\n- bd mail ack \u003cid\u003e (mark as read/close)\n- bd mail reply \u003cid\u003e -m Response (reply to thread)\n\nGraph links:\n- bd relate \u003cid1\u003e \u003cid2\u003e (create relates_to link)\n- bd duplicate \u003cid\u003e --of \u003ccanonical\u003e (mark as duplicate)\n- bd supersede \u003cid\u003e --with \u003cnew\u003e (mark superseded)\n\nCleanup:\n- bd cleanup --ephemeral (delete closed ephemeral issues)\n\n## Identity Configuration\n\nWorkers need identity for sender field:\n- BEADS_IDENTITY env var\n- Or .beads/config.json: identity field\n\n## Hooks (for GGT integration)\n\nBeads as platform - extensible without knowing about GGT.\nHook files in .beads/hooks/:\n- on_create (runs after bd create)\n- on_update (runs after bd update)\n- on_close (runs after bd close)\n- on_message (runs after bd mail send)\n\nGGT registers hooks to notify daemons of new messages.\n\n## Schema Changes (Migration Required)\n\nAdd to issue schema:\n- type: message (new valid type)\n- sender: string (optional)\n- ephemeral: boolean (optional)\n- replies_to: string (issue ID, optional)\n- relates_to: []string (issue IDs, optional)\n- duplicates: string (canonical issue ID, optional)\n- superseded_by: string (new issue ID, optional)\n\nMigration adds fields as optional - existing beads unchanged.\n\n## Success Criteria\n\n1. bd mail send/inbox/read/ack/reply work end-to-end\n2. replies_to creates proper thread structure\n3. relates_to, duplicates, supersedes links queryable\n4. Hooks fire on create/update/close/message\n5. Identity configurable via env or config\n6. Migration preserves all existing data\n7. All new features have tests","status":"tombstone","priority":0,"issue_type":"epic","created_at":"2025-12-16T03:00:53.912223-08:00","updated_at":"2025-12-17T16:11:17.070763-08:00","deleted_at":"2025-12-17T16:11:17.070763-08:00","deleted_by":"batch delete","delete_reason":"batch delete","original_type":"epic"}
|
||||
{"id":"bd-kwro.1","title":"Schema: Add message type and new fields","description":"Add to internal/storage/sqlite/schema.go and models:\n\nNew issue_type value:\n- message\n\nNew optional fields on Issue struct:\n- Sender string (who sent this)\n- Ephemeral bool (can be bulk-deleted)\n- RepliesTo string (issue ID for threading)\n- RelatesTo []string (issue IDs for knowledge graph)\n- Duplicates string (canonical issue ID)\n- SupersededBy string (replacement issue ID)\n\nUpdate:\n- internal/storage/sqlite/schema.go - add columns\n- internal/models/issue.go - add fields to struct\n- internal/storage/sqlite/sqlite.go - CRUD operations\n- Create migration from v0.30.1\n\nEnsure backward compatibility - all new fields optional.","status":"tombstone","priority":0,"issue_type":"task","created_at":"2025-12-16T03:01:19.777604-08:00","updated_at":"2025-12-17T16:11:17.070763-08:00","deleted_at":"2025-12-17T16:11:17.070763-08:00","deleted_by":"batch delete","delete_reason":"batch delete","original_type":"task"}
|
||||
{"id":"bd-kwro.10","title":"Tests for messaging and graph links","description":"Comprehensive test coverage for all new features.\n\nTest files:\n- cmd/bd/mail_test.go - mail command tests\n- internal/storage/sqlite/graph_links_test.go - graph link tests\n- internal/hooks/hooks_test.go - hook execution tests\n\nTest cases:\n- Mail send/inbox/read/ack lifecycle\n- Thread creation and traversal (replies_to)\n- Bidirectional relates_to\n- Duplicate marking and queries\n- Supersedes chains\n- Ephemeral cleanup\n- Identity resolution priority\n- Hook execution (mock hooks)\n- Schema migration preserves data\n\nTarget: \u003e80% coverage on new code","status":"tombstone","priority":2,"issue_type":"task","created_at":"2025-12-16T03:02:34.050136-08:00","updated_at":"2025-12-17T16:11:17.070763-08:00","deleted_at":"2025-12-17T16:11:17.070763-08:00","deleted_by":"batch delete","delete_reason":"batch delete","original_type":"task"}
|
||||
@@ -156,28 +159,28 @@
|
||||
{"id":"bd-ola6","title":"Implement transaction retry logic for SQLITE_BUSY","description":"BEGIN IMMEDIATE fails immediately on SQLITE_BUSY instead of retrying with exponential backoff.\n\nLocation: internal/storage/sqlite/sqlite.go:223-225\n\nProblem:\n- Under concurrent write load, BEGIN IMMEDIATE can fail with SQLITE_BUSY\n- Current implementation fails immediately instead of retrying\n- Results in spurious failures under normal concurrent usage\n\nSolution: Implement exponential backoff retry:\n- Retry up to N times (e.g., 5)\n- Backoff: 10ms, 20ms, 40ms, 80ms, 160ms\n- Check for context cancellation between retries\n- Only retry on SQLITE_BUSY/database locked errors\n\nImpact: Spurious failures under concurrent write load\n\nEffort: 3 hours","status":"open","priority":1,"issue_type":"feature","created_at":"2025-11-16T14:51:31.247147-08:00","updated_at":"2025-11-16T14:51:31.247147-08:00"}
|
||||
{"id":"bd-ork0","title":"Add comments to 30+ silently ignored errors or fix them","description":"Code health review found 30+ instances of error suppression using blank identifier without explanation:\n\nGood examples (with comments):\n- merge.go: _ = gitRmCmd.Run() // Ignore errors\n- daemon_watcher.go: _ = watcher.Add(...) // Ignore error\n\nBad examples (no context):\n- create.go:213: dbPrefix, _ = store.GetConfig(ctx, \"issue_prefix\")\n- daemon_sync_branch.go: _ = daemonClient.Close()\n- migrate_hash_ids.go, version_tracking.go: _ = store.Close()\n\nFix: Add comments explaining WHY errors are ignored, or handle them properly.","status":"open","priority":3,"issue_type":"task","created_at":"2025-12-16T18:17:25.899372-08:00","updated_at":"2025-12-16T18:17:25.899372-08:00","dependencies":[{"issue_id":"bd-ork0","depends_on_id":"bd-tggf","type":"blocks","created_at":"2025-12-16T18:19:06.275843-08:00","created_by":"daemon"}]}
|
||||
{"id":"bd-otf4","title":"Code Review: PR #481 - Context Engineering Optimizations","description":"Comprehensive code review of the merged context engineering PR (PR #481) that reduces MCP context usage by 80-90%.\n\n## Summary\nThe PR successfully implements lazy tool schema loading and minimal issue models to reduce context window usage. Overall implementation is solid and well-tested.\n\n## Positive Findings\n✅ Well-designed models (IssueMinimal, CompactedResult)\n✅ Comprehensive test coverage (28 tests, all passing)\n✅ Clear documentation and comments\n✅ Backward compatibility preserved (show() still returns full Issue)\n✅ Sensible defaults (COMPACTION_THRESHOLD=20, PREVIEW_COUNT=5)\n✅ Tool catalog complete with all 15 tools documented\n\n## Issues Identified\nSee linked issues for specific followup tasks.\n\n## Context Engineering Architecture\n- discover_tools(): List tool names only (~500 bytes vs ~15KB)\n- get_tool_info(name): Get specific tool details on-demand\n- IssueMinimal: Lightweight model for list views (~80 bytes vs ~400 bytes)\n- CompactedResult: Auto-compacts results with \u003e20 issues\n- _to_minimal(): Conversion function (efficient, no N+1 issues)","status":"open","priority":2,"issue_type":"task","created_at":"2025-12-14T14:24:13.523532-08:00","updated_at":"2025-12-14T14:24:13.523532-08:00"}
|
||||
{"id":"bd-pbh","title":"Release v0.30.4","description":"## Version Bump Workflow\n\nCoordinating release from 0.30.3 to 0.30.4.\n\n### Components Updated\n- Go CLI (cmd/bd/version.go)\n- Claude Plugin (.claude-plugin/*.json)\n- MCP Server (integrations/beads-mcp/)\n- npm Package (npm-package/package.json)\n- Git hooks (cmd/bd/templates/hooks/)\n\n### Release Channels\n- GitHub Releases (GoReleaser)\n- PyPI (beads-mcp)\n- npm (@beads/cli)\n- Homebrew (homebrew-beads tap)\n","status":"closed","priority":1,"issue_type":"epic","created_at":"2025-12-17T21:19:10.926133-08:00","updated_at":"2025-12-17T21:46:46.192948-08:00","closed_at":"2025-12-17T21:46:46.192948-08:00"}
|
||||
{"id":"bd-pbh.1","title":"Update cmd/bd/version.go to 0.30.4","description":"Update the Version constant in cmd/bd/version.go:\n```go\nVersion = \"0.30.4\"\n```\n\n\n```verify\ngrep -q 'Version = \"0.30.4\"' cmd/bd/version.go\n```","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-17T21:19:10.9462-08:00","updated_at":"2025-12-17T21:46:46.20387-08:00","closed_at":"2025-12-17T21:46:46.20387-08:00","dependencies":[{"issue_id":"bd-pbh.1","depends_on_id":"bd-pbh","type":"parent-child","created_at":"2025-12-17T21:19:10.946633-08:00","created_by":"daemon"}]}
|
||||
{"id":"bd-pbh.10","title":"Run check-versions.sh - all must pass","description":"Run the version consistency check:\n```bash\n./scripts/check-versions.sh\n```\n\nAll versions must match 0.30.4.\n\n\n```verify\n./scripts/check-versions.sh\n```","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-17T21:19:11.047311-08:00","updated_at":"2025-12-17T21:46:46.28316-08:00","closed_at":"2025-12-17T21:46:46.28316-08:00","dependencies":[{"issue_id":"bd-pbh.10","depends_on_id":"bd-pbh","type":"parent-child","created_at":"2025-12-17T21:19:11.047888-08:00","created_by":"daemon"},{"issue_id":"bd-pbh.10","depends_on_id":"bd-pbh.1","type":"blocks","created_at":"2025-12-17T21:19:11.159084-08:00","created_by":"daemon"},{"issue_id":"bd-pbh.10","depends_on_id":"bd-pbh.4","type":"blocks","created_at":"2025-12-17T21:19:11.168248-08:00","created_by":"daemon"},{"issue_id":"bd-pbh.10","depends_on_id":"bd-pbh.5","type":"blocks","created_at":"2025-12-17T21:19:11.177869-08:00","created_by":"daemon"},{"issue_id":"bd-pbh.10","depends_on_id":"bd-pbh.6","type":"blocks","created_at":"2025-12-17T21:19:11.187629-08:00","created_by":"daemon"},{"issue_id":"bd-pbh.10","depends_on_id":"bd-pbh.7","type":"blocks","created_at":"2025-12-17T21:19:11.199955-08:00","created_by":"daemon"},{"issue_id":"bd-pbh.10","depends_on_id":"bd-pbh.8","type":"blocks","created_at":"2025-12-17T21:19:11.211479-08:00","created_by":"daemon"},{"issue_id":"bd-pbh.10","depends_on_id":"bd-pbh.9","type":"blocks","created_at":"2025-12-17T21:19:11.224059-08:00","created_by":"daemon"}]}
|
||||
{"id":"bd-pbh.11","title":"Commit changes and create v0.30.4 tag","description":"```bash\ngit add -A\ngit commit -m \"chore: Bump version to 0.30.4\"\ngit tag -a v0.30.4 -m \"Release v0.30.4\"\n```\n\n\n```verify\ngit describe --tags --exact-match HEAD 2\u003e/dev/null | grep -q 'v0.30.4'\n```","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-17T21:19:11.056575-08:00","updated_at":"2025-12-17T21:46:46.292166-08:00","closed_at":"2025-12-17T21:46:46.292166-08:00","dependencies":[{"issue_id":"bd-pbh.11","depends_on_id":"bd-pbh","type":"parent-child","created_at":"2025-12-17T21:19:11.056934-08:00","created_by":"daemon"},{"issue_id":"bd-pbh.11","depends_on_id":"bd-pbh.10","type":"blocks","created_at":"2025-12-17T21:19:11.234175-08:00","created_by":"daemon"},{"issue_id":"bd-pbh.11","depends_on_id":"bd-pbh.2","type":"blocks","created_at":"2025-12-17T21:19:11.245316-08:00","created_by":"daemon"},{"issue_id":"bd-pbh.11","depends_on_id":"bd-pbh.3","type":"blocks","created_at":"2025-12-17T21:19:11.255362-08:00","created_by":"daemon"}]}
|
||||
{"id":"bd-pbh.12","title":"Push commit and tag to origin","description":"```bash\ngit push origin main\ngit push origin v0.30.4\n```\n\nThis triggers GitHub Actions:\n- GoReleaser build\n- PyPI publish\n- npm publish\n\n\n```verify\ngit ls-remote origin refs/tags/v0.30.4 | grep -q 'v0.30.4'\n```","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-17T21:19:11.066074-08:00","updated_at":"2025-12-17T21:46:46.301948-08:00","closed_at":"2025-12-17T21:46:46.301948-08:00","dependencies":[{"issue_id":"bd-pbh.12","depends_on_id":"bd-pbh","type":"parent-child","created_at":"2025-12-17T21:19:11.066442-08:00","created_by":"daemon"},{"issue_id":"bd-pbh.12","depends_on_id":"bd-pbh.11","type":"blocks","created_at":"2025-12-17T21:19:11.265986-08:00","created_by":"daemon"}]}
|
||||
{"id":"bd-pbh.13","title":"Monitor GoReleaser CI job","description":"Watch the GoReleaser action:\nhttps://github.com/steveyegge/beads/actions/workflows/release.yml\n\nShould complete in ~10 minutes and create:\n- GitHub Release with binaries for all platforms\n- Checksums and signatures\n\nCheck status:\n```bash\ngh run list --workflow=release.yml -L 1\ngh run watch # to monitor live\n```\n\nVerify release exists:\n```bash\ngh release view v0.30.4\n```\n\n\n```verify\ngh release view v0.30.4 --json tagName -q .tagName | grep -q 'v0.30.4'\n```","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-17T21:19:11.074476-08:00","updated_at":"2025-12-17T21:46:46.311506-08:00","closed_at":"2025-12-17T21:46:46.311506-08:00","dependencies":[{"issue_id":"bd-pbh.13","depends_on_id":"bd-pbh","type":"parent-child","created_at":"2025-12-17T21:19:11.074833-08:00","created_by":"daemon"},{"issue_id":"bd-pbh.13","depends_on_id":"bd-pbh.12","type":"blocks","created_at":"2025-12-17T21:19:11.279092-08:00","created_by":"daemon"}]}
|
||||
{"id":"bd-pbh.14","title":"Monitor PyPI publish","description":"Watch the PyPI publish action:\nhttps://github.com/steveyegge/beads/actions/workflows/pypi-publish.yml\n\nVerify at: https://pypi.org/project/beads-mcp/0.30.4/\n\nCheck:\n```bash\npip index versions beads-mcp 2\u003e/dev/null | grep -q '0.30.4'\n```\n\n\n```verify\npip index versions beads-mcp 2\u003e/dev/null | grep -q '0.30.4' || curl -s https://pypi.org/pypi/beads-mcp/json | jq -e '.releases[\"0.30.4\"]'\n```","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-17T21:19:11.083809-08:00","updated_at":"2025-12-17T21:46:46.320922-08:00","closed_at":"2025-12-17T21:46:46.320922-08:00","dependencies":[{"issue_id":"bd-pbh.14","depends_on_id":"bd-pbh","type":"parent-child","created_at":"2025-12-17T21:19:11.084126-08:00","created_by":"daemon"},{"issue_id":"bd-pbh.14","depends_on_id":"bd-pbh.12","type":"blocks","created_at":"2025-12-17T21:19:11.289698-08:00","created_by":"daemon"}]}
|
||||
{"id":"bd-pbh.15","title":"Monitor npm publish","description":"Watch the npm publish action:\nhttps://github.com/steveyegge/beads/actions/workflows/npm-publish.yml\n\nVerify at: https://www.npmjs.com/package/@anthropics/claude-code-beads-plugin/v/0.30.4\n\nCheck:\n```bash\nnpm view @anthropics/claude-code-beads-plugin@0.30.4 version\n```\n\n\n```verify\nnpm view @anthropics/claude-code-beads-plugin@0.30.4 version 2\u003e/dev/null | grep -q '0.30.4'\n```","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-17T21:19:11.091806-08:00","updated_at":"2025-12-17T21:46:46.333213-08:00","closed_at":"2025-12-17T21:46:46.333213-08:00","dependencies":[{"issue_id":"bd-pbh.15","depends_on_id":"bd-pbh","type":"parent-child","created_at":"2025-12-17T21:19:11.092205-08:00","created_by":"daemon"},{"issue_id":"bd-pbh.15","depends_on_id":"bd-pbh.12","type":"blocks","created_at":"2025-12-17T21:19:11.301843-08:00","created_by":"daemon"}]}
|
||||
{"id":"bd-pbh.16","title":"Update Homebrew formula","description":"After GoReleaser completes, the Homebrew tap should be auto-updated.\n\nIf manual update needed:\n```bash\n./scripts/update-homebrew.sh v0.30.4\n```\n\nOr manually update steveyegge/homebrew-beads with new SHA256.\n\nVerify:\n```bash\nbrew update\nbrew info beads\n```\n","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-17T21:19:11.100213-08:00","updated_at":"2025-12-17T21:46:46.341942-08:00","closed_at":"2025-12-17T21:46:46.341942-08:00","dependencies":[{"issue_id":"bd-pbh.16","depends_on_id":"bd-pbh","type":"parent-child","created_at":"2025-12-17T21:19:11.100541-08:00","created_by":"daemon"},{"issue_id":"bd-pbh.16","depends_on_id":"bd-pbh.13","type":"blocks","created_at":"2025-12-17T21:19:11.312625-08:00","created_by":"daemon"}]}
|
||||
{"id":"bd-pbh.17","title":"Install 0.30.4 Go binary locally","description":"Rebuild and install the Go binary:\n```bash\ngo install ./cmd/bd\n# OR\nmake install\n```\n\nVerify:\n```bash\nbd --version\n```\n\n\n```verify\nbd --version 2\u003e\u00261 | grep -q '0.30.4'\n```","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-17T21:19:11.108597-08:00","updated_at":"2025-12-17T21:46:46.352702-08:00","closed_at":"2025-12-17T21:46:46.352702-08:00","dependencies":[{"issue_id":"bd-pbh.17","depends_on_id":"bd-pbh","type":"parent-child","created_at":"2025-12-17T21:19:11.108917-08:00","created_by":"daemon"},{"issue_id":"bd-pbh.17","depends_on_id":"bd-pbh.13","type":"blocks","created_at":"2025-12-17T21:19:11.322091-08:00","created_by":"daemon"}]}
|
||||
{"id":"bd-pbh.18","title":"Restart beads daemon","description":"Kill any running daemons so they pick up the new version:\n```bash\nbd daemons killall\n```\n\nStart fresh daemon:\n```bash\nbd list # triggers daemon start\n```\n\nVerify daemon version:\n```bash\nbd version --daemon\n```\n\n\n```verify\nbd version --daemon 2\u003e\u00261 | grep -q '0.30.4'\n```","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-17T21:19:11.11636-08:00","updated_at":"2025-12-17T21:46:46.364842-08:00","closed_at":"2025-12-17T21:46:46.364842-08:00","dependencies":[{"issue_id":"bd-pbh.18","depends_on_id":"bd-pbh","type":"parent-child","created_at":"2025-12-17T21:19:11.116706-08:00","created_by":"daemon"},{"issue_id":"bd-pbh.18","depends_on_id":"bd-pbh.17","type":"blocks","created_at":"2025-12-17T21:19:11.330411-08:00","created_by":"daemon"}]}
|
||||
{"id":"bd-pbh.19","title":"Install 0.30.4 MCP server locally","description":"Upgrade the MCP server (after PyPI publish):\n```bash\npip install --upgrade beads-mcp\n# OR if using uv:\nuv tool upgrade beads-mcp\n```\n\nVerify:\n```bash\npip show beads-mcp | grep Version\n```\n\n\n```verify\npip show beads-mcp 2\u003e/dev/null | grep -q 'Version: 0.30.4'\n```","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-17T21:19:11.124496-08:00","updated_at":"2025-12-17T21:46:46.372989-08:00","closed_at":"2025-12-17T21:46:46.372989-08:00","dependencies":[{"issue_id":"bd-pbh.19","depends_on_id":"bd-pbh","type":"parent-child","created_at":"2025-12-17T21:19:11.124829-08:00","created_by":"daemon"},{"issue_id":"bd-pbh.19","depends_on_id":"bd-pbh.14","type":"blocks","created_at":"2025-12-17T21:19:11.343558-08:00","created_by":"daemon"}]}
|
||||
{"id":"bd-pbh.2","title":"Update CHANGELOG.md for 0.30.4","description":"1. Change `## [Unreleased]` to `## [0.30.4] - 2025-12-17`\n2. Add new empty `## [Unreleased]` section at top\n3. Ensure all changes since 0.30.3 are documented\n","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-17T21:19:10.956332-08:00","updated_at":"2025-12-17T21:46:46.214512-08:00","closed_at":"2025-12-17T21:46:46.214512-08:00","dependencies":[{"issue_id":"bd-pbh.2","depends_on_id":"bd-pbh","type":"parent-child","created_at":"2025-12-17T21:19:10.95683-08:00","created_by":"daemon"}]}
|
||||
{"id":"bd-pbh.20","title":"Update git hooks","description":"Install the updated hooks:\n```bash\nbd hooks install\n```\n\nVerify hook version:\n```bash\ngrep 'bd-hooks-version' .git/hooks/pre-commit\n```\n\n\n```verify\ngrep -q 'bd-hooks-version: 0.30.4' .git/hooks/pre-commit 2\u003e/dev/null || echo 'Hooks may not be installed - verify manually'\n```","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-17T21:19:11.13198-08:00","updated_at":"2025-12-17T21:46:46.381519-08:00","closed_at":"2025-12-17T21:46:46.381519-08:00","dependencies":[{"issue_id":"bd-pbh.20","depends_on_id":"bd-pbh","type":"parent-child","created_at":"2025-12-17T21:19:11.132306-08:00","created_by":"daemon"},{"issue_id":"bd-pbh.20","depends_on_id":"bd-pbh.17","type":"blocks","created_at":"2025-12-17T21:19:11.352288-08:00","created_by":"daemon"}]}
|
||||
{"id":"bd-pbh.21","title":"Final release verification","description":"Verify all release artifacts are accessible:\n\n- [ ] `bd --version` shows 0.30.4\n- [ ] `bd version --daemon` shows 0.30.4\n- [ ] GitHub release exists: https://github.com/steveyegge/beads/releases/tag/v0.30.4\n- [ ] `brew upgrade beads \u0026\u0026 bd --version` shows 0.30.4 (if using Homebrew)\n- [ ] `pip show beads-mcp` shows 0.30.4\n- [ ] npm package available at 0.30.4\n- [ ] `bd info --whats-new` shows 0.30.4 notes\n\nRun final checks:\n```bash\nbd --version\nbd version --daemon\npip show beads-mcp | grep Version\nbd info --whats-new\n```\n","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-17T21:19:11.141249-08:00","updated_at":"2025-12-17T21:46:46.390985-08:00","closed_at":"2025-12-17T21:46:46.390985-08:00","dependencies":[{"issue_id":"bd-pbh.21","depends_on_id":"bd-pbh","type":"parent-child","created_at":"2025-12-17T21:19:11.141549-08:00","created_by":"daemon"},{"issue_id":"bd-pbh.21","depends_on_id":"bd-pbh.18","type":"blocks","created_at":"2025-12-17T21:19:11.364839-08:00","created_by":"daemon"},{"issue_id":"bd-pbh.21","depends_on_id":"bd-pbh.19","type":"blocks","created_at":"2025-12-17T21:19:11.373656-08:00","created_by":"daemon"},{"issue_id":"bd-pbh.21","depends_on_id":"bd-pbh.20","type":"blocks","created_at":"2025-12-17T21:19:11.382-08:00","created_by":"daemon"},{"issue_id":"bd-pbh.21","depends_on_id":"bd-pbh.15","type":"blocks","created_at":"2025-12-17T21:19:11.389733-08:00","created_by":"daemon"},{"issue_id":"bd-pbh.21","depends_on_id":"bd-pbh.16","type":"blocks","created_at":"2025-12-17T21:19:11.398347-08:00","created_by":"daemon"}]}
|
||||
{"id":"bd-pbh.3","title":"Add 0.30.4 to info.go release notes","description":"Update cmd/bd/info.go versionChanges map with release notes for 0.30.4.\nInclude any workflow-impacting changes for --whats-new output.\n","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-17T21:19:10.966781-08:00","updated_at":"2025-12-17T21:46:46.222445-08:00","closed_at":"2025-12-17T21:46:46.222445-08:00","dependencies":[{"issue_id":"bd-pbh.3","depends_on_id":"bd-pbh","type":"parent-child","created_at":"2025-12-17T21:19:10.967287-08:00","created_by":"daemon"},{"issue_id":"bd-pbh.3","depends_on_id":"bd-pbh.2","type":"blocks","created_at":"2025-12-17T21:19:11.149584-08:00","created_by":"daemon"}]}
|
||||
{"id":"bd-pbh.4","title":"Update .claude-plugin/plugin.json to 0.30.4","description":"Update version field in .claude-plugin/plugin.json:\n```json\n\"version\": \"0.30.4\"\n```\n\n\n```verify\njq -e '.version == \"0.30.4\"' .claude-plugin/plugin.json\n```","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-17T21:19:10.976866-08:00","updated_at":"2025-12-17T21:46:46.23159-08:00","closed_at":"2025-12-17T21:46:46.23159-08:00","dependencies":[{"issue_id":"bd-pbh.4","depends_on_id":"bd-pbh","type":"parent-child","created_at":"2025-12-17T21:19:10.97729-08:00","created_by":"daemon"}]}
|
||||
{"id":"bd-pbh.5","title":"Update .claude-plugin/marketplace.json to 0.30.4","description":"Update version field in .claude-plugin/marketplace.json:\n```json\n\"version\": \"0.30.4\"\n```\n\n\n```verify\njq -e '.plugins[0].version == \"0.30.4\"' .claude-plugin/marketplace.json\n```","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-17T21:19:10.985619-08:00","updated_at":"2025-12-17T21:46:46.239122-08:00","closed_at":"2025-12-17T21:46:46.239122-08:00","dependencies":[{"issue_id":"bd-pbh.5","depends_on_id":"bd-pbh","type":"parent-child","created_at":"2025-12-17T21:19:10.985942-08:00","created_by":"daemon"}]}
|
||||
{"id":"bd-pbh.6","title":"Update integrations/beads-mcp/pyproject.toml to 0.30.4","description":"Update version in pyproject.toml:\n```toml\nversion = \"0.30.4\"\n```\n\n\n```verify\ngrep -q 'version = \"0.30.4\"' integrations/beads-mcp/pyproject.toml\n```","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-17T21:19:10.994004-08:00","updated_at":"2025-12-17T21:46:46.246574-08:00","closed_at":"2025-12-17T21:46:46.246574-08:00","dependencies":[{"issue_id":"bd-pbh.6","depends_on_id":"bd-pbh","type":"parent-child","created_at":"2025-12-17T21:19:10.994376-08:00","created_by":"daemon"}]}
|
||||
{"id":"bd-pbh.7","title":"Update beads_mcp/__init__.py to 0.30.4","description":"Update __version__ in integrations/beads-mcp/src/beads_mcp/__init__.py:\n```python\n__version__ = \"0.30.4\"\n```\n\n\n```verify\ngrep -q '__version__ = \"0.30.4\"' integrations/beads-mcp/src/beads_mcp/__init__.py\n```","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-17T21:19:11.005334-08:00","updated_at":"2025-12-17T21:46:46.254885-08:00","closed_at":"2025-12-17T21:46:46.254885-08:00","dependencies":[{"issue_id":"bd-pbh.7","depends_on_id":"bd-pbh","type":"parent-child","created_at":"2025-12-17T21:19:11.005699-08:00","created_by":"daemon"}]}
|
||||
{"id":"bd-pbh.8","title":"Update npm-package/package.json to 0.30.4","description":"Update version field in npm-package/package.json:\n```json\n\"version\": \"0.30.4\"\n```\n\n\n```verify\njq -e '.version == \"0.30.4\"' npm-package/package.json\n```","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-17T21:19:11.014905-08:00","updated_at":"2025-12-17T21:46:46.268821-08:00","closed_at":"2025-12-17T21:46:46.268821-08:00","dependencies":[{"issue_id":"bd-pbh.8","depends_on_id":"bd-pbh","type":"parent-child","created_at":"2025-12-17T21:19:11.01529-08:00","created_by":"daemon"}]}
|
||||
{"id":"bd-pbh.9","title":"Update hook templates to 0.30.4","description":"Update bd-hooks-version comment in all 4 hook templates:\n- cmd/bd/templates/hooks/pre-commit\n- cmd/bd/templates/hooks/post-merge\n- cmd/bd/templates/hooks/pre-push\n- cmd/bd/templates/hooks/post-checkout\n\nEach should have:\n```bash\n# bd-hooks-version: 0.30.4\n```\n\n\n```verify\ngrep -l 'bd-hooks-version: 0.30.4' cmd/bd/templates/hooks/* | wc -l | grep -q '4'\n```","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-17T21:19:11.0248-08:00","updated_at":"2025-12-17T21:46:46.27561-08:00","closed_at":"2025-12-17T21:46:46.27561-08:00","dependencies":[{"issue_id":"bd-pbh.9","depends_on_id":"bd-pbh","type":"parent-child","created_at":"2025-12-17T21:19:11.025124-08:00","created_by":"daemon"}]}
|
||||
{"id":"bd-pbh","title":"Release v0.30.4","description":"## Version Bump Workflow\n\nCoordinating release from 0.30.3 to 0.30.4.\n\n### Components Updated\n- Go CLI (cmd/bd/version.go)\n- Claude Plugin (.claude-plugin/*.json)\n- MCP Server (integrations/beads-mcp/)\n- npm Package (npm-package/package.json)\n- Git hooks (cmd/bd/templates/hooks/)\n\n### Release Channels\n- GitHub Releases (GoReleaser)\n- PyPI (beads-mcp)\n- npm (@beads/cli)\n- Homebrew (homebrew-beads tap)\n","status":"closed","priority":1,"issue_type":"epic","created_at":"2025-12-17T21:19:10.926133-08:00","updated_at":"2025-12-17T21:46:46.192948-08:00","closed_at":"2025-12-17T21:46:46.192948-08:00","labels":["release","v0.30.4","workflow"]}
|
||||
{"id":"bd-pbh.1","title":"Update cmd/bd/version.go to 0.30.4","description":"Update the Version constant in cmd/bd/version.go:\n```go\nVersion = \"0.30.4\"\n```\n\n\n```verify\ngrep -q 'Version = \"0.30.4\"' cmd/bd/version.go\n```","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-17T21:19:10.9462-08:00","updated_at":"2025-12-17T21:46:46.20387-08:00","closed_at":"2025-12-17T21:46:46.20387-08:00","labels":["workflow"],"dependencies":[{"issue_id":"bd-pbh.1","depends_on_id":"bd-pbh","type":"parent-child","created_at":"2025-12-17T21:19:10.946633-08:00","created_by":"daemon"}]}
|
||||
{"id":"bd-pbh.10","title":"Run check-versions.sh - all must pass","description":"Run the version consistency check:\n```bash\n./scripts/check-versions.sh\n```\n\nAll versions must match 0.30.4.\n\n\n```verify\n./scripts/check-versions.sh\n```","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-17T21:19:11.047311-08:00","updated_at":"2025-12-17T21:46:46.28316-08:00","closed_at":"2025-12-17T21:46:46.28316-08:00","labels":["workflow"],"dependencies":[{"issue_id":"bd-pbh.10","depends_on_id":"bd-pbh","type":"parent-child","created_at":"2025-12-17T21:19:11.047888-08:00","created_by":"daemon"},{"issue_id":"bd-pbh.10","depends_on_id":"bd-pbh.1","type":"blocks","created_at":"2025-12-17T21:19:11.159084-08:00","created_by":"daemon"},{"issue_id":"bd-pbh.10","depends_on_id":"bd-pbh.4","type":"blocks","created_at":"2025-12-17T21:19:11.168248-08:00","created_by":"daemon"},{"issue_id":"bd-pbh.10","depends_on_id":"bd-pbh.5","type":"blocks","created_at":"2025-12-17T21:19:11.177869-08:00","created_by":"daemon"},{"issue_id":"bd-pbh.10","depends_on_id":"bd-pbh.6","type":"blocks","created_at":"2025-12-17T21:19:11.187629-08:00","created_by":"daemon"},{"issue_id":"bd-pbh.10","depends_on_id":"bd-pbh.7","type":"blocks","created_at":"2025-12-17T21:19:11.199955-08:00","created_by":"daemon"},{"issue_id":"bd-pbh.10","depends_on_id":"bd-pbh.8","type":"blocks","created_at":"2025-12-17T21:19:11.211479-08:00","created_by":"daemon"},{"issue_id":"bd-pbh.10","depends_on_id":"bd-pbh.9","type":"blocks","created_at":"2025-12-17T21:19:11.224059-08:00","created_by":"daemon"}]}
|
||||
{"id":"bd-pbh.11","title":"Commit changes and create v0.30.4 tag","description":"```bash\ngit add -A\ngit commit -m \"chore: Bump version to 0.30.4\"\ngit tag -a v0.30.4 -m \"Release v0.30.4\"\n```\n\n\n```verify\ngit describe --tags --exact-match HEAD 2\u003e/dev/null | grep -q 'v0.30.4'\n```","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-17T21:19:11.056575-08:00","updated_at":"2025-12-17T21:46:46.292166-08:00","closed_at":"2025-12-17T21:46:46.292166-08:00","labels":["workflow"],"dependencies":[{"issue_id":"bd-pbh.11","depends_on_id":"bd-pbh","type":"parent-child","created_at":"2025-12-17T21:19:11.056934-08:00","created_by":"daemon"},{"issue_id":"bd-pbh.11","depends_on_id":"bd-pbh.10","type":"blocks","created_at":"2025-12-17T21:19:11.234175-08:00","created_by":"daemon"},{"issue_id":"bd-pbh.11","depends_on_id":"bd-pbh.2","type":"blocks","created_at":"2025-12-17T21:19:11.245316-08:00","created_by":"daemon"},{"issue_id":"bd-pbh.11","depends_on_id":"bd-pbh.3","type":"blocks","created_at":"2025-12-17T21:19:11.255362-08:00","created_by":"daemon"}]}
|
||||
{"id":"bd-pbh.12","title":"Push commit and tag to origin","description":"```bash\ngit push origin main\ngit push origin v0.30.4\n```\n\nThis triggers GitHub Actions:\n- GoReleaser build\n- PyPI publish\n- npm publish\n\n\n```verify\ngit ls-remote origin refs/tags/v0.30.4 | grep -q 'v0.30.4'\n```","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-17T21:19:11.066074-08:00","updated_at":"2025-12-17T21:46:46.301948-08:00","closed_at":"2025-12-17T21:46:46.301948-08:00","labels":["workflow"],"dependencies":[{"issue_id":"bd-pbh.12","depends_on_id":"bd-pbh","type":"parent-child","created_at":"2025-12-17T21:19:11.066442-08:00","created_by":"daemon"},{"issue_id":"bd-pbh.12","depends_on_id":"bd-pbh.11","type":"blocks","created_at":"2025-12-17T21:19:11.265986-08:00","created_by":"daemon"}]}
|
||||
{"id":"bd-pbh.13","title":"Monitor GoReleaser CI job","description":"Watch the GoReleaser action:\nhttps://github.com/steveyegge/beads/actions/workflows/release.yml\n\nShould complete in ~10 minutes and create:\n- GitHub Release with binaries for all platforms\n- Checksums and signatures\n\nCheck status:\n```bash\ngh run list --workflow=release.yml -L 1\ngh run watch # to monitor live\n```\n\nVerify release exists:\n```bash\ngh release view v0.30.4\n```\n\n\n```verify\ngh release view v0.30.4 --json tagName -q .tagName | grep -q 'v0.30.4'\n```","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-17T21:19:11.074476-08:00","updated_at":"2025-12-17T21:46:46.311506-08:00","closed_at":"2025-12-17T21:46:46.311506-08:00","labels":["workflow"],"dependencies":[{"issue_id":"bd-pbh.13","depends_on_id":"bd-pbh","type":"parent-child","created_at":"2025-12-17T21:19:11.074833-08:00","created_by":"daemon"},{"issue_id":"bd-pbh.13","depends_on_id":"bd-pbh.12","type":"blocks","created_at":"2025-12-17T21:19:11.279092-08:00","created_by":"daemon"}]}
|
||||
{"id":"bd-pbh.14","title":"Monitor PyPI publish","description":"Watch the PyPI publish action:\nhttps://github.com/steveyegge/beads/actions/workflows/pypi-publish.yml\n\nVerify at: https://pypi.org/project/beads-mcp/0.30.4/\n\nCheck:\n```bash\npip index versions beads-mcp 2\u003e/dev/null | grep -q '0.30.4'\n```\n\n\n```verify\npip index versions beads-mcp 2\u003e/dev/null | grep -q '0.30.4' || curl -s https://pypi.org/pypi/beads-mcp/json | jq -e '.releases[\"0.30.4\"]'\n```","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-17T21:19:11.083809-08:00","updated_at":"2025-12-17T21:46:46.320922-08:00","closed_at":"2025-12-17T21:46:46.320922-08:00","labels":["workflow"],"dependencies":[{"issue_id":"bd-pbh.14","depends_on_id":"bd-pbh","type":"parent-child","created_at":"2025-12-17T21:19:11.084126-08:00","created_by":"daemon"},{"issue_id":"bd-pbh.14","depends_on_id":"bd-pbh.12","type":"blocks","created_at":"2025-12-17T21:19:11.289698-08:00","created_by":"daemon"}]}
|
||||
{"id":"bd-pbh.15","title":"Monitor npm publish","description":"Watch the npm publish action:\nhttps://github.com/steveyegge/beads/actions/workflows/npm-publish.yml\n\nVerify at: https://www.npmjs.com/package/@anthropics/claude-code-beads-plugin/v/0.30.4\n\nCheck:\n```bash\nnpm view @anthropics/claude-code-beads-plugin@0.30.4 version\n```\n\n\n```verify\nnpm view @anthropics/claude-code-beads-plugin@0.30.4 version 2\u003e/dev/null | grep -q '0.30.4'\n```","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-17T21:19:11.091806-08:00","updated_at":"2025-12-17T21:46:46.333213-08:00","closed_at":"2025-12-17T21:46:46.333213-08:00","labels":["workflow"],"dependencies":[{"issue_id":"bd-pbh.15","depends_on_id":"bd-pbh","type":"parent-child","created_at":"2025-12-17T21:19:11.092205-08:00","created_by":"daemon"},{"issue_id":"bd-pbh.15","depends_on_id":"bd-pbh.12","type":"blocks","created_at":"2025-12-17T21:19:11.301843-08:00","created_by":"daemon"}]}
|
||||
{"id":"bd-pbh.16","title":"Update Homebrew formula","description":"After GoReleaser completes, the Homebrew tap should be auto-updated.\n\nIf manual update needed:\n```bash\n./scripts/update-homebrew.sh v0.30.4\n```\n\nOr manually update steveyegge/homebrew-beads with new SHA256.\n\nVerify:\n```bash\nbrew update\nbrew info beads\n```\n","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-17T21:19:11.100213-08:00","updated_at":"2025-12-17T21:46:46.341942-08:00","closed_at":"2025-12-17T21:46:46.341942-08:00","labels":["workflow"],"dependencies":[{"issue_id":"bd-pbh.16","depends_on_id":"bd-pbh","type":"parent-child","created_at":"2025-12-17T21:19:11.100541-08:00","created_by":"daemon"},{"issue_id":"bd-pbh.16","depends_on_id":"bd-pbh.13","type":"blocks","created_at":"2025-12-17T21:19:11.312625-08:00","created_by":"daemon"}]}
|
||||
{"id":"bd-pbh.17","title":"Install 0.30.4 Go binary locally","description":"Rebuild and install the Go binary:\n```bash\ngo install ./cmd/bd\n# OR\nmake install\n```\n\nVerify:\n```bash\nbd --version\n```\n\n\n```verify\nbd --version 2\u003e\u00261 | grep -q '0.30.4'\n```","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-17T21:19:11.108597-08:00","updated_at":"2025-12-17T21:46:46.352702-08:00","closed_at":"2025-12-17T21:46:46.352702-08:00","labels":["workflow"],"dependencies":[{"issue_id":"bd-pbh.17","depends_on_id":"bd-pbh","type":"parent-child","created_at":"2025-12-17T21:19:11.108917-08:00","created_by":"daemon"},{"issue_id":"bd-pbh.17","depends_on_id":"bd-pbh.13","type":"blocks","created_at":"2025-12-17T21:19:11.322091-08:00","created_by":"daemon"}]}
|
||||
{"id":"bd-pbh.18","title":"Restart beads daemon","description":"Kill any running daemons so they pick up the new version:\n```bash\nbd daemons killall\n```\n\nStart fresh daemon:\n```bash\nbd list # triggers daemon start\n```\n\nVerify daemon version:\n```bash\nbd version --daemon\n```\n\n\n```verify\nbd version --daemon 2\u003e\u00261 | grep -q '0.30.4'\n```","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-17T21:19:11.11636-08:00","updated_at":"2025-12-17T21:46:46.364842-08:00","closed_at":"2025-12-17T21:46:46.364842-08:00","labels":["workflow"],"dependencies":[{"issue_id":"bd-pbh.18","depends_on_id":"bd-pbh","type":"parent-child","created_at":"2025-12-17T21:19:11.116706-08:00","created_by":"daemon"},{"issue_id":"bd-pbh.18","depends_on_id":"bd-pbh.17","type":"blocks","created_at":"2025-12-17T21:19:11.330411-08:00","created_by":"daemon"}]}
|
||||
{"id":"bd-pbh.19","title":"Install 0.30.4 MCP server locally","description":"Upgrade the MCP server (after PyPI publish):\n```bash\npip install --upgrade beads-mcp\n# OR if using uv:\nuv tool upgrade beads-mcp\n```\n\nVerify:\n```bash\npip show beads-mcp | grep Version\n```\n\n\n```verify\npip show beads-mcp 2\u003e/dev/null | grep -q 'Version: 0.30.4'\n```","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-17T21:19:11.124496-08:00","updated_at":"2025-12-17T21:46:46.372989-08:00","closed_at":"2025-12-17T21:46:46.372989-08:00","labels":["workflow"],"dependencies":[{"issue_id":"bd-pbh.19","depends_on_id":"bd-pbh","type":"parent-child","created_at":"2025-12-17T21:19:11.124829-08:00","created_by":"daemon"},{"issue_id":"bd-pbh.19","depends_on_id":"bd-pbh.14","type":"blocks","created_at":"2025-12-17T21:19:11.343558-08:00","created_by":"daemon"}]}
|
||||
{"id":"bd-pbh.2","title":"Update CHANGELOG.md for 0.30.4","description":"1. Change `## [Unreleased]` to `## [0.30.4] - 2025-12-17`\n2. Add new empty `## [Unreleased]` section at top\n3. Ensure all changes since 0.30.3 are documented\n","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-17T21:19:10.956332-08:00","updated_at":"2025-12-17T21:46:46.214512-08:00","closed_at":"2025-12-17T21:46:46.214512-08:00","labels":["workflow"],"dependencies":[{"issue_id":"bd-pbh.2","depends_on_id":"bd-pbh","type":"parent-child","created_at":"2025-12-17T21:19:10.95683-08:00","created_by":"daemon"}]}
|
||||
{"id":"bd-pbh.20","title":"Update git hooks","description":"Install the updated hooks:\n```bash\nbd hooks install\n```\n\nVerify hook version:\n```bash\ngrep 'bd-hooks-version' .git/hooks/pre-commit\n```\n\n\n```verify\ngrep -q 'bd-hooks-version: 0.30.4' .git/hooks/pre-commit 2\u003e/dev/null || echo 'Hooks may not be installed - verify manually'\n```","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-17T21:19:11.13198-08:00","updated_at":"2025-12-17T21:46:46.381519-08:00","closed_at":"2025-12-17T21:46:46.381519-08:00","labels":["workflow"],"dependencies":[{"issue_id":"bd-pbh.20","depends_on_id":"bd-pbh","type":"parent-child","created_at":"2025-12-17T21:19:11.132306-08:00","created_by":"daemon"},{"issue_id":"bd-pbh.20","depends_on_id":"bd-pbh.17","type":"blocks","created_at":"2025-12-17T21:19:11.352288-08:00","created_by":"daemon"}]}
|
||||
{"id":"bd-pbh.21","title":"Final release verification","description":"Verify all release artifacts are accessible:\n\n- [ ] `bd --version` shows 0.30.4\n- [ ] `bd version --daemon` shows 0.30.4\n- [ ] GitHub release exists: https://github.com/steveyegge/beads/releases/tag/v0.30.4\n- [ ] `brew upgrade beads \u0026\u0026 bd --version` shows 0.30.4 (if using Homebrew)\n- [ ] `pip show beads-mcp` shows 0.30.4\n- [ ] npm package available at 0.30.4\n- [ ] `bd info --whats-new` shows 0.30.4 notes\n\nRun final checks:\n```bash\nbd --version\nbd version --daemon\npip show beads-mcp | grep Version\nbd info --whats-new\n```\n","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-17T21:19:11.141249-08:00","updated_at":"2025-12-17T21:46:46.390985-08:00","closed_at":"2025-12-17T21:46:46.390985-08:00","labels":["workflow"],"dependencies":[{"issue_id":"bd-pbh.21","depends_on_id":"bd-pbh","type":"parent-child","created_at":"2025-12-17T21:19:11.141549-08:00","created_by":"daemon"},{"issue_id":"bd-pbh.21","depends_on_id":"bd-pbh.18","type":"blocks","created_at":"2025-12-17T21:19:11.364839-08:00","created_by":"daemon"},{"issue_id":"bd-pbh.21","depends_on_id":"bd-pbh.19","type":"blocks","created_at":"2025-12-17T21:19:11.373656-08:00","created_by":"daemon"},{"issue_id":"bd-pbh.21","depends_on_id":"bd-pbh.20","type":"blocks","created_at":"2025-12-17T21:19:11.382-08:00","created_by":"daemon"},{"issue_id":"bd-pbh.21","depends_on_id":"bd-pbh.15","type":"blocks","created_at":"2025-12-17T21:19:11.389733-08:00","created_by":"daemon"},{"issue_id":"bd-pbh.21","depends_on_id":"bd-pbh.16","type":"blocks","created_at":"2025-12-17T21:19:11.398347-08:00","created_by":"daemon"}]}
|
||||
{"id":"bd-pbh.3","title":"Add 0.30.4 to info.go release notes","description":"Update cmd/bd/info.go versionChanges map with release notes for 0.30.4.\nInclude any workflow-impacting changes for --whats-new output.\n","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-17T21:19:10.966781-08:00","updated_at":"2025-12-17T21:46:46.222445-08:00","closed_at":"2025-12-17T21:46:46.222445-08:00","labels":["workflow"],"dependencies":[{"issue_id":"bd-pbh.3","depends_on_id":"bd-pbh","type":"parent-child","created_at":"2025-12-17T21:19:10.967287-08:00","created_by":"daemon"},{"issue_id":"bd-pbh.3","depends_on_id":"bd-pbh.2","type":"blocks","created_at":"2025-12-17T21:19:11.149584-08:00","created_by":"daemon"}]}
|
||||
{"id":"bd-pbh.4","title":"Update .claude-plugin/plugin.json to 0.30.4","description":"Update version field in .claude-plugin/plugin.json:\n```json\n\"version\": \"0.30.4\"\n```\n\n\n```verify\njq -e '.version == \"0.30.4\"' .claude-plugin/plugin.json\n```","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-17T21:19:10.976866-08:00","updated_at":"2025-12-17T21:46:46.23159-08:00","closed_at":"2025-12-17T21:46:46.23159-08:00","labels":["workflow"],"dependencies":[{"issue_id":"bd-pbh.4","depends_on_id":"bd-pbh","type":"parent-child","created_at":"2025-12-17T21:19:10.97729-08:00","created_by":"daemon"}]}
|
||||
{"id":"bd-pbh.5","title":"Update .claude-plugin/marketplace.json to 0.30.4","description":"Update version field in .claude-plugin/marketplace.json:\n```json\n\"version\": \"0.30.4\"\n```\n\n\n```verify\njq -e '.plugins[0].version == \"0.30.4\"' .claude-plugin/marketplace.json\n```","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-17T21:19:10.985619-08:00","updated_at":"2025-12-17T21:46:46.239122-08:00","closed_at":"2025-12-17T21:46:46.239122-08:00","labels":["workflow"],"dependencies":[{"issue_id":"bd-pbh.5","depends_on_id":"bd-pbh","type":"parent-child","created_at":"2025-12-17T21:19:10.985942-08:00","created_by":"daemon"}]}
|
||||
{"id":"bd-pbh.6","title":"Update integrations/beads-mcp/pyproject.toml to 0.30.4","description":"Update version in pyproject.toml:\n```toml\nversion = \"0.30.4\"\n```\n\n\n```verify\ngrep -q 'version = \"0.30.4\"' integrations/beads-mcp/pyproject.toml\n```","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-17T21:19:10.994004-08:00","updated_at":"2025-12-17T21:46:46.246574-08:00","closed_at":"2025-12-17T21:46:46.246574-08:00","labels":["workflow"],"dependencies":[{"issue_id":"bd-pbh.6","depends_on_id":"bd-pbh","type":"parent-child","created_at":"2025-12-17T21:19:10.994376-08:00","created_by":"daemon"}]}
|
||||
{"id":"bd-pbh.7","title":"Update beads_mcp/__init__.py to 0.30.4","description":"Update __version__ in integrations/beads-mcp/src/beads_mcp/__init__.py:\n```python\n__version__ = \"0.30.4\"\n```\n\n\n```verify\ngrep -q '__version__ = \"0.30.4\"' integrations/beads-mcp/src/beads_mcp/__init__.py\n```","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-17T21:19:11.005334-08:00","updated_at":"2025-12-17T21:46:46.254885-08:00","closed_at":"2025-12-17T21:46:46.254885-08:00","labels":["workflow"],"dependencies":[{"issue_id":"bd-pbh.7","depends_on_id":"bd-pbh","type":"parent-child","created_at":"2025-12-17T21:19:11.005699-08:00","created_by":"daemon"}]}
|
||||
{"id":"bd-pbh.8","title":"Update npm-package/package.json to 0.30.4","description":"Update version field in npm-package/package.json:\n```json\n\"version\": \"0.30.4\"\n```\n\n\n```verify\njq -e '.version == \"0.30.4\"' npm-package/package.json\n```","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-17T21:19:11.014905-08:00","updated_at":"2025-12-17T21:46:46.268821-08:00","closed_at":"2025-12-17T21:46:46.268821-08:00","labels":["workflow"],"dependencies":[{"issue_id":"bd-pbh.8","depends_on_id":"bd-pbh","type":"parent-child","created_at":"2025-12-17T21:19:11.01529-08:00","created_by":"daemon"}]}
|
||||
{"id":"bd-pbh.9","title":"Update hook templates to 0.30.4","description":"Update bd-hooks-version comment in all 4 hook templates:\n- cmd/bd/templates/hooks/pre-commit\n- cmd/bd/templates/hooks/post-merge\n- cmd/bd/templates/hooks/pre-push\n- cmd/bd/templates/hooks/post-checkout\n\nEach should have:\n```bash\n# bd-hooks-version: 0.30.4\n```\n\n\n```verify\ngrep -l 'bd-hooks-version: 0.30.4' cmd/bd/templates/hooks/* | wc -l | grep -q '4'\n```","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-17T21:19:11.0248-08:00","updated_at":"2025-12-17T21:46:46.27561-08:00","closed_at":"2025-12-17T21:46:46.27561-08:00","labels":["workflow"],"dependencies":[{"issue_id":"bd-pbh.9","depends_on_id":"bd-pbh","type":"parent-child","created_at":"2025-12-17T21:19:11.025124-08:00","created_by":"daemon"}]}
|
||||
{"id":"bd-pdr2","title":"Consider backwards compatibility for ready() and list() return type change","description":"PR #481 changed the return types of `ready()` and `list()` from `list[Issue]` to `list[IssueMinimal] | CompactedResult`. This is a breaking change for MCP clients.\n\n## Impact Assessment\nBreaking change affects:\n- Any MCP client expecting `list[Issue]` from ready()\n- Any MCP client expecting `list[Issue]` from list()\n- Client code that accesses full Issue fields (description, design, acceptance_criteria, timestamps, dependencies, dependents)\n\n## Current Behavior\n- ready() returns `list[IssueMinimal] | CompactedResult`\n- list() returns `list[IssueMinimal] | CompactedResult`\n- show() still returns full `Issue` (good)\n\n## Considerations\n**Pros of current approach:**\n- Forces clients to use show() for full details (good for context efficiency)\n- Simple mental model (always use show for full data)\n- Documentation warns about this\n\n**Cons:**\n- Clients expecting list[Issue] will break\n- No graceful degradation option\n- No migration period\n\n## Potential Solutions\n1. Add optional parameter `full_details=false` to ready/list (would increase payload)\n2. Create separate tools: ready_minimal/list_minimal + ready_full/list_full\n3. Accept breaking change and document upgrade path (current approach)\n4. Version the MCP server and document migration guide\n\n## Recommendation\nCurrent approach (solution 3) is reasonable if:\n- Changelog clearly documents the breaking change\n- Migration guide provided to clients\n- Error handling is graceful for clients expecting specific fields","status":"open","priority":2,"issue_type":"task","created_at":"2025-12-14T14:24:56.460465-08:00","updated_at":"2025-12-14T14:24:56.460465-08:00","dependencies":[{"issue_id":"bd-pdr2","depends_on_id":"bd-otf4","type":"discovered-from","created_at":"2025-12-14T14:24:56.461959-08:00","created_by":"stevey"}]}
|
||||
{"id":"bd-pe4s","title":"JSON test issue","description":"Line 1\nLine 2\nLine 3","status":"tombstone","priority":2,"issue_type":"task","created_at":"2025-12-16T16:14:36.969074-08:00","updated_at":"2025-12-17T16:11:17.070763-08:00","deleted_at":"2025-12-17T16:11:17.070763-08:00","deleted_by":"batch delete","delete_reason":"batch delete","original_type":"task"}
|
||||
{"id":"bd-pgcs","title":"Clean up orphaned child issues (bd-cb64c226.*, bd-cbed9619.*)","description":"## Problem\n\nEvery bd command shows warnings about 12 orphaned child issues:\n- bd-cb64c226.1, .6, .8, .9, .10, .12, .13\n- bd-cbed9619.1, .2, .3, .4, .5\n\nThese are hierarchical IDs (parent.child format) where the parent issues no longer exist.\n\n## Impact\n\n- Clutters output of every bd command\n- Confusing for users\n- Indicates incomplete cleanup of deleted parent issues\n\n## Proposed Solution\n\n1. Delete the orphaned issues since their parents no longer exist:\n ```bash\n bd delete bd-cb64c226.1 bd-cb64c226.6 bd-cb64c226.8 ...\n ```\n\n2. Or convert them to top-level issues if they contain useful content\n\n## Investigation Needed\n\n- What were the parent issues bd-cb64c226 and bd-cbed9619?\n- Why were they deleted without their children?\n- Should bd delete cascade to children automatically?","status":"tombstone","priority":2,"issue_type":"task","created_at":"2025-12-16T23:06:17.240571-08:00","updated_at":"2025-12-17T16:11:17.070763-08:00","deleted_at":"2025-12-17T16:11:17.070763-08:00","deleted_by":"batch delete","delete_reason":"batch delete","original_type":"task"}
|
||||
|
||||
@@ -362,9 +362,7 @@ func runMailInbox(cmd *cobra.Command, args []string) error {
|
||||
|
||||
fmt.Printf(" %s: %s%s\n", msg.ID, msg.Title, priorityStr)
|
||||
fmt.Printf(" From: %s (%s)\n", msg.Sender, timeStr)
|
||||
if msg.RepliesTo != "" {
|
||||
fmt.Printf(" Re: %s\n", msg.RepliesTo)
|
||||
}
|
||||
// NOTE: Thread info now in dependencies (Decision 004)
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
@@ -418,9 +416,7 @@ func runMailRead(cmd *cobra.Command, args []string) error {
|
||||
if issue.Priority <= 1 {
|
||||
fmt.Printf("Priority: P%d\n", issue.Priority)
|
||||
}
|
||||
if issue.RepliesTo != "" {
|
||||
fmt.Printf("Re: %s\n", issue.RepliesTo)
|
||||
}
|
||||
// NOTE: Thread info (RepliesTo) now in dependencies (Decision 004)
|
||||
fmt.Printf("Status: %s\n", issue.Status)
|
||||
fmt.Println(strings.Repeat("─", 66))
|
||||
fmt.Println()
|
||||
@@ -593,10 +589,11 @@ func runMailReply(cmd *cobra.Command, args []string) error {
|
||||
Assignee: recipient,
|
||||
Sender: sender,
|
||||
Ephemeral: true,
|
||||
RepliesTo: messageID, // Thread link
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
// NOTE: RepliesTo now handled via dependency API (Decision 004)
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
_ = messageID // RepliesTo handled via CreateArgs.RepliesTo -> server creates dependency
|
||||
|
||||
if daemonClient != nil {
|
||||
// Daemon mode - create reply with all messaging fields
|
||||
|
||||
@@ -183,7 +183,7 @@ func TestMailReply(t *testing.T) {
|
||||
t.Fatalf("Failed to create original message: %v", err)
|
||||
}
|
||||
|
||||
// Create reply
|
||||
// Create reply (thread link now done via dependencies per Decision 004)
|
||||
reply := &types.Issue{
|
||||
Title: "Re: Original Subject",
|
||||
Description: "Reply body",
|
||||
@@ -193,7 +193,6 @@ func TestMailReply(t *testing.T) {
|
||||
Assignee: "manager", // Reply goes to original sender
|
||||
Sender: "worker",
|
||||
Ephemeral: true,
|
||||
RepliesTo: original.ID, // Thread link
|
||||
CreatedAt: now.Add(time.Minute),
|
||||
UpdatedAt: now.Add(time.Minute),
|
||||
}
|
||||
@@ -202,14 +201,31 @@ func TestMailReply(t *testing.T) {
|
||||
t.Fatalf("Failed to create reply: %v", err)
|
||||
}
|
||||
|
||||
// Verify reply has correct thread link
|
||||
savedReply, err := testStore.GetIssue(ctx, reply.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("GetIssue failed: %v", err)
|
||||
// Add replies-to dependency (thread link per Decision 004)
|
||||
dep := &types.Dependency{
|
||||
IssueID: reply.ID,
|
||||
DependsOnID: original.ID,
|
||||
Type: types.DepRepliesTo,
|
||||
}
|
||||
if err := testStore.AddDependency(ctx, dep, "test"); err != nil {
|
||||
t.Fatalf("Failed to add replies-to dependency: %v", err)
|
||||
}
|
||||
|
||||
if savedReply.RepliesTo != original.ID {
|
||||
t.Errorf("RepliesTo = %q, want %q", savedReply.RepliesTo, original.ID)
|
||||
// Verify reply has correct thread link via dependencies
|
||||
deps, err := testStore.GetDependenciesWithMetadata(ctx, reply.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("GetDependenciesWithMetadata failed: %v", err)
|
||||
}
|
||||
|
||||
var foundReplyLink bool
|
||||
for _, d := range deps {
|
||||
if d.DependencyType == types.DepRepliesTo && d.ID == original.ID {
|
||||
foundReplyLink = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !foundReplyLink {
|
||||
t.Errorf("Reply missing replies-to link to original message")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
156
cmd/bd/relate.go
156
cmd/bd/relate.go
@@ -119,47 +119,45 @@ func runRelate(cmd *cobra.Command, args []string) error {
|
||||
return fmt.Errorf("issue not found: %s", id2)
|
||||
}
|
||||
|
||||
// Add id2 to issue1's relates_to if not already present
|
||||
if !contains(issue1.RelatesTo, id2) {
|
||||
newRelatesTo1 := append(issue1.RelatesTo, id2)
|
||||
formattedRelatesTo1 := formatRelatesTo(newRelatesTo1)
|
||||
if daemonClient != nil {
|
||||
// Use RPC for daemon mode (bd-fu83)
|
||||
_, err := daemonClient.Update(&rpc.UpdateArgs{
|
||||
ID: id1,
|
||||
RelatesTo: &formattedRelatesTo1,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to update %s: %w", id1, err)
|
||||
}
|
||||
} else {
|
||||
if err := store.UpdateIssue(ctx, id1, map[string]interface{}{
|
||||
"relates_to": formattedRelatesTo1,
|
||||
}, actor); err != nil {
|
||||
return fmt.Errorf("failed to update %s: %w", id1, err)
|
||||
}
|
||||
// Add relates-to dependency: id1 -> id2 (bidirectional, so also id2 -> id1)
|
||||
// Per Decision 004, relates-to links are now stored in dependencies table
|
||||
if daemonClient != nil {
|
||||
// Add id1 -> id2
|
||||
_, err := daemonClient.AddDependency(&rpc.DepAddArgs{
|
||||
FromID: id1,
|
||||
ToID: id2,
|
||||
DepType: string(types.DepRelatesTo),
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to add relates-to %s -> %s: %w", id1, id2, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Add id1 to issue2's relates_to if not already present
|
||||
if !contains(issue2.RelatesTo, id1) {
|
||||
newRelatesTo2 := append(issue2.RelatesTo, id1)
|
||||
formattedRelatesTo2 := formatRelatesTo(newRelatesTo2)
|
||||
if daemonClient != nil {
|
||||
// Use RPC for daemon mode (bd-fu83)
|
||||
_, err := daemonClient.Update(&rpc.UpdateArgs{
|
||||
ID: id2,
|
||||
RelatesTo: &formattedRelatesTo2,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to update %s: %w", id2, err)
|
||||
}
|
||||
} else {
|
||||
if err := store.UpdateIssue(ctx, id2, map[string]interface{}{
|
||||
"relates_to": formattedRelatesTo2,
|
||||
}, actor); err != nil {
|
||||
return fmt.Errorf("failed to update %s: %w", id2, err)
|
||||
}
|
||||
// Add id2 -> id1 (bidirectional)
|
||||
_, err = daemonClient.AddDependency(&rpc.DepAddArgs{
|
||||
FromID: id2,
|
||||
ToID: id1,
|
||||
DepType: string(types.DepRelatesTo),
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to add relates-to %s -> %s: %w", id2, id1, err)
|
||||
}
|
||||
} else {
|
||||
// Add id1 -> id2
|
||||
dep1 := &types.Dependency{
|
||||
IssueID: id1,
|
||||
DependsOnID: id2,
|
||||
Type: types.DepRelatesTo,
|
||||
}
|
||||
if err := store.AddDependency(ctx, dep1, actor); err != nil {
|
||||
return fmt.Errorf("failed to add relates-to %s -> %s: %w", id1, id2, err)
|
||||
}
|
||||
// Add id2 -> id1 (bidirectional)
|
||||
dep2 := &types.Dependency{
|
||||
IssueID: id2,
|
||||
DependsOnID: id1,
|
||||
Type: types.DepRelatesTo,
|
||||
}
|
||||
if err := store.AddDependency(ctx, dep2, actor); err != nil {
|
||||
return fmt.Errorf("failed to add relates-to %s -> %s: %w", id2, id1, err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -254,43 +252,35 @@ func runUnrelate(cmd *cobra.Command, args []string) error {
|
||||
return fmt.Errorf("issue not found: %s", id2)
|
||||
}
|
||||
|
||||
// Remove id2 from issue1's relates_to
|
||||
newRelatesTo1 := remove(issue1.RelatesTo, id2)
|
||||
formattedRelatesTo1 := formatRelatesTo(newRelatesTo1)
|
||||
// Remove relates-to dependency in both directions
|
||||
// Per Decision 004, relates-to links are now stored in dependencies table
|
||||
if daemonClient != nil {
|
||||
// Use RPC for daemon mode (bd-fu83)
|
||||
_, err := daemonClient.Update(&rpc.UpdateArgs{
|
||||
ID: id1,
|
||||
RelatesTo: &formattedRelatesTo1,
|
||||
// Remove id1 -> id2
|
||||
_, err := daemonClient.RemoveDependency(&rpc.DepRemoveArgs{
|
||||
FromID: id1,
|
||||
ToID: id2,
|
||||
DepType: string(types.DepRelatesTo),
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to update %s: %w", id1, err)
|
||||
return fmt.Errorf("failed to remove relates-to %s -> %s: %w", id1, id2, err)
|
||||
}
|
||||
} else {
|
||||
if err := store.UpdateIssue(ctx, id1, map[string]interface{}{
|
||||
"relates_to": formattedRelatesTo1,
|
||||
}, actor); err != nil {
|
||||
return fmt.Errorf("failed to update %s: %w", id1, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Remove id1 from issue2's relates_to
|
||||
newRelatesTo2 := remove(issue2.RelatesTo, id1)
|
||||
formattedRelatesTo2 := formatRelatesTo(newRelatesTo2)
|
||||
if daemonClient != nil {
|
||||
// Use RPC for daemon mode (bd-fu83)
|
||||
_, err := daemonClient.Update(&rpc.UpdateArgs{
|
||||
ID: id2,
|
||||
RelatesTo: &formattedRelatesTo2,
|
||||
// Remove id2 -> id1 (bidirectional)
|
||||
_, err = daemonClient.RemoveDependency(&rpc.DepRemoveArgs{
|
||||
FromID: id2,
|
||||
ToID: id1,
|
||||
DepType: string(types.DepRelatesTo),
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to update %s: %w", id2, err)
|
||||
return fmt.Errorf("failed to remove relates-to %s -> %s: %w", id2, id1, err)
|
||||
}
|
||||
} else {
|
||||
if err := store.UpdateIssue(ctx, id2, map[string]interface{}{
|
||||
"relates_to": formattedRelatesTo2,
|
||||
}, actor); err != nil {
|
||||
return fmt.Errorf("failed to update %s: %w", id2, err)
|
||||
// Remove id1 -> id2
|
||||
if err := store.RemoveDependency(ctx, id1, id2, actor); err != nil {
|
||||
return fmt.Errorf("failed to remove relates-to %s -> %s: %w", id1, id2, err)
|
||||
}
|
||||
// Remove id2 -> id1 (bidirectional)
|
||||
if err := store.RemoveDependency(ctx, id2, id1, actor); err != nil {
|
||||
return fmt.Errorf("failed to remove relates-to %s -> %s: %w", id2, id1, err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -315,29 +305,5 @@ func runUnrelate(cmd *cobra.Command, args []string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func contains(slice []string, item string) bool {
|
||||
for _, s := range slice {
|
||||
if s == item {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func remove(slice []string, item string) []string {
|
||||
result := make([]string, 0, len(slice))
|
||||
for _, s := range slice {
|
||||
if s != item {
|
||||
result = append(result, s)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func formatRelatesTo(ids []string) string {
|
||||
if len(ids) == 0 {
|
||||
return ""
|
||||
}
|
||||
data, _ := json.Marshal(ids)
|
||||
return string(data)
|
||||
}
|
||||
// Note: contains, remove, formatRelatesTo functions removed per Decision 004
|
||||
// relates-to links now use dependencies API instead of Issue.RelatesTo field
|
||||
|
||||
161
cmd/bd/show.go
161
cmd/bd/show.go
@@ -13,6 +13,7 @@ import (
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/steveyegge/beads/internal/hooks"
|
||||
"github.com/steveyegge/beads/internal/rpc"
|
||||
"github.com/steveyegge/beads/internal/storage"
|
||||
"github.com/steveyegge/beads/internal/storage/sqlite"
|
||||
"github.com/steveyegge/beads/internal/types"
|
||||
"github.com/steveyegge/beads/internal/utils"
|
||||
@@ -1096,20 +1097,26 @@ func showMessageThread(ctx context.Context, messageID string, jsonOutput bool) {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Find the root of the thread by following replies_to chain upward
|
||||
// Find the root of the thread by following replies-to dependencies upward
|
||||
// Per Decision 004, RepliesTo is now stored as a dependency, not an Issue field
|
||||
rootMsg := startMsg
|
||||
seen := make(map[string]bool)
|
||||
seen[rootMsg.ID] = true
|
||||
|
||||
for rootMsg.RepliesTo != "" {
|
||||
if seen[rootMsg.RepliesTo] {
|
||||
for {
|
||||
// Find parent via replies-to dependency
|
||||
parentID := findRepliesTo(ctx, rootMsg.ID, daemonClient, store)
|
||||
if parentID == "" {
|
||||
break // No parent, this is the root
|
||||
}
|
||||
if seen[parentID] {
|
||||
break // Avoid infinite loops
|
||||
}
|
||||
seen[rootMsg.RepliesTo] = true
|
||||
seen[parentID] = true
|
||||
|
||||
var parentMsg *types.Issue
|
||||
if daemonClient != nil {
|
||||
resp, err := daemonClient.Show(&rpc.ShowArgs{ID: rootMsg.RepliesTo})
|
||||
resp, err := daemonClient.Show(&rpc.ShowArgs{ID: parentID})
|
||||
if err != nil {
|
||||
break // Parent not found, use current as root
|
||||
}
|
||||
@@ -1117,7 +1124,7 @@ func showMessageThread(ctx context.Context, messageID string, jsonOutput bool) {
|
||||
break
|
||||
}
|
||||
} else {
|
||||
parentMsg, _ = store.GetIssue(ctx, rootMsg.RepliesTo)
|
||||
parentMsg, _ = store.GetIssue(ctx, parentID)
|
||||
}
|
||||
if parentMsg == nil {
|
||||
break
|
||||
@@ -1127,8 +1134,10 @@ func showMessageThread(ctx context.Context, messageID string, jsonOutput bool) {
|
||||
|
||||
// Now collect all messages in the thread
|
||||
// Start from root and find all replies
|
||||
// Build a map of child ID -> parent ID for display purposes
|
||||
threadMessages := []*types.Issue{rootMsg}
|
||||
threadIDs := map[string]bool{rootMsg.ID: true}
|
||||
repliesTo := map[string]string{} // child ID -> parent ID
|
||||
queue := []string{rootMsg.ID}
|
||||
|
||||
// BFS to find all replies
|
||||
@@ -1136,39 +1145,17 @@ func showMessageThread(ctx context.Context, messageID string, jsonOutput bool) {
|
||||
currentID := queue[0]
|
||||
queue = queue[1:]
|
||||
|
||||
// Find all messages that reply to currentID
|
||||
var replies []*types.Issue
|
||||
if daemonClient != nil {
|
||||
// In daemon mode, search for messages with replies_to = currentID
|
||||
// Use list with a filter (simplified: we'll search all messages)
|
||||
// This is inefficient but works for now
|
||||
listArgs := &rpc.ListArgs{IssueType: "message"}
|
||||
resp, err := daemonClient.List(listArgs)
|
||||
if err == nil {
|
||||
var allMessages []*types.Issue
|
||||
if err := json.Unmarshal(resp.Data, &allMessages); err == nil {
|
||||
for _, msg := range allMessages {
|
||||
if msg.RepliesTo == currentID && !threadIDs[msg.ID] {
|
||||
replies = append(replies, msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Direct mode - search for replies
|
||||
messageType := types.TypeMessage
|
||||
filter := types.IssueFilter{IssueType: &messageType}
|
||||
allMessages, _ := store.SearchIssues(ctx, "", filter)
|
||||
for _, msg := range allMessages {
|
||||
if msg.RepliesTo == currentID && !threadIDs[msg.ID] {
|
||||
replies = append(replies, msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
// Find all messages that reply to currentID via replies-to dependency
|
||||
// Per Decision 004, replies are found via dependents with type replies-to
|
||||
replies := findReplies(ctx, currentID, daemonClient, store)
|
||||
|
||||
for _, reply := range replies {
|
||||
if threadIDs[reply.ID] {
|
||||
continue // Already seen
|
||||
}
|
||||
threadMessages = append(threadMessages, reply)
|
||||
threadIDs[reply.ID] = true
|
||||
repliesTo[reply.ID] = currentID // Track parent for display
|
||||
queue = append(queue, reply.ID)
|
||||
}
|
||||
}
|
||||
@@ -1193,18 +1180,12 @@ func showMessageThread(ctx context.Context, messageID string, jsonOutput bool) {
|
||||
fmt.Println(strings.Repeat("─", 66))
|
||||
|
||||
for _, msg := range threadMessages {
|
||||
// Show indent based on depth (count replies_to chain)
|
||||
// Show indent based on depth (count replies_to chain using our map)
|
||||
depth := 0
|
||||
parent := msg.RepliesTo
|
||||
parent := repliesTo[msg.ID]
|
||||
for parent != "" && depth < 5 {
|
||||
depth++
|
||||
// Find parent to get its replies_to
|
||||
for _, m := range threadMessages {
|
||||
if m.ID == parent {
|
||||
parent = m.RepliesTo
|
||||
break
|
||||
}
|
||||
}
|
||||
parent = repliesTo[parent]
|
||||
}
|
||||
indent := strings.Repeat(" ", depth)
|
||||
|
||||
@@ -1219,8 +1200,8 @@ func showMessageThread(ctx context.Context, messageID string, jsonOutput bool) {
|
||||
|
||||
fmt.Printf("%s%s %s %s\n", indent, statusIcon, cyan(msg.ID), dim(timeStr))
|
||||
fmt.Printf("%s From: %s To: %s\n", indent, msg.Sender, msg.Assignee)
|
||||
if msg.RepliesTo != "" {
|
||||
fmt.Printf("%s Re: %s\n", indent, msg.RepliesTo)
|
||||
if parentID := repliesTo[msg.ID]; parentID != "" {
|
||||
fmt.Printf("%s Re: %s\n", indent, parentID)
|
||||
}
|
||||
fmt.Printf("%s %s: %s\n", indent, dim("Subject"), msg.Title)
|
||||
if msg.Description != "" {
|
||||
@@ -1236,6 +1217,94 @@ func showMessageThread(ctx context.Context, messageID string, jsonOutput bool) {
|
||||
fmt.Printf("Total: %d messages in thread\n\n", len(threadMessages))
|
||||
}
|
||||
|
||||
// findRepliesTo finds the parent ID that this issue replies to via replies-to dependency.
|
||||
// Returns empty string if no parent found.
|
||||
func findRepliesTo(ctx context.Context, issueID string, daemonClient *rpc.Client, store storage.Storage) string {
|
||||
if daemonClient != nil {
|
||||
// In daemon mode, use Show to get dependencies with metadata
|
||||
resp, err := daemonClient.Show(&rpc.ShowArgs{ID: issueID})
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
// Parse the full show response to get dependencies
|
||||
type showResponse struct {
|
||||
Dependencies []struct {
|
||||
ID string `json:"id"`
|
||||
DependencyType string `json:"dependency_type"`
|
||||
} `json:"dependencies"`
|
||||
}
|
||||
var details showResponse
|
||||
if err := json.Unmarshal(resp.Data, &details); err != nil {
|
||||
return ""
|
||||
}
|
||||
for _, dep := range details.Dependencies {
|
||||
if dep.DependencyType == string(types.DepRepliesTo) {
|
||||
return dep.ID
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
// Direct mode - query storage
|
||||
if sqliteStore, ok := store.(*sqlite.SQLiteStorage); ok {
|
||||
deps, err := sqliteStore.GetDependenciesWithMetadata(ctx, issueID)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
for _, dep := range deps {
|
||||
if dep.DependencyType == types.DepRepliesTo {
|
||||
return dep.ID
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// findReplies finds all issues that reply to this issue via replies-to dependency.
|
||||
func findReplies(ctx context.Context, issueID string, daemonClient *rpc.Client, store storage.Storage) []*types.Issue {
|
||||
if daemonClient != nil {
|
||||
// In daemon mode, use Show to get dependents with metadata
|
||||
resp, err := daemonClient.Show(&rpc.ShowArgs{ID: issueID})
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
// Parse the full show response to get dependents
|
||||
type showResponse struct {
|
||||
Dependents []struct {
|
||||
types.Issue
|
||||
DependencyType string `json:"dependency_type"`
|
||||
} `json:"dependents"`
|
||||
}
|
||||
var details showResponse
|
||||
if err := json.Unmarshal(resp.Data, &details); err != nil {
|
||||
return nil
|
||||
}
|
||||
var replies []*types.Issue
|
||||
for _, dep := range details.Dependents {
|
||||
if dep.DependencyType == string(types.DepRepliesTo) {
|
||||
issue := dep.Issue // Copy to avoid aliasing
|
||||
replies = append(replies, &issue)
|
||||
}
|
||||
}
|
||||
return replies
|
||||
}
|
||||
// Direct mode - query storage
|
||||
if sqliteStore, ok := store.(*sqlite.SQLiteStorage); ok {
|
||||
deps, err := sqliteStore.GetDependentsWithMetadata(ctx, issueID)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
var replies []*types.Issue
|
||||
for _, dep := range deps {
|
||||
if dep.DependencyType == types.DepRepliesTo {
|
||||
issue := dep.Issue // Copy to avoid aliasing
|
||||
replies = append(replies, &issue)
|
||||
}
|
||||
}
|
||||
return replies
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
showCmd.Flags().Bool("json", false, "Output JSON format")
|
||||
showCmd.Flags().Bool("thread", false, "Show full conversation thread (for messages)")
|
||||
|
||||
@@ -174,7 +174,7 @@ func (s *Server) handleCreate(req *Request) Response {
|
||||
// Messaging fields (bd-kwro)
|
||||
Sender: createArgs.Sender,
|
||||
Ephemeral: createArgs.Ephemeral,
|
||||
RepliesTo: createArgs.RepliesTo,
|
||||
// NOTE: RepliesTo now handled via replies-to dependency (Decision 004)
|
||||
}
|
||||
|
||||
// Check if any dependencies are discovered-from type
|
||||
@@ -234,6 +234,22 @@ func (s *Server) handleCreate(req *Request) Response {
|
||||
}
|
||||
}
|
||||
|
||||
// If RepliesTo was specified, add replies-to dependency (Decision 004)
|
||||
if createArgs.RepliesTo != "" {
|
||||
dep := &types.Dependency{
|
||||
IssueID: issue.ID,
|
||||
DependsOnID: createArgs.RepliesTo,
|
||||
Type: types.DepRepliesTo,
|
||||
ThreadID: createArgs.RepliesTo, // Use parent ID as thread root
|
||||
}
|
||||
if err := store.AddDependency(ctx, dep, s.reqActor(req)); err != nil {
|
||||
return Response{
|
||||
Success: false,
|
||||
Error: fmt.Sprintf("failed to add replies-to dependency %s -> %s: %v", issue.ID, createArgs.RepliesTo, err),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add labels if specified
|
||||
for _, label := range createArgs.Labels {
|
||||
if err := store.AddLabel(ctx, issue.ID, label, s.reqActor(req)); err != nil {
|
||||
|
||||
@@ -70,60 +70,68 @@ func (s *SQLiteStorage) AddDependency(ctx context.Context, dep *types.Dependency
|
||||
|
||||
return s.withTx(ctx, func(tx *sql.Tx) error {
|
||||
// Cycle Detection and Prevention
|
||||
//
|
||||
// We prevent cycles across ALL dependency types (blocks, related, parent-child, discovered-from)
|
||||
// to maintain a directed acyclic graph (DAG). This is critical for:
|
||||
//
|
||||
// 1. Ready Work Calculation: Cycles can hide issues from the ready list by making them
|
||||
// appear blocked when they're actually part of a circular dependency.
|
||||
//
|
||||
// 2. Dependency Traversal: Operations like dep tree and blocking propagation rely on
|
||||
// DAG structure. Cycles would require special handling and could cause confusion.
|
||||
//
|
||||
// 3. Semantic Clarity: Circular dependencies are conceptually problematic - if A depends
|
||||
// on B and B depends on A (directly or through other issues), which should be done first?
|
||||
//
|
||||
// Implementation: We use a recursive CTE to traverse from DependsOnID to see if we can
|
||||
// reach IssueID. If yes, adding "IssueID depends on DependsOnID" would complete a cycle.
|
||||
// We check ALL dependency types because cross-type cycles (e.g., A blocks B, B parent-child A)
|
||||
// are just as problematic as single-type cycles.
|
||||
//
|
||||
// The traversal is depth-limited to maxDependencyDepth (100) to prevent infinite loops
|
||||
// and excessive query cost. We check before inserting to avoid unnecessary write on failure.
|
||||
var cycleExists bool
|
||||
err = tx.QueryRowContext(ctx, `
|
||||
WITH RECURSIVE paths AS (
|
||||
SELECT
|
||||
issue_id,
|
||||
depends_on_id,
|
||||
1 as depth
|
||||
FROM dependencies
|
||||
WHERE issue_id = ?
|
||||
//
|
||||
// We prevent cycles across most dependency types to maintain a directed acyclic graph (DAG).
|
||||
// This is critical for:
|
||||
//
|
||||
// 1. Ready Work Calculation: Cycles can hide issues from the ready list by making them
|
||||
// appear blocked when they're actually part of a circular dependency.
|
||||
//
|
||||
// 2. Dependency Traversal: Operations like dep tree and blocking propagation rely on
|
||||
// DAG structure. Cycles would require special handling and could cause confusion.
|
||||
//
|
||||
// 3. Semantic Clarity: Circular dependencies are conceptually problematic - if A depends
|
||||
// on B and B depends on A (directly or through other issues), which should be done first?
|
||||
//
|
||||
// EXCEPTION: relates-to links are inherently bidirectional ("see also" relationships).
|
||||
// When A relates-to B, we also create B relates-to A. This is not a cycle in the
|
||||
// problematic sense - it's a symmetric relationship that doesn't affect work ordering.
|
||||
//
|
||||
// Implementation: We use a recursive CTE to traverse from DependsOnID to see if we can
|
||||
// reach IssueID. If yes, adding "IssueID depends on DependsOnID" would complete a cycle.
|
||||
// We check ALL dependency types because cross-type cycles (e.g., A blocks B, B parent-child A)
|
||||
// are just as problematic as single-type cycles.
|
||||
//
|
||||
// The traversal is depth-limited to maxDependencyDepth (100) to prevent infinite loops
|
||||
// and excessive query cost. We check before inserting to avoid unnecessary write on failure.
|
||||
|
||||
UNION ALL
|
||||
// Skip cycle detection for relates-to (inherently bidirectional)
|
||||
if dep.Type != types.DepRelatesTo {
|
||||
var cycleExists bool
|
||||
err = tx.QueryRowContext(ctx, `
|
||||
WITH RECURSIVE paths AS (
|
||||
SELECT
|
||||
issue_id,
|
||||
depends_on_id,
|
||||
1 as depth
|
||||
FROM dependencies
|
||||
WHERE issue_id = ?
|
||||
|
||||
SELECT
|
||||
d.issue_id,
|
||||
d.depends_on_id,
|
||||
p.depth + 1
|
||||
FROM dependencies d
|
||||
JOIN paths p ON d.issue_id = p.depends_on_id
|
||||
WHERE p.depth < ?
|
||||
)
|
||||
SELECT EXISTS(
|
||||
SELECT 1 FROM paths
|
||||
WHERE depends_on_id = ?
|
||||
)
|
||||
`, dep.DependsOnID, maxDependencyDepth, dep.IssueID).Scan(&cycleExists)
|
||||
UNION ALL
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to check for cycles: %w", err)
|
||||
}
|
||||
SELECT
|
||||
d.issue_id,
|
||||
d.depends_on_id,
|
||||
p.depth + 1
|
||||
FROM dependencies d
|
||||
JOIN paths p ON d.issue_id = p.depends_on_id
|
||||
WHERE p.depth < ?
|
||||
)
|
||||
SELECT EXISTS(
|
||||
SELECT 1 FROM paths
|
||||
WHERE depends_on_id = ?
|
||||
)
|
||||
`, dep.DependsOnID, maxDependencyDepth, dep.IssueID).Scan(&cycleExists)
|
||||
|
||||
if cycleExists {
|
||||
return fmt.Errorf("cannot add dependency: would create a cycle (%s → %s → ... → %s)",
|
||||
dep.IssueID, dep.DependsOnID, dep.IssueID)
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to check for cycles: %w", err)
|
||||
}
|
||||
|
||||
if cycleExists {
|
||||
return fmt.Errorf("cannot add dependency: would create a cycle (%s → %s → ... → %s)",
|
||||
dep.IssueID, dep.DependsOnID, dep.IssueID)
|
||||
}
|
||||
}
|
||||
|
||||
// Insert dependency (including metadata and thread_id for edge consolidation - Decision 004)
|
||||
_, err = tx.ExecContext(ctx, `
|
||||
@@ -225,7 +233,7 @@ func (s *SQLiteStorage) GetDependenciesWithMetadata(ctx context.Context, issueID
|
||||
i.status, i.priority, i.issue_type, i.assignee, i.estimated_minutes,
|
||||
i.created_at, i.updated_at, i.closed_at, i.external_ref, i.source_repo,
|
||||
i.deleted_at, i.deleted_by, i.delete_reason, i.original_type,
|
||||
i.sender, i.ephemeral, i.replies_to, i.relates_to, i.duplicate_of, i.superseded_by,
|
||||
i.sender, i.ephemeral,
|
||||
d.type
|
||||
FROM issues i
|
||||
JOIN dependencies d ON i.id = d.depends_on_id
|
||||
@@ -247,7 +255,7 @@ func (s *SQLiteStorage) GetDependentsWithMetadata(ctx context.Context, issueID s
|
||||
i.status, i.priority, i.issue_type, i.assignee, i.estimated_minutes,
|
||||
i.created_at, i.updated_at, i.closed_at, i.external_ref, i.source_repo,
|
||||
i.deleted_at, i.deleted_by, i.delete_reason, i.original_type,
|
||||
i.sender, i.ephemeral, i.replies_to, i.relates_to, i.duplicate_of, i.superseded_by,
|
||||
i.sender, i.ephemeral,
|
||||
d.type
|
||||
FROM issues i
|
||||
JOIN dependencies d ON i.id = d.issue_id
|
||||
@@ -706,10 +714,6 @@ func (s *SQLiteStorage) scanIssues(ctx context.Context, rows *sql.Rows) ([]*type
|
||||
// Messaging fields (bd-kwro)
|
||||
var sender sql.NullString
|
||||
var ephemeral sql.NullInt64
|
||||
var repliesTo sql.NullString
|
||||
var relatesTo sql.NullString
|
||||
var duplicateOf sql.NullString
|
||||
var supersededBy sql.NullString
|
||||
|
||||
err := rows.Scan(
|
||||
&issue.ID, &contentHash, &issue.Title, &issue.Description, &issue.Design,
|
||||
@@ -717,7 +721,7 @@ func (s *SQLiteStorage) scanIssues(ctx context.Context, rows *sql.Rows) ([]*type
|
||||
&issue.Priority, &issue.IssueType, &assignee, &estimatedMinutes,
|
||||
&issue.CreatedAt, &issue.UpdatedAt, &closedAt, &externalRef, &sourceRepo, &closeReason,
|
||||
&deletedAt, &deletedBy, &deleteReason, &originalType,
|
||||
&sender, &ephemeral, &repliesTo, &relatesTo, &duplicateOf, &supersededBy,
|
||||
&sender, &ephemeral,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to scan issue: %w", err)
|
||||
@@ -762,18 +766,6 @@ func (s *SQLiteStorage) scanIssues(ctx context.Context, rows *sql.Rows) ([]*type
|
||||
if ephemeral.Valid && ephemeral.Int64 != 0 {
|
||||
issue.Ephemeral = true
|
||||
}
|
||||
if repliesTo.Valid {
|
||||
issue.RepliesTo = repliesTo.String
|
||||
}
|
||||
if relatesTo.Valid && relatesTo.String != "" {
|
||||
issue.RelatesTo = parseJSONStringArray(relatesTo.String)
|
||||
}
|
||||
if duplicateOf.Valid {
|
||||
issue.DuplicateOf = duplicateOf.String
|
||||
}
|
||||
if supersededBy.Valid {
|
||||
issue.SupersededBy = supersededBy.String
|
||||
}
|
||||
|
||||
issues = append(issues, &issue)
|
||||
issueIDs = append(issueIDs, issue.ID)
|
||||
@@ -813,10 +805,6 @@ func (s *SQLiteStorage) scanIssuesWithDependencyType(ctx context.Context, rows *
|
||||
// Messaging fields (bd-kwro)
|
||||
var sender sql.NullString
|
||||
var ephemeral sql.NullInt64
|
||||
var repliesTo sql.NullString
|
||||
var relatesTo sql.NullString
|
||||
var duplicateOf sql.NullString
|
||||
var supersededBy sql.NullString
|
||||
var depType types.DependencyType
|
||||
|
||||
err := rows.Scan(
|
||||
@@ -825,7 +813,7 @@ func (s *SQLiteStorage) scanIssuesWithDependencyType(ctx context.Context, rows *
|
||||
&issue.Priority, &issue.IssueType, &assignee, &estimatedMinutes,
|
||||
&issue.CreatedAt, &issue.UpdatedAt, &closedAt, &externalRef, &sourceRepo,
|
||||
&deletedAt, &deletedBy, &deleteReason, &originalType,
|
||||
&sender, &ephemeral, &repliesTo, &relatesTo, &duplicateOf, &supersededBy,
|
||||
&sender, &ephemeral,
|
||||
&depType,
|
||||
)
|
||||
if err != nil {
|
||||
@@ -868,18 +856,6 @@ func (s *SQLiteStorage) scanIssuesWithDependencyType(ctx context.Context, rows *
|
||||
if ephemeral.Valid && ephemeral.Int64 != 0 {
|
||||
issue.Ephemeral = true
|
||||
}
|
||||
if repliesTo.Valid {
|
||||
issue.RepliesTo = repliesTo.String
|
||||
}
|
||||
if relatesTo.Valid && relatesTo.String != "" {
|
||||
issue.RelatesTo = parseJSONStringArray(relatesTo.String)
|
||||
}
|
||||
if duplicateOf.Valid {
|
||||
issue.DuplicateOf = duplicateOf.String
|
||||
}
|
||||
if supersededBy.Valid {
|
||||
issue.SupersededBy = supersededBy.String
|
||||
}
|
||||
|
||||
// Fetch labels for this issue
|
||||
labels, err := s.GetLabels(ctx, issue.ID)
|
||||
|
||||
@@ -2,13 +2,14 @@ package sqlite
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/steveyegge/beads/internal/types"
|
||||
)
|
||||
|
||||
// TestRelatesTo verifies relates-to dependencies work via the dependency API.
|
||||
// Per Decision 004, relates-to links are now stored in the dependencies table.
|
||||
func TestRelatesTo(t *testing.T) {
|
||||
store, cleanup := setupTestDB(t)
|
||||
defer cleanup()
|
||||
@@ -39,36 +40,51 @@ func TestRelatesTo(t *testing.T) {
|
||||
t.Fatalf("Failed to create issue2: %v", err)
|
||||
}
|
||||
|
||||
// Add relates_to link (bidirectional)
|
||||
relatesTo1, _ := json.Marshal([]string{issue2.ID})
|
||||
if err := store.UpdateIssue(ctx, issue1.ID, map[string]interface{}{
|
||||
"relates_to": string(relatesTo1),
|
||||
}, "test"); err != nil {
|
||||
t.Fatalf("Failed to update issue1 relates_to: %v", err)
|
||||
// Add relates-to dependency (bidirectional)
|
||||
dep1 := &types.Dependency{
|
||||
IssueID: issue1.ID,
|
||||
DependsOnID: issue2.ID,
|
||||
Type: types.DepRelatesTo,
|
||||
}
|
||||
if err := store.AddDependency(ctx, dep1, "test"); err != nil {
|
||||
t.Fatalf("Failed to add relates-to dep1: %v", err)
|
||||
}
|
||||
dep2 := &types.Dependency{
|
||||
IssueID: issue2.ID,
|
||||
DependsOnID: issue1.ID,
|
||||
Type: types.DepRelatesTo,
|
||||
}
|
||||
if err := store.AddDependency(ctx, dep2, "test"); err != nil {
|
||||
t.Fatalf("Failed to add relates-to dep2: %v", err)
|
||||
}
|
||||
|
||||
relatesTo2, _ := json.Marshal([]string{issue1.ID})
|
||||
if err := store.UpdateIssue(ctx, issue2.ID, map[string]interface{}{
|
||||
"relates_to": string(relatesTo2),
|
||||
}, "test"); err != nil {
|
||||
t.Fatalf("Failed to update issue2 relates_to: %v", err)
|
||||
}
|
||||
|
||||
// Verify links
|
||||
updated1, err := store.GetIssue(ctx, issue1.ID)
|
||||
// Verify links via GetDependenciesWithMetadata
|
||||
deps1, err := store.GetDependenciesWithMetadata(ctx, issue1.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("GetIssue failed: %v", err)
|
||||
t.Fatalf("GetDependenciesWithMetadata failed: %v", err)
|
||||
}
|
||||
if len(updated1.RelatesTo) != 1 || updated1.RelatesTo[0] != issue2.ID {
|
||||
t.Errorf("issue1.RelatesTo = %v, want [%s]", updated1.RelatesTo, issue2.ID)
|
||||
found1 := false
|
||||
for _, d := range deps1 {
|
||||
if d.ID == issue2.ID && d.DependencyType == types.DepRelatesTo {
|
||||
found1 = true
|
||||
}
|
||||
}
|
||||
if !found1 {
|
||||
t.Errorf("issue1 should have relates-to link to issue2")
|
||||
}
|
||||
|
||||
updated2, err := store.GetIssue(ctx, issue2.ID)
|
||||
deps2, err := store.GetDependenciesWithMetadata(ctx, issue2.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("GetIssue failed: %v", err)
|
||||
t.Fatalf("GetDependenciesWithMetadata failed: %v", err)
|
||||
}
|
||||
if len(updated2.RelatesTo) != 1 || updated2.RelatesTo[0] != issue1.ID {
|
||||
t.Errorf("issue2.RelatesTo = %v, want [%s]", updated2.RelatesTo, issue1.ID)
|
||||
found2 := false
|
||||
for _, d := range deps2 {
|
||||
if d.ID == issue1.ID && d.DependencyType == types.DepRelatesTo {
|
||||
found2 = true
|
||||
}
|
||||
}
|
||||
if !found2 {
|
||||
t.Errorf("issue2 should have relates-to link to issue1")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,23 +110,34 @@ func TestRelatesTo_MultipleLinks(t *testing.T) {
|
||||
}
|
||||
|
||||
// Link issue0 to both issue1 and issue2
|
||||
relatesTo, _ := json.Marshal([]string{issues[1].ID, issues[2].ID})
|
||||
if err := store.UpdateIssue(ctx, issues[0].ID, map[string]interface{}{
|
||||
"relates_to": string(relatesTo),
|
||||
}, "test"); err != nil {
|
||||
t.Fatalf("Failed to update relates_to: %v", err)
|
||||
for _, targetIssue := range []*types.Issue{issues[1], issues[2]} {
|
||||
dep := &types.Dependency{
|
||||
IssueID: issues[0].ID,
|
||||
DependsOnID: targetIssue.ID,
|
||||
Type: types.DepRelatesTo,
|
||||
}
|
||||
if err := store.AddDependency(ctx, dep, "test"); err != nil {
|
||||
t.Fatalf("Failed to add relates-to: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Verify
|
||||
updated, err := store.GetIssue(ctx, issues[0].ID)
|
||||
deps, err := store.GetDependenciesWithMetadata(ctx, issues[0].ID)
|
||||
if err != nil {
|
||||
t.Fatalf("GetIssue failed: %v", err)
|
||||
t.Fatalf("GetDependenciesWithMetadata failed: %v", err)
|
||||
}
|
||||
if len(updated.RelatesTo) != 2 {
|
||||
t.Errorf("RelatesTo has %d links, want 2", len(updated.RelatesTo))
|
||||
relatesCount := 0
|
||||
for _, d := range deps {
|
||||
if d.DependencyType == types.DepRelatesTo {
|
||||
relatesCount++
|
||||
}
|
||||
}
|
||||
if relatesCount != 2 {
|
||||
t.Errorf("RelatesTo has %d links, want 2", relatesCount)
|
||||
}
|
||||
}
|
||||
|
||||
// TestDuplicateOf verifies duplicates dependencies work via the dependency API.
|
||||
func TestDuplicateOf(t *testing.T) {
|
||||
store, cleanup := setupTestDB(t)
|
||||
defer cleanup()
|
||||
@@ -141,27 +168,47 @@ func TestDuplicateOf(t *testing.T) {
|
||||
t.Fatalf("Failed to create duplicate: %v", err)
|
||||
}
|
||||
|
||||
// Mark as duplicate and close
|
||||
if err := store.UpdateIssue(ctx, duplicate.ID, map[string]interface{}{
|
||||
"duplicate_of": canonical.ID,
|
||||
"status": string(types.StatusClosed),
|
||||
}, "test"); err != nil {
|
||||
t.Fatalf("Failed to mark as duplicate: %v", err)
|
||||
// Add duplicates dependency
|
||||
dep := &types.Dependency{
|
||||
IssueID: duplicate.ID,
|
||||
DependsOnID: canonical.ID,
|
||||
Type: types.DepDuplicates,
|
||||
}
|
||||
if err := store.AddDependency(ctx, dep, "test"); err != nil {
|
||||
t.Fatalf("Failed to add duplicates dep: %v", err)
|
||||
}
|
||||
|
||||
// Verify
|
||||
// Close the duplicate
|
||||
if err := store.CloseIssue(ctx, duplicate.ID, "Closed as duplicate", "test"); err != nil {
|
||||
t.Fatalf("Failed to close duplicate: %v", err)
|
||||
}
|
||||
|
||||
// Verify dependency
|
||||
deps, err := store.GetDependenciesWithMetadata(ctx, duplicate.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("GetDependenciesWithMetadata failed: %v", err)
|
||||
}
|
||||
found := false
|
||||
for _, d := range deps {
|
||||
if d.ID == canonical.ID && d.DependencyType == types.DepDuplicates {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("duplicate should have duplicates link to canonical")
|
||||
}
|
||||
|
||||
// Verify closed status
|
||||
updated, err := store.GetIssue(ctx, duplicate.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("GetIssue failed: %v", err)
|
||||
}
|
||||
if updated.DuplicateOf != canonical.ID {
|
||||
t.Errorf("DuplicateOf = %q, want %q", updated.DuplicateOf, canonical.ID)
|
||||
}
|
||||
if updated.Status != types.StatusClosed {
|
||||
t.Errorf("Status = %q, want %q", updated.Status, types.StatusClosed)
|
||||
}
|
||||
}
|
||||
|
||||
// TestSupersededBy verifies supersedes dependencies work via the dependency API.
|
||||
func TestSupersededBy(t *testing.T) {
|
||||
store, cleanup := setupTestDB(t)
|
||||
defer cleanup()
|
||||
@@ -192,27 +239,48 @@ func TestSupersededBy(t *testing.T) {
|
||||
t.Fatalf("Failed to create new version: %v", err)
|
||||
}
|
||||
|
||||
// Mark old as superseded
|
||||
if err := store.UpdateIssue(ctx, oldVersion.ID, map[string]interface{}{
|
||||
"superseded_by": newVersion.ID,
|
||||
"status": string(types.StatusClosed),
|
||||
}, "test"); err != nil {
|
||||
t.Fatalf("Failed to mark as superseded: %v", err)
|
||||
// Add supersedes dependency (newVersion supersedes oldVersion)
|
||||
// Stored as: oldVersion depends on newVersion with type supersedes
|
||||
dep := &types.Dependency{
|
||||
IssueID: oldVersion.ID,
|
||||
DependsOnID: newVersion.ID,
|
||||
Type: types.DepSupersedes,
|
||||
}
|
||||
if err := store.AddDependency(ctx, dep, "test"); err != nil {
|
||||
t.Fatalf("Failed to add supersedes dep: %v", err)
|
||||
}
|
||||
|
||||
// Verify
|
||||
// Close old version
|
||||
if err := store.CloseIssue(ctx, oldVersion.ID, "Superseded by v2", "test"); err != nil {
|
||||
t.Fatalf("Failed to close old version: %v", err)
|
||||
}
|
||||
|
||||
// Verify dependency
|
||||
deps, err := store.GetDependenciesWithMetadata(ctx, oldVersion.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("GetDependenciesWithMetadata failed: %v", err)
|
||||
}
|
||||
found := false
|
||||
for _, d := range deps {
|
||||
if d.ID == newVersion.ID && d.DependencyType == types.DepSupersedes {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("oldVersion should have supersedes link to newVersion")
|
||||
}
|
||||
|
||||
// Verify closed status
|
||||
updated, err := store.GetIssue(ctx, oldVersion.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("GetIssue failed: %v", err)
|
||||
}
|
||||
if updated.SupersededBy != newVersion.ID {
|
||||
t.Errorf("SupersededBy = %q, want %q", updated.SupersededBy, newVersion.ID)
|
||||
}
|
||||
if updated.Status != types.StatusClosed {
|
||||
t.Errorf("Status = %q, want %q", updated.Status, types.StatusClosed)
|
||||
}
|
||||
}
|
||||
|
||||
// TestRepliesTo verifies replies-to dependencies work via the dependency API.
|
||||
func TestRepliesTo(t *testing.T) {
|
||||
store, cleanup := setupTestDB(t)
|
||||
defer cleanup()
|
||||
@@ -247,20 +315,34 @@ func TestRepliesTo(t *testing.T) {
|
||||
if err := store.CreateIssue(ctx, original, "test"); err != nil {
|
||||
t.Fatalf("Failed to create original: %v", err)
|
||||
}
|
||||
|
||||
// Set replies_to before creation
|
||||
reply.RepliesTo = original.ID
|
||||
if err := store.CreateIssue(ctx, reply, "test"); err != nil {
|
||||
t.Fatalf("Failed to create reply: %v", err)
|
||||
}
|
||||
|
||||
// Verify thread link
|
||||
savedReply, err := store.GetIssue(ctx, reply.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("GetIssue failed: %v", err)
|
||||
// Add replies-to dependency
|
||||
dep := &types.Dependency{
|
||||
IssueID: reply.ID,
|
||||
DependsOnID: original.ID,
|
||||
Type: types.DepRepliesTo,
|
||||
ThreadID: original.ID, // Thread root is the original message
|
||||
}
|
||||
if savedReply.RepliesTo != original.ID {
|
||||
t.Errorf("RepliesTo = %q, want %q", savedReply.RepliesTo, original.ID)
|
||||
if err := store.AddDependency(ctx, dep, "test"); err != nil {
|
||||
t.Fatalf("Failed to add replies-to dep: %v", err)
|
||||
}
|
||||
|
||||
// Verify thread link
|
||||
deps, err := store.GetDependenciesWithMetadata(ctx, reply.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("GetDependenciesWithMetadata failed: %v", err)
|
||||
}
|
||||
found := false
|
||||
for _, d := range deps {
|
||||
if d.ID == original.ID && d.DependencyType == types.DepRepliesTo {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("reply should have replies-to link to original")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -282,24 +364,42 @@ func TestRepliesTo_Chain(t *testing.T) {
|
||||
Sender: "user",
|
||||
Assignee: "inbox",
|
||||
Ephemeral: true,
|
||||
RepliesTo: prevID,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
if err := store.CreateIssue(ctx, messages[i], "test"); err != nil {
|
||||
t.Fatalf("Failed to create message %d: %v", i, err)
|
||||
}
|
||||
|
||||
// Add replies-to dependency for subsequent messages
|
||||
if prevID != "" {
|
||||
dep := &types.Dependency{
|
||||
IssueID: messages[i].ID,
|
||||
DependsOnID: prevID,
|
||||
Type: types.DepRepliesTo,
|
||||
ThreadID: messages[0].ID, // Thread root is the first message
|
||||
}
|
||||
if err := store.AddDependency(ctx, dep, "test"); err != nil {
|
||||
t.Fatalf("Failed to add replies-to dep for message %d: %v", i, err)
|
||||
}
|
||||
}
|
||||
prevID = messages[i].ID
|
||||
}
|
||||
|
||||
// Verify chain
|
||||
for i := 1; i < len(messages); i++ {
|
||||
saved, err := store.GetIssue(ctx, messages[i].ID)
|
||||
// Verify chain by checking dependents
|
||||
for i := 0; i < len(messages)-1; i++ {
|
||||
dependents, err := store.GetDependentsWithMetadata(ctx, messages[i].ID)
|
||||
if err != nil {
|
||||
t.Fatalf("GetIssue failed for message %d: %v", i, err)
|
||||
t.Fatalf("GetDependentsWithMetadata failed for message %d: %v", i, err)
|
||||
}
|
||||
if saved.RepliesTo != messages[i-1].ID {
|
||||
t.Errorf("Message %d: RepliesTo = %q, want %q", i, saved.RepliesTo, messages[i-1].ID)
|
||||
found := false
|
||||
for _, d := range dependents {
|
||||
if d.ID == messages[i+1].ID && d.DependencyType == types.DepRepliesTo {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("Message %d should have reply from message %d", i, i+1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,8 +15,6 @@ func insertIssue(ctx context.Context, conn *sql.Conn, issue *types.Issue) error
|
||||
sourceRepo = "." // Default to primary repo
|
||||
}
|
||||
|
||||
// Format relates_to as JSON for storage
|
||||
relatesTo := formatJSONStringArray(issue.RelatesTo)
|
||||
ephemeral := 0
|
||||
if issue.Ephemeral {
|
||||
ephemeral = 1
|
||||
@@ -28,8 +26,8 @@ func insertIssue(ctx context.Context, conn *sql.Conn, issue *types.Issue) error
|
||||
status, priority, issue_type, assignee, estimated_minutes,
|
||||
created_at, updated_at, closed_at, external_ref, source_repo, close_reason,
|
||||
deleted_at, deleted_by, delete_reason, original_type,
|
||||
sender, ephemeral, replies_to, relates_to, duplicate_of, superseded_by
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
sender, ephemeral
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`,
|
||||
issue.ID, issue.ContentHash, issue.Title, issue.Description, issue.Design,
|
||||
issue.AcceptanceCriteria, issue.Notes, issue.Status,
|
||||
@@ -37,7 +35,7 @@ func insertIssue(ctx context.Context, conn *sql.Conn, issue *types.Issue) error
|
||||
issue.EstimatedMinutes, issue.CreatedAt, issue.UpdatedAt,
|
||||
issue.ClosedAt, issue.ExternalRef, sourceRepo, issue.CloseReason,
|
||||
issue.DeletedAt, issue.DeletedBy, issue.DeleteReason, issue.OriginalType,
|
||||
issue.Sender, ephemeral, issue.RepliesTo, relatesTo, issue.DuplicateOf, issue.SupersededBy,
|
||||
issue.Sender, ephemeral,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to insert issue: %w", err)
|
||||
@@ -53,8 +51,8 @@ func insertIssues(ctx context.Context, conn *sql.Conn, issues []*types.Issue) er
|
||||
status, priority, issue_type, assignee, estimated_minutes,
|
||||
created_at, updated_at, closed_at, external_ref, source_repo, close_reason,
|
||||
deleted_at, deleted_by, delete_reason, original_type,
|
||||
sender, ephemeral, replies_to, relates_to, duplicate_of, superseded_by
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
sender, ephemeral
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to prepare statement: %w", err)
|
||||
@@ -67,8 +65,6 @@ func insertIssues(ctx context.Context, conn *sql.Conn, issues []*types.Issue) er
|
||||
sourceRepo = "." // Default to primary repo
|
||||
}
|
||||
|
||||
// Format relates_to as JSON for storage
|
||||
relatesTo := formatJSONStringArray(issue.RelatesTo)
|
||||
ephemeral := 0
|
||||
if issue.Ephemeral {
|
||||
ephemeral = 1
|
||||
@@ -81,7 +77,7 @@ func insertIssues(ctx context.Context, conn *sql.Conn, issues []*types.Issue) er
|
||||
issue.EstimatedMinutes, issue.CreatedAt, issue.UpdatedAt,
|
||||
issue.ClosedAt, issue.ExternalRef, sourceRepo, issue.CloseReason,
|
||||
issue.DeletedAt, issue.DeletedBy, issue.DeleteReason, issue.OriginalType,
|
||||
issue.Sender, ephemeral, issue.RepliesTo, relatesTo, issue.DuplicateOf, issue.SupersededBy,
|
||||
issue.Sender, ephemeral,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to insert issue %s: %w", issue.ID, err)
|
||||
|
||||
@@ -159,7 +159,7 @@ func (s *SQLiteStorage) GetIssuesByLabel(ctx context.Context, label string) ([]*
|
||||
i.status, i.priority, i.issue_type, i.assignee, i.estimated_minutes,
|
||||
i.created_at, i.updated_at, i.closed_at, i.external_ref, i.source_repo, i.close_reason,
|
||||
i.deleted_at, i.deleted_by, i.delete_reason, i.original_type,
|
||||
i.sender, i.ephemeral, i.replies_to, i.relates_to, i.duplicate_of, i.superseded_by
|
||||
i.sender, i.ephemeral
|
||||
FROM issues i
|
||||
JOIN labels l ON i.id = l.issue_id
|
||||
WHERE l.label = ?
|
||||
|
||||
@@ -38,6 +38,7 @@ var migrationsList = []Migration{
|
||||
{"messaging_fields", migrations.MigrateMessagingFields},
|
||||
{"edge_consolidation", migrations.MigrateEdgeConsolidation},
|
||||
{"migrate_edge_fields", migrations.MigrateEdgeFields},
|
||||
{"drop_edge_columns", migrations.MigrateDropEdgeColumns},
|
||||
}
|
||||
|
||||
// MigrationInfo contains metadata about a migration for inspection
|
||||
@@ -83,6 +84,7 @@ func getMigrationDescription(name string) string {
|
||||
"messaging_fields": "Adds messaging fields (sender, ephemeral, replies_to, relates_to, duplicate_of, superseded_by) for inter-agent communication (bd-kwro)",
|
||||
"edge_consolidation": "Adds metadata and thread_id columns to dependencies table for edge schema consolidation (Decision 004)",
|
||||
"migrate_edge_fields": "Migrates existing issue fields (replies_to, relates_to, duplicate_of, superseded_by) to dependency edges (Decision 004 Phase 3)",
|
||||
"drop_edge_columns": "Drops deprecated edge columns (replies_to, relates_to, duplicate_of, superseded_by) from issues table (Decision 004 Phase 4)",
|
||||
}
|
||||
|
||||
if desc, ok := descriptions[name]; ok {
|
||||
|
||||
236
internal/storage/sqlite/migrations/022_drop_edge_columns.go
Normal file
236
internal/storage/sqlite/migrations/022_drop_edge_columns.go
Normal file
@@ -0,0 +1,236 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// MigrateDropEdgeColumns removes the deprecated edge fields from the issues table.
|
||||
// This is Phase 4 of the Edge Schema Consolidation (Decision 004).
|
||||
//
|
||||
// Removes columns:
|
||||
// - replies_to (now: replies-to dependency)
|
||||
// - relates_to (now: relates-to dependencies)
|
||||
// - duplicate_of (now: duplicates dependency)
|
||||
// - superseded_by (now: supersedes dependency)
|
||||
//
|
||||
// Prerequisites:
|
||||
// - Migration 021 (migrate_edge_fields) must have already run to convert data
|
||||
// - All code must be updated to use the dependencies API
|
||||
//
|
||||
// SQLite doesn't support DROP COLUMN directly in older versions, so we
|
||||
// recreate the table without the deprecated columns.
|
||||
func MigrateDropEdgeColumns(db *sql.DB) error {
|
||||
// Check if any of the columns still exist
|
||||
var hasRepliesTo, hasRelatesTo, hasDuplicateOf, hasSupersededBy bool
|
||||
|
||||
checkCol := func(name string) (bool, error) {
|
||||
var exists bool
|
||||
err := db.QueryRow(`
|
||||
SELECT COUNT(*) > 0
|
||||
FROM pragma_table_info('issues')
|
||||
WHERE name = ?
|
||||
`, name).Scan(&exists)
|
||||
return exists, err
|
||||
}
|
||||
|
||||
var err error
|
||||
hasRepliesTo, err = checkCol("replies_to")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to check replies_to column: %w", err)
|
||||
}
|
||||
hasRelatesTo, err = checkCol("relates_to")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to check relates_to column: %w", err)
|
||||
}
|
||||
hasDuplicateOf, err = checkCol("duplicate_of")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to check duplicate_of column: %w", err)
|
||||
}
|
||||
hasSupersededBy, err = checkCol("superseded_by")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to check superseded_by column: %w", err)
|
||||
}
|
||||
|
||||
// If none of the columns exist, migration already ran
|
||||
if !hasRepliesTo && !hasRelatesTo && !hasDuplicateOf && !hasSupersededBy {
|
||||
return nil
|
||||
}
|
||||
|
||||
// SQLite 3.35.0+ supports DROP COLUMN, but we use table recreation for compatibility
|
||||
// This is idempotent - we recreate the table without the deprecated columns
|
||||
|
||||
// CRITICAL: Disable foreign keys to prevent CASCADE deletes when we drop the issues table
|
||||
// The dependencies table has FOREIGN KEY (depends_on_id) REFERENCES issues(id) ON DELETE CASCADE
|
||||
// Without disabling foreign keys, dropping the issues table would delete all dependencies!
|
||||
_, err = db.Exec(`PRAGMA foreign_keys = OFF`)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to disable foreign keys: %w", err)
|
||||
}
|
||||
// Re-enable foreign keys at the end (deferred to ensure it runs)
|
||||
defer db.Exec(`PRAGMA foreign_keys = ON`)
|
||||
|
||||
// Drop views that depend on the issues table BEFORE starting transaction
|
||||
// This is necessary because SQLite validates views during table operations
|
||||
_, err = db.Exec(`DROP VIEW IF EXISTS ready_issues`)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to drop ready_issues view: %w", err)
|
||||
}
|
||||
_, err = db.Exec(`DROP VIEW IF EXISTS blocked_issues`)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to drop blocked_issues view: %w", err)
|
||||
}
|
||||
|
||||
// Start a transaction for atomicity
|
||||
tx, err := db.Begin()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to begin transaction: %w", err)
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
// Create new table without the deprecated columns
|
||||
_, err = tx.Exec(`
|
||||
CREATE TABLE IF NOT EXISTS issues_new (
|
||||
id TEXT PRIMARY KEY,
|
||||
content_hash TEXT,
|
||||
title TEXT NOT NULL CHECK(length(title) <= 500),
|
||||
description TEXT NOT NULL DEFAULT '',
|
||||
design TEXT NOT NULL DEFAULT '',
|
||||
acceptance_criteria TEXT NOT NULL DEFAULT '',
|
||||
notes TEXT NOT NULL DEFAULT '',
|
||||
status TEXT NOT NULL DEFAULT 'open',
|
||||
priority INTEGER NOT NULL DEFAULT 2 CHECK(priority >= 0 AND priority <= 4),
|
||||
issue_type TEXT NOT NULL DEFAULT 'task',
|
||||
assignee TEXT,
|
||||
estimated_minutes INTEGER,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
closed_at DATETIME,
|
||||
external_ref TEXT,
|
||||
source_repo TEXT DEFAULT '',
|
||||
compaction_level INTEGER DEFAULT 0,
|
||||
compacted_at DATETIME,
|
||||
compacted_at_commit TEXT,
|
||||
original_size INTEGER,
|
||||
deleted_at DATETIME,
|
||||
deleted_by TEXT DEFAULT '',
|
||||
delete_reason TEXT DEFAULT '',
|
||||
original_type TEXT DEFAULT '',
|
||||
sender TEXT DEFAULT '',
|
||||
ephemeral INTEGER DEFAULT 0,
|
||||
close_reason TEXT DEFAULT '',
|
||||
CHECK ((status = 'closed') = (closed_at IS NOT NULL))
|
||||
)
|
||||
`)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create new issues table: %w", err)
|
||||
}
|
||||
|
||||
// Copy data from old table to new table (excluding deprecated columns)
|
||||
_, err = tx.Exec(`
|
||||
INSERT INTO issues_new (
|
||||
id, content_hash, title, description, design, acceptance_criteria,
|
||||
notes, status, priority, issue_type, assignee, estimated_minutes,
|
||||
created_at, updated_at, closed_at, external_ref, source_repo, compaction_level,
|
||||
compacted_at, compacted_at_commit, original_size, deleted_at,
|
||||
deleted_by, delete_reason, original_type, sender, ephemeral, close_reason
|
||||
)
|
||||
SELECT
|
||||
id, content_hash, title, description, design, acceptance_criteria,
|
||||
notes, status, priority, issue_type, assignee, estimated_minutes,
|
||||
created_at, updated_at, closed_at, external_ref, COALESCE(source_repo, ''), compaction_level,
|
||||
compacted_at, compacted_at_commit, original_size, deleted_at,
|
||||
deleted_by, delete_reason, original_type, sender, ephemeral,
|
||||
COALESCE(close_reason, '')
|
||||
FROM issues
|
||||
`)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to copy issues data: %w", err)
|
||||
}
|
||||
|
||||
// Drop old table
|
||||
_, err = tx.Exec(`DROP TABLE issues`)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to drop old issues table: %w", err)
|
||||
}
|
||||
|
||||
// Rename new table to issues
|
||||
_, err = tx.Exec(`ALTER TABLE issues_new RENAME TO issues`)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to rename new issues table: %w", err)
|
||||
}
|
||||
|
||||
// Recreate indexes
|
||||
indexes := []string{
|
||||
`CREATE INDEX IF NOT EXISTS idx_issues_status ON issues(status)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_issues_priority ON issues(priority)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_issues_assignee ON issues(assignee)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_issues_created_at ON issues(created_at)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_issues_external_ref ON issues(external_ref) WHERE external_ref IS NOT NULL`,
|
||||
}
|
||||
|
||||
for _, idx := range indexes {
|
||||
_, err = tx.Exec(idx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create index: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Commit transaction
|
||||
if err := tx.Commit(); err != nil {
|
||||
return fmt.Errorf("failed to commit migration: %w", err)
|
||||
}
|
||||
|
||||
// Recreate views that we dropped earlier (after commit, outside transaction)
|
||||
// ready_issues view
|
||||
_, err = db.Exec(`
|
||||
CREATE VIEW IF NOT EXISTS ready_issues AS
|
||||
WITH RECURSIVE
|
||||
blocked_directly AS (
|
||||
SELECT DISTINCT d.issue_id
|
||||
FROM dependencies d
|
||||
JOIN issues blocker ON d.depends_on_id = blocker.id
|
||||
WHERE d.type = 'blocks'
|
||||
AND blocker.status IN ('open', 'in_progress', 'blocked')
|
||||
),
|
||||
blocked_transitively AS (
|
||||
SELECT issue_id, 0 as depth
|
||||
FROM blocked_directly
|
||||
UNION ALL
|
||||
SELECT d.issue_id, bt.depth + 1
|
||||
FROM blocked_transitively bt
|
||||
JOIN dependencies d ON d.depends_on_id = bt.issue_id
|
||||
WHERE d.type = 'parent-child'
|
||||
AND bt.depth < 50
|
||||
)
|
||||
SELECT i.*
|
||||
FROM issues i
|
||||
WHERE i.status = 'open'
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM blocked_transitively WHERE issue_id = i.id
|
||||
)
|
||||
`)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to recreate ready_issues view: %w", err)
|
||||
}
|
||||
|
||||
// blocked_issues view
|
||||
_, err = db.Exec(`
|
||||
CREATE VIEW IF NOT EXISTS blocked_issues AS
|
||||
SELECT
|
||||
i.*,
|
||||
COUNT(d.depends_on_id) as blocked_by_count
|
||||
FROM issues i
|
||||
JOIN dependencies d ON i.id = d.issue_id
|
||||
JOIN issues blocker ON d.depends_on_id = blocker.id
|
||||
WHERE i.status IN ('open', 'in_progress', 'blocked')
|
||||
AND d.type = 'blocks'
|
||||
AND blocker.status IN ('open', 'in_progress', 'blocked')
|
||||
GROUP BY i.id
|
||||
`)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to recreate blocked_issues view: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -257,8 +257,6 @@ func (s *SQLiteStorage) upsertIssueInTx(ctx context.Context, tx *sql.Tx, issue *
|
||||
var existingID string
|
||||
err := tx.QueryRowContext(ctx, `SELECT id FROM issues WHERE id = ?`, issue.ID).Scan(&existingID)
|
||||
|
||||
// Format relates_to as JSON for storage
|
||||
relatesTo := formatJSONStringArray(issue.RelatesTo)
|
||||
ephemeral := 0
|
||||
if issue.Ephemeral {
|
||||
ephemeral = 1
|
||||
@@ -272,8 +270,8 @@ func (s *SQLiteStorage) upsertIssueInTx(ctx context.Context, tx *sql.Tx, issue *
|
||||
status, priority, issue_type, assignee, estimated_minutes,
|
||||
created_at, updated_at, closed_at, external_ref, source_repo, close_reason,
|
||||
deleted_at, deleted_by, delete_reason, original_type,
|
||||
sender, ephemeral, replies_to, relates_to, duplicate_of, superseded_by
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
sender, ephemeral
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`,
|
||||
issue.ID, issue.ContentHash, issue.Title, issue.Description, issue.Design,
|
||||
issue.AcceptanceCriteria, issue.Notes, issue.Status,
|
||||
@@ -281,7 +279,7 @@ func (s *SQLiteStorage) upsertIssueInTx(ctx context.Context, tx *sql.Tx, issue *
|
||||
issue.EstimatedMinutes, issue.CreatedAt, issue.UpdatedAt,
|
||||
issue.ClosedAt, issue.ExternalRef, issue.SourceRepo, issue.CloseReason,
|
||||
issue.DeletedAt, issue.DeletedBy, issue.DeleteReason, issue.OriginalType,
|
||||
issue.Sender, ephemeral, issue.RepliesTo, relatesTo, issue.DuplicateOf, issue.SupersededBy,
|
||||
issue.Sender, ephemeral,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to insert issue: %w", err)
|
||||
@@ -305,7 +303,7 @@ func (s *SQLiteStorage) upsertIssueInTx(ctx context.Context, tx *sql.Tx, issue *
|
||||
issue_type = ?, assignee = ?, estimated_minutes = ?,
|
||||
updated_at = ?, closed_at = ?, external_ref = ?, source_repo = ?,
|
||||
deleted_at = ?, deleted_by = ?, delete_reason = ?, original_type = ?,
|
||||
sender = ?, ephemeral = ?, replies_to = ?, relates_to = ?, duplicate_of = ?, superseded_by = ?
|
||||
sender = ?, ephemeral = ?
|
||||
WHERE id = ?
|
||||
`,
|
||||
issue.ContentHash, issue.Title, issue.Description, issue.Design,
|
||||
@@ -313,7 +311,7 @@ func (s *SQLiteStorage) upsertIssueInTx(ctx context.Context, tx *sql.Tx, issue *
|
||||
issue.IssueType, issue.Assignee, issue.EstimatedMinutes,
|
||||
issue.UpdatedAt, issue.ClosedAt, issue.ExternalRef, issue.SourceRepo,
|
||||
issue.DeletedAt, issue.DeletedBy, issue.DeleteReason, issue.OriginalType,
|
||||
issue.Sender, ephemeral, issue.RepliesTo, relatesTo, issue.DuplicateOf, issue.SupersededBy,
|
||||
issue.Sender, ephemeral,
|
||||
issue.ID,
|
||||
)
|
||||
if err != nil {
|
||||
|
||||
@@ -11,149 +11,10 @@ import (
|
||||
"github.com/steveyegge/beads/internal/types"
|
||||
)
|
||||
|
||||
// createGraphEdgesFromIssueFields creates dependency edges for issue messaging/graph fields.
|
||||
// This implements Phase 2 of Edge Schema Consolidation (Decision 004) - dual-write mode.
|
||||
// When issue fields like RepliesTo, RelatesTo, etc. are set, we also create corresponding
|
||||
// dependency edges. This ensures both the field and the dependency table stay in sync.
|
||||
//
|
||||
// For replies-to edges, we also compute and store the thread_id for efficient thread queries:
|
||||
// - If parent has a thread_id, inherit it
|
||||
// - If parent has no thread_id, use the parent's issue ID as the thread root
|
||||
func createGraphEdgesFromIssueFields(ctx context.Context, conn *sql.Conn, issue *types.Issue, actor string) error {
|
||||
now := time.Now()
|
||||
|
||||
// Helper to insert a dependency edge (no cycle check needed for new issues)
|
||||
insertEdge := func(toID string, edgeType types.DependencyType, metadata, threadID string) error {
|
||||
_, err := conn.ExecContext(ctx, `
|
||||
INSERT OR IGNORE INTO dependencies (issue_id, depends_on_id, type, created_at, created_by, metadata, thread_id)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
`, issue.ID, toID, edgeType, now, actor, metadata, threadID)
|
||||
return err
|
||||
}
|
||||
|
||||
// RepliesTo -> replies-to dependency with thread_id
|
||||
if issue.RepliesTo != "" {
|
||||
// Compute thread_id: check if parent has a thread_id, otherwise use parent's ID
|
||||
var parentThreadID string
|
||||
err := conn.QueryRowContext(ctx, `
|
||||
SELECT COALESCE(
|
||||
(SELECT thread_id FROM dependencies WHERE issue_id = ? AND type = 'replies-to' AND thread_id != '' LIMIT 1),
|
||||
?
|
||||
)
|
||||
`, issue.RepliesTo, issue.RepliesTo).Scan(&parentThreadID)
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
return fmt.Errorf("failed to get parent thread_id: %w", err)
|
||||
}
|
||||
if parentThreadID == "" {
|
||||
parentThreadID = issue.RepliesTo // Use parent's ID as thread root
|
||||
}
|
||||
|
||||
if err := insertEdge(issue.RepliesTo, types.DepRepliesTo, "{}", parentThreadID); err != nil {
|
||||
return fmt.Errorf("failed to create replies-to edge: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// RelatesTo -> relates-to dependencies
|
||||
for _, relatedID := range issue.RelatesTo {
|
||||
if relatedID != "" {
|
||||
if err := insertEdge(relatedID, types.DepRelatesTo, "{}", ""); err != nil {
|
||||
return fmt.Errorf("failed to create relates-to edge for %s: %w", relatedID, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DuplicateOf -> duplicates dependency
|
||||
if issue.DuplicateOf != "" {
|
||||
if err := insertEdge(issue.DuplicateOf, types.DepDuplicates, "{}", ""); err != nil {
|
||||
return fmt.Errorf("failed to create duplicates edge: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// SupersededBy -> supersedes dependency (reversed: this issue is superseded BY another)
|
||||
// So we create: this issue depends on the superseding issue
|
||||
if issue.SupersededBy != "" {
|
||||
if err := insertEdge(issue.SupersededBy, types.DepSupersedes, "{}", ""); err != nil {
|
||||
return fmt.Errorf("failed to create supersedes edge: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// createGraphEdgesFromUpdates creates dependency edges when graph fields are updated.
|
||||
// This implements Phase 2 of Edge Schema Consolidation (Decision 004) for UpdateIssue.
|
||||
func createGraphEdgesFromUpdates(ctx context.Context, tx *sql.Tx, issueID string, updates map[string]interface{}, actor string) error {
|
||||
now := time.Now()
|
||||
|
||||
// Helper to insert a dependency edge
|
||||
insertEdge := func(toID string, edgeType types.DependencyType, metadata, threadID string) error {
|
||||
_, err := tx.ExecContext(ctx, `
|
||||
INSERT OR IGNORE INTO dependencies (issue_id, depends_on_id, type, created_at, created_by, metadata, thread_id)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
`, issueID, toID, edgeType, now, actor, metadata, threadID)
|
||||
return err
|
||||
}
|
||||
|
||||
// RepliesTo -> replies-to dependency with thread_id
|
||||
if repliesTo, ok := updates["replies_to"]; ok {
|
||||
if replyID, isString := repliesTo.(string); isString && replyID != "" {
|
||||
// Compute thread_id
|
||||
var parentThreadID string
|
||||
err := tx.QueryRowContext(ctx, `
|
||||
SELECT COALESCE(
|
||||
(SELECT thread_id FROM dependencies WHERE issue_id = ? AND type = 'replies-to' AND thread_id != '' LIMIT 1),
|
||||
?
|
||||
)
|
||||
`, replyID, replyID).Scan(&parentThreadID)
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
return fmt.Errorf("failed to get parent thread_id: %w", err)
|
||||
}
|
||||
if parentThreadID == "" {
|
||||
parentThreadID = replyID
|
||||
}
|
||||
|
||||
if err := insertEdge(replyID, types.DepRepliesTo, "{}", parentThreadID); err != nil {
|
||||
return fmt.Errorf("failed to create replies-to edge: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// RelatesTo -> relates-to dependencies (JSON string array)
|
||||
if relatesTo, ok := updates["relates_to"]; ok {
|
||||
if relatesStr, isString := relatesTo.(string); isString && relatesStr != "" && relatesStr != "[]" {
|
||||
var relatedIDs []string
|
||||
if err := json.Unmarshal([]byte(relatesStr), &relatedIDs); err == nil {
|
||||
for _, relatedID := range relatedIDs {
|
||||
if relatedID != "" {
|
||||
if err := insertEdge(relatedID, types.DepRelatesTo, "{}", ""); err != nil {
|
||||
return fmt.Errorf("failed to create relates-to edge for %s: %w", relatedID, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DuplicateOf -> duplicates dependency
|
||||
if duplicateOf, ok := updates["duplicate_of"]; ok {
|
||||
if dupID, isString := duplicateOf.(string); isString && dupID != "" {
|
||||
if err := insertEdge(dupID, types.DepDuplicates, "{}", ""); err != nil {
|
||||
return fmt.Errorf("failed to create duplicates edge: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SupersededBy -> supersedes dependency
|
||||
if supersededBy, ok := updates["superseded_by"]; ok {
|
||||
if supID, isString := supersededBy.(string); isString && supID != "" {
|
||||
if err := insertEdge(supID, types.DepSupersedes, "{}", ""); err != nil {
|
||||
return fmt.Errorf("failed to create supersedes edge: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
// NOTE: createGraphEdgesFromIssueFields and createGraphEdgesFromUpdates removed
|
||||
// per Decision 004 Phase 4 - Edge Schema Consolidation.
|
||||
// Graph edges (replies-to, relates-to, duplicates, supersedes) are now managed
|
||||
// exclusively through the dependency API. Use AddDependency() instead.
|
||||
|
||||
// parseNullableTimeString parses a nullable time string from database TEXT columns.
|
||||
// The ncruces/go-sqlite3 driver only auto-converts TEXT→time.Time for columns declared
|
||||
@@ -342,10 +203,8 @@ func (s *SQLiteStorage) CreateIssue(ctx context.Context, issue *types.Issue, act
|
||||
return wrapDBError("record creation event", err)
|
||||
}
|
||||
|
||||
// Create graph edges for messaging/graph fields (Phase 2: dual-write - Decision 004)
|
||||
if err := createGraphEdgesFromIssueFields(ctx, conn, issue, actor); err != nil {
|
||||
return wrapDBError("create graph edges from issue fields", err)
|
||||
}
|
||||
// NOTE: Graph edges (replies-to, relates-to, duplicates, supersedes) are now
|
||||
// managed via AddDependency() per Decision 004 Phase 4.
|
||||
|
||||
// Mark issue as dirty for incremental export
|
||||
if err := markDirty(ctx, conn, issue.ID); err != nil {
|
||||
@@ -384,10 +243,6 @@ func (s *SQLiteStorage) GetIssue(ctx context.Context, id string) (*types.Issue,
|
||||
// Messaging fields (bd-kwro)
|
||||
var sender sql.NullString
|
||||
var ephemeral sql.NullInt64
|
||||
var repliesTo sql.NullString
|
||||
var relatesTo sql.NullString
|
||||
var duplicateOf sql.NullString
|
||||
var supersededBy sql.NullString
|
||||
|
||||
var contentHash sql.NullString
|
||||
var compactedAtCommit sql.NullString
|
||||
@@ -397,7 +252,7 @@ func (s *SQLiteStorage) GetIssue(ctx context.Context, id string) (*types.Issue,
|
||||
created_at, updated_at, closed_at, external_ref,
|
||||
compaction_level, compacted_at, compacted_at_commit, original_size, source_repo, close_reason,
|
||||
deleted_at, deleted_by, delete_reason, original_type,
|
||||
sender, ephemeral, replies_to, relates_to, duplicate_of, superseded_by
|
||||
sender, ephemeral
|
||||
FROM issues
|
||||
WHERE id = ?
|
||||
`, id).Scan(
|
||||
@@ -407,7 +262,7 @@ func (s *SQLiteStorage) GetIssue(ctx context.Context, id string) (*types.Issue,
|
||||
&issue.CreatedAt, &issue.UpdatedAt, &closedAt, &externalRef,
|
||||
&issue.CompactionLevel, &compactedAt, &compactedAtCommit, &originalSize, &sourceRepo, &closeReason,
|
||||
&deletedAt, &deletedBy, &deleteReason, &originalType,
|
||||
&sender, &ephemeral, &repliesTo, &relatesTo, &duplicateOf, &supersededBy,
|
||||
&sender, &ephemeral,
|
||||
)
|
||||
|
||||
if err == sql.ErrNoRows {
|
||||
@@ -465,18 +320,6 @@ func (s *SQLiteStorage) GetIssue(ctx context.Context, id string) (*types.Issue,
|
||||
if ephemeral.Valid && ephemeral.Int64 != 0 {
|
||||
issue.Ephemeral = true
|
||||
}
|
||||
if repliesTo.Valid {
|
||||
issue.RepliesTo = repliesTo.String
|
||||
}
|
||||
if relatesTo.Valid && relatesTo.String != "" {
|
||||
issue.RelatesTo = parseJSONStringArray(relatesTo.String)
|
||||
}
|
||||
if duplicateOf.Valid {
|
||||
issue.DuplicateOf = duplicateOf.String
|
||||
}
|
||||
if supersededBy.Valid {
|
||||
issue.SupersededBy = supersededBy.String
|
||||
}
|
||||
|
||||
// Fetch labels for this issue
|
||||
labels, err := s.GetLabels(ctx, issue.ID)
|
||||
@@ -583,10 +426,6 @@ func (s *SQLiteStorage) GetIssueByExternalRef(ctx context.Context, externalRef s
|
||||
// Messaging fields (bd-kwro)
|
||||
var sender sql.NullString
|
||||
var ephemeral sql.NullInt64
|
||||
var repliesTo sql.NullString
|
||||
var relatesTo sql.NullString
|
||||
var duplicateOf sql.NullString
|
||||
var supersededBy sql.NullString
|
||||
|
||||
err := s.db.QueryRowContext(ctx, `
|
||||
SELECT id, content_hash, title, description, design, acceptance_criteria, notes,
|
||||
@@ -594,7 +433,7 @@ func (s *SQLiteStorage) GetIssueByExternalRef(ctx context.Context, externalRef s
|
||||
created_at, updated_at, closed_at, external_ref,
|
||||
compaction_level, compacted_at, compacted_at_commit, original_size, source_repo, close_reason,
|
||||
deleted_at, deleted_by, delete_reason, original_type,
|
||||
sender, ephemeral, replies_to, relates_to, duplicate_of, superseded_by
|
||||
sender, ephemeral
|
||||
FROM issues
|
||||
WHERE external_ref = ?
|
||||
`, externalRef).Scan(
|
||||
@@ -604,7 +443,7 @@ func (s *SQLiteStorage) GetIssueByExternalRef(ctx context.Context, externalRef s
|
||||
&issue.CreatedAt, &issue.UpdatedAt, &closedAt, &externalRefCol,
|
||||
&issue.CompactionLevel, &compactedAt, &compactedAtCommit, &originalSize, &sourceRepo, &closeReason,
|
||||
&deletedAt, &deletedBy, &deleteReason, &originalType,
|
||||
&sender, &ephemeral, &repliesTo, &relatesTo, &duplicateOf, &supersededBy,
|
||||
&sender, &ephemeral,
|
||||
)
|
||||
|
||||
if err == sql.ErrNoRows {
|
||||
@@ -662,18 +501,6 @@ func (s *SQLiteStorage) GetIssueByExternalRef(ctx context.Context, externalRef s
|
||||
if ephemeral.Valid && ephemeral.Int64 != 0 {
|
||||
issue.Ephemeral = true
|
||||
}
|
||||
if repliesTo.Valid {
|
||||
issue.RepliesTo = repliesTo.String
|
||||
}
|
||||
if relatesTo.Valid && relatesTo.String != "" {
|
||||
issue.RelatesTo = parseJSONStringArray(relatesTo.String)
|
||||
}
|
||||
if duplicateOf.Valid {
|
||||
issue.DuplicateOf = duplicateOf.String
|
||||
}
|
||||
if supersededBy.Valid {
|
||||
issue.SupersededBy = supersededBy.String
|
||||
}
|
||||
|
||||
// Fetch labels for this issue
|
||||
labels, err := s.GetLabels(ctx, issue.ID)
|
||||
@@ -700,12 +527,10 @@ var allowedUpdateFields = map[string]bool{
|
||||
"external_ref": true,
|
||||
"closed_at": true,
|
||||
// Messaging fields (bd-kwro)
|
||||
"sender": true,
|
||||
"ephemeral": true,
|
||||
"replies_to": true,
|
||||
"relates_to": true,
|
||||
"duplicate_of": true,
|
||||
"superseded_by": true,
|
||||
"sender": true,
|
||||
"ephemeral": true,
|
||||
// NOTE: replies_to, relates_to, duplicate_of, superseded_by removed per Decision 004
|
||||
// Use AddDependency() to create graph edges instead
|
||||
}
|
||||
|
||||
// validatePriority validates a priority value
|
||||
@@ -923,10 +748,7 @@ func (s *SQLiteStorage) UpdateIssue(ctx context.Context, id string, updates map[
|
||||
return fmt.Errorf("failed to record event: %w", err)
|
||||
}
|
||||
|
||||
// Create graph edges for messaging/graph fields (Phase 2: dual-write - Decision 004)
|
||||
if err := createGraphEdgesFromUpdates(ctx, tx, id, updates, actor); err != nil {
|
||||
return fmt.Errorf("failed to create graph edges from updates: %w", err)
|
||||
}
|
||||
// NOTE: Graph edges now managed via AddDependency() per Decision 004 Phase 4.
|
||||
|
||||
// Mark issue as dirty for incremental export
|
||||
_, err = tx.ExecContext(ctx, `
|
||||
@@ -1732,7 +1554,7 @@ func (s *SQLiteStorage) SearchIssues(ctx context.Context, query string, filter t
|
||||
status, priority, issue_type, assignee, estimated_minutes,
|
||||
created_at, updated_at, closed_at, external_ref, source_repo, close_reason,
|
||||
deleted_at, deleted_by, delete_reason, original_type,
|
||||
sender, ephemeral, replies_to, relates_to, duplicate_of, superseded_by
|
||||
sender, ephemeral
|
||||
FROM issues
|
||||
%s
|
||||
ORDER BY priority ASC, created_at DESC
|
||||
|
||||
@@ -101,7 +101,7 @@ func (s *SQLiteStorage) GetReadyWork(ctx context.Context, filter types.WorkFilte
|
||||
i.status, i.priority, i.issue_type, i.assignee, i.estimated_minutes,
|
||||
i.created_at, i.updated_at, i.closed_at, i.external_ref, i.source_repo, i.close_reason,
|
||||
i.deleted_at, i.deleted_by, i.delete_reason, i.original_type,
|
||||
i.sender, i.ephemeral, i.replies_to, i.relates_to, i.duplicate_of, i.superseded_by
|
||||
i.sender, i.ephemeral
|
||||
FROM issues i
|
||||
WHERE %s
|
||||
AND NOT EXISTS (
|
||||
@@ -130,7 +130,7 @@ func (s *SQLiteStorage) GetStaleIssues(ctx context.Context, filter types.StaleFi
|
||||
created_at, updated_at, closed_at, external_ref, source_repo,
|
||||
compaction_level, compacted_at, compacted_at_commit, original_size, close_reason,
|
||||
deleted_at, deleted_by, delete_reason, original_type,
|
||||
sender, ephemeral, replies_to, relates_to, duplicate_of, superseded_by
|
||||
sender, ephemeral
|
||||
FROM issues
|
||||
WHERE status != 'closed'
|
||||
AND datetime(updated_at) < datetime('now', '-' || ? || ' days')
|
||||
@@ -179,10 +179,6 @@ func (s *SQLiteStorage) GetStaleIssues(ctx context.Context, filter types.StaleFi
|
||||
// Messaging fields (bd-kwro)
|
||||
var sender sql.NullString
|
||||
var ephemeral sql.NullInt64
|
||||
var repliesTo sql.NullString
|
||||
var relatesTo sql.NullString
|
||||
var duplicateOf sql.NullString
|
||||
var supersededBy sql.NullString
|
||||
|
||||
err := rows.Scan(
|
||||
&issue.ID, &contentHash, &issue.Title, &issue.Description, &issue.Design,
|
||||
@@ -191,7 +187,7 @@ func (s *SQLiteStorage) GetStaleIssues(ctx context.Context, filter types.StaleFi
|
||||
&issue.CreatedAt, &issue.UpdatedAt, &closedAt, &externalRef, &sourceRepo,
|
||||
&compactionLevel, &compactedAt, &compactedAtCommit, &originalSize, &closeReason,
|
||||
&deletedAt, &deletedBy, &deleteReason, &originalType,
|
||||
&sender, &ephemeral, &repliesTo, &relatesTo, &duplicateOf, &supersededBy,
|
||||
&sender, &ephemeral,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to scan stale issue: %w", err)
|
||||
@@ -248,18 +244,6 @@ func (s *SQLiteStorage) GetStaleIssues(ctx context.Context, filter types.StaleFi
|
||||
if ephemeral.Valid && ephemeral.Int64 != 0 {
|
||||
issue.Ephemeral = true
|
||||
}
|
||||
if repliesTo.Valid {
|
||||
issue.RepliesTo = repliesTo.String
|
||||
}
|
||||
if relatesTo.Valid && relatesTo.String != "" {
|
||||
issue.RelatesTo = parseJSONStringArray(relatesTo.String)
|
||||
}
|
||||
if duplicateOf.Valid {
|
||||
issue.DuplicateOf = duplicateOf.String
|
||||
}
|
||||
if supersededBy.Valid {
|
||||
issue.SupersededBy = supersededBy.String
|
||||
}
|
||||
|
||||
issues = append(issues, &issue)
|
||||
}
|
||||
|
||||
@@ -30,10 +30,8 @@ CREATE TABLE IF NOT EXISTS issues (
|
||||
-- Messaging fields (bd-kwro)
|
||||
sender TEXT DEFAULT '',
|
||||
ephemeral INTEGER DEFAULT 0,
|
||||
replies_to TEXT DEFAULT '',
|
||||
relates_to TEXT DEFAULT '',
|
||||
duplicate_of TEXT DEFAULT '',
|
||||
superseded_by TEXT DEFAULT '',
|
||||
-- NOTE: replies_to, relates_to, duplicate_of, superseded_by removed per Decision 004
|
||||
-- These relationships are now stored in the dependencies table
|
||||
CHECK ((status = 'closed') = (closed_at IS NOT NULL))
|
||||
);
|
||||
|
||||
|
||||
@@ -306,7 +306,7 @@ func (t *sqliteTxStorage) GetIssue(ctx context.Context, id string) (*types.Issue
|
||||
created_at, updated_at, closed_at, external_ref,
|
||||
compaction_level, compacted_at, compacted_at_commit, original_size, source_repo, close_reason,
|
||||
deleted_at, deleted_by, delete_reason, original_type,
|
||||
sender, ephemeral, replies_to, relates_to, duplicate_of, superseded_by
|
||||
sender, ephemeral
|
||||
FROM issues
|
||||
WHERE id = ?
|
||||
`, id)
|
||||
@@ -1095,7 +1095,7 @@ func (t *sqliteTxStorage) SearchIssues(ctx context.Context, query string, filter
|
||||
created_at, updated_at, closed_at, external_ref,
|
||||
compaction_level, compacted_at, compacted_at_commit, original_size, source_repo, close_reason,
|
||||
deleted_at, deleted_by, delete_reason, original_type,
|
||||
sender, ephemeral, replies_to, relates_to, duplicate_of, superseded_by
|
||||
sender, ephemeral
|
||||
FROM issues
|
||||
%s
|
||||
ORDER BY priority ASC, created_at DESC
|
||||
@@ -1138,10 +1138,6 @@ func scanIssueRow(row scanner) (*types.Issue, error) {
|
||||
// Messaging fields (bd-kwro)
|
||||
var sender sql.NullString
|
||||
var ephemeral sql.NullInt64
|
||||
var repliesTo sql.NullString
|
||||
var relatesTo sql.NullString
|
||||
var duplicateOf sql.NullString
|
||||
var supersededBy sql.NullString
|
||||
|
||||
err := row.Scan(
|
||||
&issue.ID, &contentHash, &issue.Title, &issue.Description, &issue.Design,
|
||||
@@ -1150,7 +1146,7 @@ func scanIssueRow(row scanner) (*types.Issue, error) {
|
||||
&issue.CreatedAt, &issue.UpdatedAt, &closedAt, &externalRef,
|
||||
&issue.CompactionLevel, &compactedAt, &compactedAtCommit, &originalSize, &sourceRepo, &closeReason,
|
||||
&deletedAt, &deletedBy, &deleteReason, &originalType,
|
||||
&sender, &ephemeral, &repliesTo, &relatesTo, &duplicateOf, &supersededBy,
|
||||
&sender, &ephemeral,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to scan issue: %w", err)
|
||||
@@ -1204,18 +1200,6 @@ func scanIssueRow(row scanner) (*types.Issue, error) {
|
||||
if ephemeral.Valid && ephemeral.Int64 != 0 {
|
||||
issue.Ephemeral = true
|
||||
}
|
||||
if repliesTo.Valid {
|
||||
issue.RepliesTo = repliesTo.String
|
||||
}
|
||||
if relatesTo.Valid && relatesTo.String != "" {
|
||||
issue.RelatesTo = parseJSONStringArray(relatesTo.String)
|
||||
}
|
||||
if duplicateOf.Valid {
|
||||
issue.DuplicateOf = duplicateOf.String
|
||||
}
|
||||
if supersededBy.Valid {
|
||||
issue.SupersededBy = supersededBy.String
|
||||
}
|
||||
|
||||
return &issue, nil
|
||||
}
|
||||
|
||||
@@ -41,12 +41,10 @@ type Issue struct {
|
||||
OriginalType string `json:"original_type,omitempty"` // Issue type before deletion (for tombstones)
|
||||
|
||||
// Messaging fields (bd-kwro): inter-agent communication support
|
||||
Sender string `json:"sender,omitempty"` // Who sent this (for messages)
|
||||
Ephemeral bool `json:"ephemeral,omitempty"` // Can be bulk-deleted when closed
|
||||
RepliesTo string `json:"replies_to,omitempty"` // Issue ID for conversation threading
|
||||
RelatesTo []string `json:"relates_to,omitempty"` // Issue IDs for knowledge graph edges
|
||||
DuplicateOf string `json:"duplicate_of,omitempty"` // Canonical issue ID (this is a duplicate)
|
||||
SupersededBy string `json:"superseded_by,omitempty"` // Replacement issue ID (this is obsolete)
|
||||
Sender string `json:"sender,omitempty"` // Who sent this (for messages)
|
||||
Ephemeral bool `json:"ephemeral,omitempty"` // Can be bulk-deleted when closed
|
||||
// NOTE: RepliesTo, RelatesTo, DuplicateOf, SupersededBy moved to dependencies table
|
||||
// per Decision 004 (Edge Schema Consolidation). Use dependency API instead.
|
||||
}
|
||||
|
||||
// ComputeContentHash creates a deterministic hash of the issue's content.
|
||||
|
||||
Reference in New Issue
Block a user