From 83ae1105088dc81df4453e697f7de339bbbdc97b Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Wed, 17 Dec 2025 23:14:05 -0800 Subject: [PATCH] refactor: Remove legacy MCP Agent Mail integration (bd-6gd) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove the external MCP Agent Mail server integration that required running a separate HTTP server and configuring environment variables. The native `bd mail` system (stored as git-synced issues) remains unchanged and is the recommended approach for inter-agent messaging. Files removed: - cmd/bd/message.go - Legacy `bd message` command - integrations/beads-mcp/src/beads_mcp/mail.py, mail_tools.py - lib/beads_mail_adapter.py - Python adapter library - examples/go-agent/ - Agent Mail-focused example - examples/python-agent/agent_with_mail.py, AGENT_MAIL_EXAMPLE.md - docs/AGENT_MAIL*.md, docs/adr/002-agent-mail-integration.md - tests/integration/test_agent_race.py, test_mail_failures.py, etc. - tests/benchmarks/ - Agent Mail benchmarks Updated documentation to remove Agent Mail references while keeping native `bd mail` documentation intact. šŸ¤– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .beads/issues.jsonl | 386 ++++--- @AGENTS.md | 40 + AGENTS.md | 48 +- cmd/bd/message.go | 520 --------- docs/AGENT_MAIL.md | 525 --------- docs/AGENT_MAIL_DEPLOYMENT.md | 643 ----------- docs/AGENT_MAIL_MULTI_WORKSPACE_SETUP.md | 345 ------ docs/AGENT_MAIL_QUICKSTART.md | 477 --------- docs/CLI_REFERENCE.md | 3 +- docs/QUICKSTART.md | 34 - docs/adr/002-agent-mail-integration.md | 228 ---- examples/README.md | 1 - examples/bash-agent/README.md | 26 - examples/bash-agent/agent.sh | 110 +- examples/go-agent/README.md | 97 -- examples/go-agent/go.mod | 7 - examples/go-agent/main.go | 238 ----- examples/python-agent/AGENT_MAIL_EXAMPLE.md | 418 -------- examples/python-agent/README.md | 1 - examples/python-agent/agent.py | 65 +- examples/python-agent/agent_with_mail.py | 288 ----- integrations/beads-mcp/src/beads_mcp/mail.py | 613 ----------- .../beads-mcp/src/beads_mcp/mail_tools.py | 211 ---- .../beads-mcp/src/beads_mcp/models.py | 60 -- integrations/beads-mcp/tests/test_mail.py | 426 -------- lib/README.md | 173 --- lib/beads_mail_adapter.py | 271 ----- lib/test_beads_mail_adapter.py | 999 ------------------ skills/beads/references/CLI_REFERENCE.md | 3 +- tests/benchmarks/README.md | 84 -- tests/benchmarks/git_traffic.py | 489 --------- tests/benchmarks/git_traffic_50_issues.md | 49 - tests/integration/AGENT_MAIL_TEST_COVERAGE.md | 196 ---- tests/integration/README.md | 90 +- tests/integration/test_agent_race.py | 414 -------- tests/integration/test_mail_failures.py | 797 -------------- .../test_multi_agent_coordination.py | 510 --------- tests/integration/test_reservation_ttl.py | 635 ----------- 38 files changed, 267 insertions(+), 10253 deletions(-) create mode 100644 @AGENTS.md delete mode 100644 cmd/bd/message.go delete mode 100644 docs/AGENT_MAIL.md delete mode 100644 docs/AGENT_MAIL_DEPLOYMENT.md delete mode 100644 docs/AGENT_MAIL_MULTI_WORKSPACE_SETUP.md delete mode 100644 docs/AGENT_MAIL_QUICKSTART.md delete mode 100644 docs/adr/002-agent-mail-integration.md delete mode 100644 examples/go-agent/README.md delete mode 100644 examples/go-agent/go.mod delete mode 100644 examples/go-agent/main.go delete mode 100644 examples/python-agent/AGENT_MAIL_EXAMPLE.md delete mode 100755 examples/python-agent/agent_with_mail.py delete mode 100644 integrations/beads-mcp/src/beads_mcp/mail.py delete mode 100644 integrations/beads-mcp/src/beads_mcp/mail_tools.py delete mode 100644 integrations/beads-mcp/tests/test_mail.py delete mode 100644 lib/README.md delete mode 100644 lib/beads_mail_adapter.py delete mode 100644 lib/test_beads_mail_adapter.py delete mode 100644 tests/benchmarks/README.md delete mode 100755 tests/benchmarks/git_traffic.py delete mode 100644 tests/benchmarks/git_traffic_50_issues.md delete mode 100644 tests/integration/AGENT_MAIL_TEST_COVERAGE.md delete mode 100755 tests/integration/test_agent_race.py delete mode 100644 tests/integration/test_mail_failures.py delete mode 100755 tests/integration/test_multi_agent_coordination.py delete mode 100755 tests/integration/test_reservation_ttl.py diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index f305b2cc..c5c0d3e2 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -1,182 +1,220 @@ -{"id":"bd-3sz0","title":"Auto-repair stale merge driver configs with invalid placeholders","description":"Old bd versions (\u003c0.24.0) installed merge driver with invalid placeholders %L %R instead of %A %B. Add detection to bd doctor --fix: check if git config merge.beads.driver contains %L or %R, auto-repair to 'bd merge %A %O %A %B'. One-time migration for users who initialized with old versions.","status":"open","priority":2,"issue_type":"feature","created_at":"2025-11-21T23:16:10.762808-08:00","updated_at":"2025-11-21T23:16:28.892655-08:00","dependencies":[{"issue_id":"bd-3sz0","depends_on_id":"bd-tbz3","type":"parent-child","created_at":"2025-11-21T23:16:10.763612-08:00","created_by":"daemon"}]} -{"id":"bd-au0.10","title":"Add global verbosity flags (--verbose, --quiet)","description":"Add consistent verbosity controls across all commands.\n\n**Current state:**\n- bd init has --quiet flag\n- No other commands have verbosity controls\n- Debug output controlled by BD_VERBOSE env var\n\n**Proposal:**\nAdd persistent flags:\n- --verbose / -v: Enable debug output\n- --quiet / -q: Suppress non-essential output\n\n**Implementation:**\n- Add to rootCmd.PersistentFlags()\n- Replace BD_VERBOSE checks with flag checks\n- Standardize output levels:\n * Quiet: Errors only\n * Normal: Errors + success messages\n * Verbose: Errors + success + debug info\n\n**Files to modify:**\n- cmd/bd/main.go (add flags)\n- internal/debug/debug.go (respect flags)\n- Update all commands to respect quiet mode\n\n**Testing:**\n- Verify --verbose shows debug output\n- Verify --quiet suppresses normal output\n- Ensure errors always show regardless of mode","status":"open","priority":3,"issue_type":"task","created_at":"2025-11-21T21:08:21.600209-05:00","updated_at":"2025-11-21T21:08:21.600209-05:00","dependencies":[{"issue_id":"bd-au0.10","depends_on_id":"bd-au0","type":"parent-child","created_at":"2025-11-21T21:08:21.602557-05:00","created_by":"daemon"}]} -{"id":"bd-hlsw","title":"Add sync resilience guardrails for forced pushes and prefix mismatches","description":"Beads can get into unrecoverable sync states when remote forces pushes occur (e.g., rebases) combined with prefix mismatches from multi-worker scenarios. Add detection, prevention, and auto-recovery features to handle this gracefully.","status":"open","priority":2,"issue_type":"epic","created_at":"2025-12-14T10:40:14.872875259-07:00","updated_at":"2025-12-14T10:40:14.872875259-07:00"} -{"id":"bd-581b80b3","title":"bd find-duplicates - AI-powered duplicate detection","description":"Find semantically duplicate issues.\n\nApproaches:\n1. Mechanical: Exact title/description matching\n2. Embeddings: Cosine similarity (cheap, scalable)\n3. AI: LLM-based semantic comparison (expensive, accurate)\n\nUses embeddings by default for \u003e100 issues.\n\nFiles: cmd/bd/find_duplicates.go (new)","status":"open","priority":1,"issue_type":"task","created_at":"2025-10-29T20:49:49.126801-07:00","updated_at":"2025-10-30T17:12:58.218673-07:00"} -{"id":"bd-z8a6","title":"bd delete --from-file should add deleted issues to deletions manifest","description":"When using bd delete --from-file to bulk delete issues, the deleted issue IDs are not being added to the deletions.jsonl manifest.\n\nThis causes those issues to be resurrected during bd sync when git history scanning finds them in old commits.\n\nExpected: All deleted issues should be added to deletions.jsonl so they wont be reimported from git history.\n\nWorkaround: Manually add deletion records to deletions.jsonl.","status":"tombstone","priority":1,"issue_type":"bug","created_at":"2025-12-16T01:48:14.099855-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-n4td","title":"Add warning when staleness check errors","description":"## Problem\n\nWhen ensureDatabaseFresh() calls CheckStaleness() and it errors (corrupted metadata, permission issues, etc.), we silently proceed with potentially stale data.\n\n**Location:** cmd/bd/staleness.go:27-32\n\n**Scenarios:**\n- Corrupted metadata table\n- Database locked by another process \n- Permission issues reading JSONL file\n- Invalid last_import_time format in DB\n\n## Current Code\n\n```go\nisStale, err := autoimport.CheckStaleness(ctx, store, dbPath)\nif err \\!= nil {\n // If we can't determine staleness, allow operation to proceed\n // (better to show potentially stale data than block user)\n return nil\n}\n```\n\n## Fix\n\n```go\nisStale, err := autoimport.CheckStaleness(ctx, store, dbPath)\nif err \\!= nil {\n fmt.Fprintf(os.Stderr, \"Warning: Could not verify database freshness: %v\\n\", err)\n fmt.Fprintf(os.Stderr, \"Proceeding anyway. Data may be stale.\\n\\n\")\n return nil\n}\n```\n\n## Impact\nMedium - users should know when staleness check fails\n\n## Effort\nEasy - 5 minutes","status":"open","priority":2,"issue_type":"bug","created_at":"2025-11-20T20:16:34.889997-05:00","updated_at":"2025-11-20T20:16:34.889997-05:00","dependencies":[{"issue_id":"bd-n4td","depends_on_id":"bd-2q6d","type":"blocks","created_at":"2025-11-20T20:18:20.154723-05:00","created_by":"stevey"}]} -{"id":"bd-4ri","title":"Fix TestFallbackToDirectModeEnablesFlush deadlock causing 10min test timeout","description":"## Problem\n\nTestFallbackToDirectModeEnablesFlush in direct_mode_test.go deadlocks for 9m59s before timing out, causing the entire test suite to take 10+ minutes instead of \u003c10 seconds.\n\n## Root Cause\n\nDatabase lock contention between test cleanup and flushToJSONL():\n- Test cleanup (line 36) tries to close DB via defer\n- flushToJSONL() (line 132) is still accessing DB\n- Results in deadlock: database/sql.(*DB).Close() waits for mutex while GetJSONLFileHash() holds it\n\n## Stack Trace Evidence\n\n```\ngoroutine 512 [sync.Mutex.Lock, 9 minutes]:\ndatabase/sql.(*DB).Close(0x14000643790)\n .../database/sql/sql.go:927 +0x84\ngithub.com/steveyegge/beads/cmd/bd.TestFallbackToDirectModeEnablesFlush.func1()\n .../direct_mode_test.go:36 +0xf4\n\nWhile goroutine running flushToJSONL() holds DB connection via GetJSONLFileHash()\n```\n\n## Impact\n\n- Test suite: 10+ minutes → should be \u003c10 seconds\n- ALL other tests pass in ~4 seconds\n- This ONE test accounts for 99.9% of test runtime\n\n## Related\n\nThis is the EXACT same issue documented in MAIN_TEST_REFACTOR_NOTES.md for why main_test.go refactoring was deferred - global state manipulation + DB cleanup = deadlock.\n\n## Fix Approaches\n\n1. **Add proper cleanup sequencing** - stop flush goroutines BEFORE closing DB\n2. **Use test-specific DB lifecycle** - ensure flush completes before cleanup\n3. **Mock the flush mechanism** - avoid real DB for testing this code path \n4. **Add explicit timeout handling** - fail fast with clear error instead of hanging\n\n## Files\n\n- cmd/bd/direct_mode_test.go:36-132\n- cmd/bd/autoflush.go:353 (validateJSONLIntegrity)\n- cmd/bd/autoflush.go:508 (flushToJSONLWithState)\n\n## Acceptance\n\n- Test passes without timeout\n- Test suite completes in \u003c10 seconds\n- No deadlock between cleanup and flush operations","status":"open","priority":1,"issue_type":"bug","created_at":"2025-11-21T20:09:00.794372-05:00","updated_at":"2025-11-21T20:09:00.794372-05:00"} -{"id":"bd-4uoc","title":"Code Review Followup Summary: PR #481 + PR #551","description":"## Merged PRs Summary\n\n### PR #551: Persist close_reason to issues table\n- āœ… Merged successfully\n- āœ… Bug fix: close_reason now persisted in database column (not just events table)\n- āœ… Comprehensive test coverage added\n- āœ… Handles reopen case (clearing close_reason)\n\n**Followup Issues Filed:**\n- bd-lxzx: Document close_reason in JSONL export format\n- bd-077e: Update CLI documentation for close_reason field\n\n---\n\n### PR #481: Context Engineering Optimizations (80-90% context reduction)\n- āœ… Merged successfully \n- āœ… Lazy tool discovery: discover_tools() + get_tool_info()\n- āœ… Minimal issue models: IssueMinimal (~80% smaller than full Issue)\n- āœ… Result compaction: Auto-compacts results \u003e20 items\n- āœ… All 28 tests passing\n- āš ļø Breaking change: ready() and list() return type changed\n\n**Followup Issues Filed:**\n- bd-b318: Add integration tests for CompactedResult\n- bd-4u2b: Make compaction settings configurable (THRESHOLD, PREVIEW_COUNT)\n- bd-2kf8: Document CompactedResult response format in CONTEXT_ENGINEERING.md\n- bd-pdr2: Document backwards compatibility considerations\n\n---\n\n## Overall Assessment\n\nBoth PRs are production-ready with solid implementations. All critical functionality works and tests pass. Followup issues focus on:\n1. Documentation improvements (5 issues)\n2. Integration test coverage (1 issue)\n3. Configuration flexibility (1 issue)\n4. Backwards compatibility guidance (1 issue)\n\nNo critical bugs or design issues found.\n\n## Review Completed By\nCode review process completed. Issues auto-created for tracking improvements.","status":"open","priority":2,"issue_type":"task","created_at":"2025-12-14T14:25:59.214886-08:00","updated_at":"2025-12-14T14:25:59.214886-08:00","dependencies":[{"issue_id":"bd-4uoc","depends_on_id":"bd-otf4","type":"discovered-from","created_at":"2025-12-14T14:25:59.216884-08:00","created_by":"stevey"},{"issue_id":"bd-4uoc","depends_on_id":"bd-z86n","type":"discovered-from","created_at":"2025-12-14T14:25:59.217296-08:00","created_by":"stevey"}]} -{"id":"bd-8fgn","title":"test hash length","status":"tombstone","priority":2,"issue_type":"task","created_at":"2025-12-16T13:49:32.113843-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-nuh1","title":"GH#403: bd doctor --fix circular error message","description":"bd doctor --fix suggests running bd doctor --fix for deletions manifest issue. Fix to provide actual resolution. See GitHub issue #403.","status":"tombstone","priority":2,"issue_type":"bug","created_at":"2025-12-16T01:03:16.290018-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-7di","title":"worktree: any bd command is slow","description":"in a git worktree any bd command is slow, with a 2-3s pause before any results are shown. The identical command with `--no-daemon` is near instant.","status":"open","priority":2,"issue_type":"task","created_at":"2025-12-05T15:33:42.924618693-07:00","updated_at":"2025-12-05T15:33:42.924618693-07:00"} -{"id":"bd-e1085716","title":"bd validate - Comprehensive health check","description":"Run all validation checks in one command.\n\nChecks:\n- Duplicates\n- Orphaned dependencies\n- Test pollution\n- Git conflicts\n\nSupports --fix-all for auto-repair.\n\nDepends on bd-cbed9619.1, bd-0dcea000, bd-31aab707, bd-9826b69a.\n\nFiles: cmd/bd/validate.go (new)","status":"open","priority":1,"issue_type":"task","created_at":"2025-10-29T23:05:13.980679-07:00","updated_at":"2025-10-30T17:12:58.19736-07:00"} -{"id":"bd-au0.8","title":"Improve clean vs cleanup command naming/documentation","description":"Clarify the difference between bd clean and bd cleanup to reduce user confusion.\n\n**Current state:**\n- bd clean: Remove temporary artifacts (.beads/bd.sock, logs, etc.)\n- bd cleanup: Delete old closed issues from database\n\n**Options:**\n1. Rename for clarity:\n - bd clean → bd clean-temp\n - bd cleanup → bd cleanup-issues\n \n2. Keep names but improve help text and documentation\n\n3. Add prominent warnings in help output\n\n**Preferred approach:** Option 2 (improve documentation)\n- Update short/long descriptions in commands\n- Add examples to help text\n- Update README.md\n- Add cross-references in help output\n\n**Files to modify:**\n- cmd/bd/clean.go\n- cmd/bd/cleanup.go\n- README.md or ADVANCED.md","status":"open","priority":2,"issue_type":"task","created_at":"2025-11-21T21:07:49.960534-05:00","updated_at":"2025-11-21T21:07:49.960534-05:00","dependencies":[{"issue_id":"bd-au0.8","depends_on_id":"bd-au0","type":"parent-child","created_at":"2025-11-21T21:07:49.962743-05:00","created_by":"daemon"}]} -{"id":"bd-toy3","title":"Test hook","status":"tombstone","priority":2,"issue_type":"task","created_at":"2025-12-16T18:33:39.717036-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-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-aydr.3","title":"Add git operations for --hard reset","description":"Implement git integration for hard reset mode.\n\n## Operations Needed\n1. `git rm -rf .beads/*.jsonl` - remove data files from index\n2. `git commit -m 'beads: reset to clean state'` - commit removal\n3. After re-init: `git add .beads/` and commit fresh state\n\n## Edge Cases to Handle\n- Uncommitted changes in .beads/ - warn or error\n- Detached HEAD state - warn, maybe block\n- Git not initialized - skip git ops, warn\n- Git operations fail mid-way - clear error messaging\n\n## Interface\n```go\ntype GitState struct {\n IsRepo bool\n IsDirty bool // uncommitted changes in .beads/\n IsDetached bool // detached HEAD\n Branch string // current branch name\n}\n\nfunc CheckGitState(beadsDir string) (*GitState, error)\nfunc GitRemoveBeads(beadsDir string) error\nfunc GitCommitReset(message string) error\nfunc GitAddAndCommit(beadsDir, message string) error\n```\n\n## Location\n`internal/reset/git.go` - keep with reset package for now\n\nNote: Codebase has no central git package. internal/compact/git.go is compact-specific.\nFuture refactoring could extract shared git utilities, but YAGNI for now.","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-13T08:44:52.798312+11:00","updated_at":"2025-12-13T10:13:32.611131+11:00","closed_at":"2025-12-13T09:17:40.785927+11:00","dependencies":[{"issue_id":"bd-aydr.3","depends_on_id":"bd-aydr","type":"parent-child","created_at":"2025-12-13T08:44:52.798715+11:00","created_by":"daemon"}]} -{"id":"bd-8g8","title":"Fix G304 potential file inclusion in cmd/bd/tips.go:259","description":"Linting issue: G304: Potential file inclusion via variable (gosec) at cmd/bd/tips.go:259:18. Error: if data, err := os.ReadFile(settingsPath); err == nil {","status":"open","issue_type":"bug","created_at":"2025-12-07T15:34:57.189730843-07:00","updated_at":"2025-12-07T15:34:57.189730843-07: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","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-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-16T01:00:40.702015-08:00","closed_at":"2025-10-28T14:08:38.061444-07:00"} -{"id":"bd-kwro.9","title":"Cleanup: --ephemeral flag","description":"Update bd cleanup to handle ephemeral issues.\n\nNew flag:\n- bd cleanup --ephemeral - deletes all CLOSED issues with ephemeral=true\n\nBehavior:\n- Only deletes if status=closed AND ephemeral=true\n- Respects --dry-run flag\n- Reports count of deleted ephemeral issues\n\nThis allows swarm cleanup to remove transient messages without affecting permanent issues.","status":"tombstone","priority":2,"issue_type":"task","created_at":"2025-12-16T03:02:28.563871-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-hkr6","title":"GH#518: Document bd setup command","description":"bd setup is undiscoverable. Add to README/docs. Currently only findable by grepping source. See GitHub issue #518.","status":"tombstone","priority":2,"issue_type":"task","created_at":"2025-12-16T01:03:54.664668-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-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-16T01:00:43.240689-08:00","closed_at":"2025-10-28T14:08:38.065118-07: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-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-16T01:17:18.509493-08:00","closed_at":"2025-10-28T14:08:38.060291-07:00"} -{"id":"bd-4qfb","title":"Improve bd doctor output formatting for better readability","description":"The current bd doctor output is a wall of text that's hard to process. Consider improvements like:\n- Grouping related checks into collapsible sections\n- Using color/bold for section headers\n- Showing only failures/warnings by default with --verbose for full output\n- Better visual hierarchy between major sections\n- Summary line at top (e.g., '24 checks passed, 0 warnings, 0 errors')","status":"open","priority":2,"issue_type":"task","created_at":"2025-12-13T09:29:27.557578+11:00","updated_at":"2025-12-13T09:29:27.557578+11:00"} -{"id":"bd-aydr.5","title":"Enhance bd doctor to suggest reset for broken states","description":"Update bd doctor to detect severely broken states and suggest reset.\n\n## Detection Criteria\nSuggest reset when:\n- Multiple unfixable errors detected\n- Corrupted JSONL that can't be repaired\n- Schema version mismatch that can't be migrated\n- Daemon state inconsistent and unkillable\n\n## Implementation\nAdd to doctor's check/fix flow:\n```go\nif unfixableErrors \u003e threshold {\n suggest('State may be too broken to fix. Consider: bd reset')\n}\n```\n\n## Output Example\n```\nāœ— Found 5 unfixable errors\n \n Your beads state may be too corrupted to repair.\n Consider running 'bd reset' to start fresh.\n (Use 'bd reset --backup' to save current state first)\n```\n\n## Notes\n- Don't auto-run reset, just suggest\n- This is lower priority, can be done in parallel with main work","status":"closed","priority":3,"issue_type":"task","created_at":"2025-12-13T08:44:55.591986+11:00","updated_at":"2025-12-13T06:24:29.561624-08:00","closed_at":"2025-12-13T10:17:23.4522+11:00","dependencies":[{"issue_id":"bd-aydr.5","depends_on_id":"bd-aydr","type":"parent-child","created_at":"2025-12-13T08:44:55.59239+11:00","created_by":"daemon"}]} +{"id":"bd-05a8","title":"Split large cmd/bd files: doctor.go (2948 lines), sync.go (2121 lines)","description":"Code health review found several oversized files:\n\n1. doctor.go - 2948 lines, 48 functions mixed together\n - Should split into doctor/checks/*.go for individual diagnostics\n - applyFixes() and previewFixes() are nearly identical\n\n2. sync.go - 2121 lines\n - ZFC (Zero Flush Check) logic embedded inline (lines 213-247)\n - Multiple mode handlers should be extracted\n\n3. init.go - 1732 lines\n4. compact.go - 1097 lines\n5. show.go - 1069 lines\n\nRecommendation: Extract into focused sub-packages or split into logical files.","status":"open","priority":2,"issue_type":"task","created_at":"2025-12-16T18:17:18.169927-08:00","updated_at":"2025-12-16T18:17:18.169927-08:00","dependencies":[{"issue_id":"bd-05a8","depends_on_id":"bd-tggf","type":"blocks","created_at":"2025-12-16T18:19:05.846503-08:00","created_by":"daemon"}]} +{"id":"bd-06px","title":"bd sync --from-main fails: unknown flag --no-git-history","description":"","status":"closed","priority":1,"issue_type":"bug","created_at":"2025-12-17T14:32:02.998106-08:00","updated_at":"2025-12-17T23:13:40.531756-08:00","closed_at":"2025-12-17T17:21:48.506039-08:00"} {"id":"bd-077e","title":"Add close_reason field to CLI schema and documentation","description":"PR #551 persists close_reason, but the CLI documentation may not mention this field as part of the issue schema.\n\n## Current State\n- close_reason is now persisted in database\n- `bd show --json` will return close_reason in JSON output\n- Documentation may not reflect this new field\n\n## What's Missing\n- CLI reference documentation for close_reason field\n- Schema documentation showing close_reason is a top-level issue field\n- Example output showing close_reason in bd show --json\n- bd close command documentation should mention close_reason parameter is optional\n\n## Suggested Action\n1. Update README.md or CLI reference docs to list close_reason as an issue field\n2. Add example to bd close documentation\n3. Update any type definitions or schema specs\n4. Consider adding close_reason to verbose list output (bd list --verbose)","status":"open","priority":3,"issue_type":"task","created_at":"2025-12-14T14:25:28.448654-08:00","updated_at":"2025-12-14T14:25:28.448654-08:00","dependencies":[{"issue_id":"bd-077e","depends_on_id":"bd-z86n","type":"discovered-from","created_at":"2025-12-14T14:25:28.449968-08:00","created_by":"stevey"}]} -{"id":"bd-thgk","title":"Improve test coverage for internal/compact (18.2% → 70%)","description":"The compact package has only 18.2% test coverage. This is a core package handling issue compaction and should have at least 70% coverage.\n\nKey functions needing tests:\n- runCompactSingle (0%)\n- runCompactAll (0%)\n- runCompactRPC (0%)\n- runCompactStatsRPC (0%)\n- runCompactAnalyze (0%)\n- runCompactApply (0%)\n- pruneDeletionsManifest (0%)\n\nCurrent coverage: 18.2%\nTarget coverage: 70%","status":"open","priority":1,"issue_type":"task","created_at":"2025-12-13T20:42:58.455767-08:00","updated_at":"2025-12-13T21:01:15.070551-08:00"} -{"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-16T01:00:40.546745-08:00","closed_at":"2025-10-28T14:08:38.064592-07:00"} -{"id":"bd-nl2","title":"No logging/debugging for tombstone resurrection events","description":"Per the design document bd-zvg Open Question 1: Should resurrection log a warning? Recommendation was Yes. Currently, when an expired tombstone loses to a live issue (resurrection), there is no logging or debugging output. This makes it hard to understand why an issue reappeared. Recommendation: Add optional debug logging when resurrection occurs, e.g., Issue bd-abc resurrected (tombstone expired). Files: internal/merge/merge.go:359-366, 371-378, 400-405, 410-415","status":"open","priority":4,"issue_type":"feature","created_at":"2025-12-05T16:36:52.27525-08:00","updated_at":"2025-12-05T16:36:52.27525-08:00"} -{"id":"bd-mql4","title":"getLocalSyncBranch silently ignores YAML parse errors","description":"In autoimport.go:170-172, YAML parsing errors are silently ignored. If a user has malformed YAML in config.yaml, sync-branch will just silently be empty with no feedback.\n\nRecommendation: Add debug logging since this function is only called during auto-import, and debugging silent failures is painful.\n\nAdd: debug.Logf(\"Warning: failed to parse config.yaml: %v\", err)","status":"open","priority":4,"issue_type":"task","created_at":"2025-12-07T02:03:44.217728-08:00","updated_at":"2025-12-07T02:03:44.217728-08:00"} -{"id":"bd-a15d","title":"Add test files for internal/storage","description":"The internal/storage package has no test files at all. This package provides the storage interface abstraction.\n\nCurrent coverage: N/A (no test files)\nTarget: Add basic interface tests","status":"open","priority":2,"issue_type":"task","created_at":"2025-12-13T20:43:11.363017-08:00","updated_at":"2025-12-13T21:01:20.925779-08:00"} -{"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":"open","issue_type":"bug","created_at":"2025-12-07T15:35:13.051671889-07:00","updated_at":"2025-12-07T15:35:13.051671889-07:00"} -{"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","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-dsdh","title":"Document sync.branch 'always dirty' working tree behavior","description":"## Context\n\nWhen sync.branch is configured, the .beads/issues.jsonl file in main's working tree is ALWAYS dirty. This is by design:\n\n1. bd sync commits to beads-sync branch (via worktree)\n2. bd sync copies JSONL to main's working tree (so CLI commands work)\n3. This copy is NOT committed to main (to reduce commit noise)\n\nContributors who watch main branch history pushed for sync.branch to avoid constant beads commit noise. But users need to understand the trade-off.\n\n## Documentation Needed\n\nUpdate README.md sync.branch section with:\n\n1. **Clear explanation** of why .beads/ is always dirty on main\n2. **\"Be Zen about it\"** - this is expected, not a bug\n3. **Workflow options:**\n - Accept dirty state, use `bd sync --merge` periodically to snapshot to main\n - Or disable sync.branch if clean working tree is more important\n4. **Shell alias tip** to hide beads from git status:\n ```bash\n alias gs='git status -- \":!.beads/\"'\n ```\n5. **When to merge**: releases, milestones, or periodic snapshots\n\n## Related\n\n- bd-7b7h: Fix that allows bd sync --merge to work with dirty .beads/\n- bd-elqd: Investigation that identified this as expected behavior","status":"tombstone","priority":2,"issue_type":"task","created_at":"2025-12-16T23:16:12.253559-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-de6","title":"Fix FindBeadsDir to prioritize main repo .beads for worktrees","description":"The FindBeadsDir function should prioritize finding .beads in the main repository root when accessed from a worktree, rather than finding worktree-local .beads directories. This ensures proper sharing of the database across all worktrees.","status":"open","priority":2,"issue_type":"bug","created_at":"2025-12-07T16:48:36.883117467-07:00","updated_at":"2025-12-07T16:48:36.883117467-07:00"} -{"id":"bd-6fe4622f","title":"Remove unreachable utility functions","description":"Several small utility functions are unreachable:\n\nFiles to clean:\n1. `internal/storage/sqlite/hash.go` - `computeIssueContentHash` (line 17)\n - Check if entire file can be deleted if only contains this function\n\n2. `internal/config/config.go` - `FileUsed` (line 151)\n - Delete unused config helper\n\n3. `cmd/bd/git_sync_test.go` - `verifyIssueOpen` (line 300)\n - Delete dead test helper\n\n4. `internal/compact/haiku.go` - `HaikuClient.SummarizeTier2` (line 81)\n - Tier 2 summarization not implemented\n - Options: implement feature OR delete method\n\nImpact: Removes 50-100 LOC depending on decisions","status":"open","priority":2,"issue_type":"task","created_at":"2025-10-28T16:20:02.434573-07:00","updated_at":"2025-10-30T17:12:58.224957-07:00"} -{"id":"bd-dhza","title":"Reduce global state in cmd/bd/main.go (25+ variables)","description":"Code health review found main.go has 25+ global variables (lines 57-112):\n\n- dbPath, actor, store, jsonOutput, daemonClient, noDaemon\n- rootCtx, rootCancel, autoFlushEnabled\n- isDirty (marked 'USED BY LEGACY CODE')\n- needsFullExport (marked 'USED BY LEGACY CODE')\n- flushTimer (marked 'DEPRECATED')\n- flushMutex, storeMutex, storeActive\n- flushFailureCount, lastFlushError, flushManager\n- skipFinalFlush, autoImportEnabled\n- versionUpgradeDetected, previousVersion, upgradeAcknowledged\n\nImpact:\n- Hard to test individual commands\n- Race conditions possible\n- State leakage between commands\n\nFix: Move toward dependency injection. Remove deprecated variables. Consider cmd/bd/internal package.","status":"open","priority":3,"issue_type":"task","created_at":"2025-12-16T18:17:29.643293-08:00","updated_at":"2025-12-16T18:17:29.643293-08:00","dependencies":[{"issue_id":"bd-dhza","depends_on_id":"bd-tggf","type":"blocks","created_at":"2025-12-16T18:19:06.492112-08:00","created_by":"daemon"}]} -{"id":"bd-fx7v","title":"Improve test coverage for cmd/bd/doctor/fix (23.9% → 50%)","description":"The doctor/fix package has only 23.9% test coverage. The doctor fix functionality is important for troubleshooting.\n\nCurrent coverage: 23.9%\nTarget coverage: 50%","status":"open","priority":2,"issue_type":"task","created_at":"2025-12-13T20:43:05.67127-08:00","updated_at":"2025-12-13T21:01:20.839298-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":"open","issue_type":"task","created_at":"2025-11-21T23:58:35.044762-08:00","updated_at":"2025-11-21T23:58:35.044762-08:00"} -{"id":"bd-9usz","title":"Test suite hangs/never finishes","description":"Running 'go test ./... -count=1' hangs indefinitely. The full test suite never completes, making it difficult to verify changes. Need to investigate which tests are hanging and fix or add timeouts.","status":"open","priority":2,"issue_type":"bug","created_at":"2025-12-16T21:56:27.80191-08:00","updated_at":"2025-12-16T21:56:27.80191-08:00"} -{"id":"bd-7yg","title":"Git merge driver uses invalid placeholders (%L, %R instead of %A, %B)","description":"## Problem\n\nThe beads git merge driver is configured with invalid Git placeholders:\n\n```\ngit config merge.beads.driver \"bd merge %A %O %L %R\"\n```\n\nGit doesn't recognize `%L` or `%R` as valid merge driver placeholders. The valid placeholders are:\n- `%O` = base (common ancestor)\n- `%A` = current version (ours)\n- `%B` = other version (theirs)\n\n## Impact\n\n- Affects ALL users when they have `.beads/beads.jsonl` merge conflicts\n- Automatic JSONL merge fails with error: \"error reading left file: failed to open file: open 7: no such file or directory\"\n- Users must manually resolve conflicts instead of getting automatic merge\n\n## Root Cause\n\nThe `bd init` command (or wherever the merge driver is configured) is using non-standard placeholders. When Git encounters `%L` and `%R`, it either passes them literally or interprets them incorrectly.\n\n## Fix\n\nUpdate the merge driver configuration to:\n```\ngit config merge.beads.driver \"bd merge %A %O %A %B\"\n```\n\nWhere:\n- 1st `%A` = output file (current file, will be overwritten)\n- `%O` = base (common ancestor)\n- 2nd `%A` = left/current version\n- `%B` = right/other version\n\n## Action Items\n\n1. Fix `bd init` (or equivalent setup command) to use correct placeholders\n2. Add migration/warning for existing users with misconfigured merge driver\n3. Update documentation with correct merge driver setup\n4. Consider adding validation when `bd init` is run","status":"open","priority":1,"issue_type":"bug","created_at":"2025-11-21T19:51:55.747608-05:00","updated_at":"2025-11-21T19:51:55.747608-05:00"} -{"id":"bd-xj2e","title":"GH#522: Add --type flag to bd update command","description":"Add --type flag to bd update for changing issue type (task/epic/bug/feature). Storage layer already supports it. See GitHub issue #522.","status":"tombstone","priority":2,"issue_type":"task","created_at":"2025-12-16T01:03:12.506583-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-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-16T01:00:46.990578-08:00","closed_at":"2025-10-29T11:02:40.756891-07:00"} -{"id":"bd-aydr.4","title":"Implement CLI command (cmd/bd/reset.go)","description":"Wire up the reset command with Cobra CLI.\n\n## Responsibilities\n- Define command and all flags\n- User confirmation prompt (unless --force)\n- Display impact summary before confirmation\n- Colored output and progress indicators\n- Call core reset package\n- Handle errors with user-friendly messages\n- Register command with rootCmd in init()\n\n## Flags\n```go\n--hard bool \"Also remove from git and commit\"\n--force bool \"Skip confirmation prompt\"\n--backup bool \"Create backup before reset\"\n--dry-run bool \"Preview what would happen\"\n--skip-init bool \"Do not re-initialize after reset\"\n--verbose bool \"Show detailed progress output\"\n```\n\n## Output Format\n```\nāš ļø This will reset beads to a clean state.\n\nWill be deleted:\n • 47 issues (23 open, 24 closed)\n • 12 tombstones\n\nContinue? [y/N] y\n\n→ Stopping daemons... āœ“\n→ Removing .beads/... āœ“\n→ Initializing fresh... āœ“\n\nāœ“ Reset complete. Run 'bd onboard' to set up hooks.\n```\n\n## Implementation Notes\n- Confirmation logic lives HERE, not in core package\n- Use color package (github.com/fatih/color) for output\n- Follow patterns from other commands (init.go, doctor.go)\n- Add to rootCmd in init() function\n\n## File Location\n`cmd/bd/reset.go`","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-13T08:44:54.318854+11:00","updated_at":"2025-12-13T10:13:32.611434+11:00","closed_at":"2025-12-13T09:59:41.72638+11:00","dependencies":[{"issue_id":"bd-aydr.4","depends_on_id":"bd-aydr","type":"parent-child","created_at":"2025-12-13T08:44:54.319237+11:00","created_by":"daemon"},{"issue_id":"bd-aydr.4","depends_on_id":"bd-aydr.1","type":"blocks","created_at":"2025-12-13T08:45:09.762138+11:00","created_by":"daemon"},{"issue_id":"bd-aydr.4","depends_on_id":"bd-aydr.2","type":"blocks","created_at":"2025-12-13T08:45:09.817854+11:00","created_by":"daemon"},{"issue_id":"bd-aydr.4","depends_on_id":"bd-aydr.3","type":"blocks","created_at":"2025-12-13T08:45:09.883658+11:00","created_by":"daemon"}]} -{"id":"bd-kwro.5","title":"Graph Link: supersedes for version chains","description":"Implement supersedes link type for version tracking.\n\nNew command:\n- bd supersede \u003cid\u003e --with \u003cnew\u003e - marks id as superseded by new\n- Auto-closes the superseded issue\n\nQuery support:\n- bd show \u003cid\u003e shows 'Superseded by: \u003cnew\u003e'\n- bd show \u003cnew\u003e shows 'Supersedes: \u003cid\u003e'\n- bd list --superseded shows version chains\n\nStorage:\n- superseded_by column pointing to replacement issue\n\nUseful for design docs, specs, and evolving artifacts.","status":"tombstone","priority":1,"issue_type":"task","created_at":"2025-12-16T03:01:41.749294-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-d3e5","title":"Test issue 2","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"} -{"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-77gm","title":"Import reports misleading '0 created, 0 updated' when actually importing all issues","description":"When running 'bd import' on a fresh database (no existing issues), the command reports 'Import complete: 0 created, 0 updated' even though it successfully imported all issues from the JSONL file.\n\n**Steps to reproduce:**\n1. Delete .beads/beads.db\n2. Run: bd import .beads/issues.jsonl\n3. Observe output: 'Import complete: 0 created, 0 updated'\n4. Run: bd list\n5. Confirm: All issues are actually present in the database\n\n**Expected behavior:**\nReport the actual number of issues imported, e.g., 'Import complete: 523 created, 0 updated'\n\n**Actual behavior:**\n'Import complete: 0 created, 0 updated' (misleading - makes user think import failed)\n\n**Impact:**\n- Users think import failed when it succeeded\n- Confusing during database sync operations (e.g., after git pull)\n- Makes debugging harder (can't tell if import actually worked)\n\n**Context:**\nDiscovered during VC session when syncing database after git pull. The misleading message caused confusion about whether the database was properly synced with the canonical JSONL file.","status":"open","priority":2,"issue_type":"bug","created_at":"2025-11-09T16:20:13.191156-08:00","updated_at":"2025-11-09T16:20:13.191156-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":"open","priority":1,"issue_type":"bug","created_at":"2025-11-20T19:33:40.019297-05:00","updated_at":"2025-11-22T14:57:44.481917204-05:00"} -{"id":"bd-aydr","title":"Add bd reset command for clean slate restart","description":"Implement a `bd reset` command to reset beads to a clean starting state.\n\n## Context\nGitHub 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## Design\n\n### Command Interface\n```\nbd reset [--hard] [--force] [--backup] [--dry-run] [--no-init]\n```\n\n| Flag | Effect |\n|------|--------|\n| `--hard` | Also remove from git index and commit |\n| `--force` | Skip confirmation prompt |\n| `--backup` | Create `.beads-backup-{timestamp}/` first |\n| `--dry-run` | Preview what would happen |\n| `--no-init` | Don't re-initialize after clearing |\n\n### Reset Levels\n1. **Soft Reset (default)** - Kill daemons, clear .beads/, re-init. Git history unchanged.\n2. **Hard Reset (`--hard`)** - Also git rm and commit the removal, then commit fresh state.\n\n### Implementation Flow\n1. Validate .beads/ exists\n2. If not --force: show impact summary, prompt confirmation\n3. If --backup: copy .beads/ to .beads-backup-{timestamp}/\n4. Kill daemons\n5. If --hard: git rm + commit\n6. rm -rf .beads/*\n7. If not --no-init: bd init (and git add+commit if --hard)\n8. Print summary\n\n### Safety Mechanisms\n- Confirmation prompt (skip with --force)\n- Impact summary (issue/tombstone counts)\n- Backup option\n- Dry-run preview\n- Git dirty check warning\n\n### Code Structure\n- `cmd/bd/reset.go` - CLI command\n- `internal/reset/` - Core logic package","status":"closed","priority":2,"issue_type":"epic","created_at":"2025-12-13T08:44:01.38379+11:00","updated_at":"2025-12-13T06:24:29.561294-08:00","closed_at":"2025-12-13T10:18:19.965287+11:00"} -{"id":"bd-f5cc","title":"Thread Test","description":"Testing the thread feature","status":"tombstone","priority":2,"issue_type":"message","created_at":"2025-12-16T18:21:01.244501-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":"message"} -{"id":"bd-lxzx","title":"Add close_reason to JSONL export format documentation","description":"PR #551 now persists close_reason to the database, but there's a question about whether this field should be exported to JSONL format.\n\n## Current State\n- close_reason is stored in issues.close_reason column\n- close_reason is also stored in events table (audit trail)\n- The JSONL export format may or may not include close_reason\n\n## Questions\n1. Should close_reason be exported to JSONL format?\n2. If yes, where should it go (root level or nested in events)?\n3. Should there be any special handling to avoid duplication?\n4. How should close_reason be handled during JSONL import?\n\n## Why This Matters\n- JSONL is the git-friendly sync format\n- Other beads instances import from JSONL\n- close_reason is meaningful data that should be preserved across clones\n\n## Suggested Action\n- Check if close_reason is currently exported in JSONL\n- If not, add it to the export schema\n- Document the field in JSONL format spec\n- Add tests for round-trip (export -\u003e import -\u003e verify close_reason)","status":"open","priority":2,"issue_type":"task","created_at":"2025-12-14T14:25:17.414916-08:00","updated_at":"2025-12-14T14:25:17.414916-08:00","dependencies":[{"issue_id":"bd-lxzx","depends_on_id":"bd-z86n","type":"discovered-from","created_at":"2025-12-14T14:25:17.416131-08:00","created_by":"stevey"}]} -{"id":"bd-rgyd","title":"Split internal/storage/sqlite/queries.go (1586 lines)","description":"Code health review found queries.go is too large at 1586 lines with:\n\n- Issue scanning logic duplicated between transaction.go and queries.go\n- Timestamp defensive fixes duplicated in CreateIssue, CreateIssues, batch_ops.go\n- Multiple similar patterns that should be consolidated\n\nAlso: transaction.go at 1284 lines, dependencies.go at 893 lines\n\nRecommendation:\n1. Extract common scanning into shared function\n2. Consolidate timestamp defensive fixes\n3. Split queries.go by domain (CRUD, search, batch, events)","status":"open","priority":2,"issue_type":"task","created_at":"2025-12-16T18:17:23.85869-08:00","updated_at":"2025-12-16T18:17:23.85869-08:00","dependencies":[{"issue_id":"bd-rgyd","depends_on_id":"bd-tggf","type":"blocks","created_at":"2025-12-16T18:19:06.062038-08:00","created_by":"daemon"}]} -{"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"} -{"id":"bd-kwro.2","title":"Graph Link: replies_to for conversation threading","description":"Implement replies_to link type for message threading.\n\nNew command:\n- bd mail reply \u003cid\u003e -m 'Response' creates a message with replies_to set\n\nQuery support:\n- bd show \u003cid\u003e --thread shows full conversation thread\n- Thread traversal in storage layer\n\nStorage:\n- replies_to column in issues table\n- Index for efficient thread queries\n\nThis enables Reddit-style nested threads where messages reply to other messages.","status":"tombstone","priority":1,"issue_type":"task","created_at":"2025-12-16T03:01:25.292728-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-aydr.7","title":"Add integration tests for bd reset command","description":"End-to-end integration tests for the reset command.\n\n## Test Scenarios\n\n### Basic reset\n1. Init beads, create some issues\n2. Run bd reset --force\n3. Verify .beads/ is fresh, issues gone\n\n### Hard reset\n1. Init beads, create issues, commit\n2. Run bd reset --hard --force \n3. Verify git history has reset commits\n\n### Backup functionality\n1. Init beads, create issues\n2. Run bd reset --backup --force\n3. Verify backup exists with correct contents\n4. Verify main .beads/ is reset\n\n### Dry run\n1. Init beads, create issues\n2. Run bd reset --dry-run\n3. Verify nothing changed\n\n### Confirmation prompt\n1. Init beads\n2. Run bd reset (no --force)\n3. Verify prompts for confirmation\n4. Test both y and n responses\n\n## Location\ntests/integration/reset_test.go or similar","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-13T08:44:58.479282+11:00","updated_at":"2025-12-13T06:24:29.561908-08:00","closed_at":"2025-12-13T10:15:59.221637+11:00","dependencies":[{"issue_id":"bd-aydr.7","depends_on_id":"bd-aydr","type":"parent-child","created_at":"2025-12-13T08:44:58.479686+11:00","created_by":"daemon"},{"issue_id":"bd-aydr.7","depends_on_id":"bd-aydr.4","type":"blocks","created_at":"2025-12-13T08:45:11.15972+11:00","created_by":"daemon"}]} -{"id":"bd-89f89fc0","title":"Remove unreachable RPC methods","description":"Several RPC server and client methods are unreachable and should be removed:\n\nServer methods (internal/rpc/server.go):\n- `Server.GetLastImportTime` (line 2116)\n- `Server.SetLastImportTime` (line 2123)\n- `Server.findJSONLPath` (line 2255)\n\nClient methods (internal/rpc/client.go):\n- `Client.Import` (line 311) - RPC import not used (daemon uses autoimport)\n\nEvidence:\n```bash\ngo run golang.org/x/tools/cmd/deadcode@latest -test ./...\n```\n\nImpact: Removes ~80 LOC of unused RPC code","status":"open","priority":2,"issue_type":"task","created_at":"2025-10-28T16:20:02.432202-07:00","updated_at":"2025-10-30T17:12:58.222655-07:00"} -{"id":"bd-s2t","title":"wish: a 'continue' or similar cmd/flag which means alter last issue","description":"so many time I create an issue and then have another thought: 'oh, before I did X and it crashed there was ZZZ happening' or 'actually that is P4 not P2'. It would be nice if when `bd {cmd}` is used without a {title} or {id} it just adds or updates the most recently touched issue.","status":"open","priority":3,"issue_type":"task","created_at":"2025-12-08T06:46:37.529160416-07:00","updated_at":"2025-12-08T06:46:37.529160416-07:00"} -{"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-16T01:00:44.239397-08:00","closed_at":"2025-10-28T18:57:10.985198-07:00","dependencies":[{"issue_id":"bd-cbed9619.5","depends_on_id":"bd-325da116","type":"parent-child","created_at":"2025-10-28T18:39:20.547325-07:00","created_by":"daemon"}]} -{"id":"bd-siz1","title":"GH#532: bd sync circular error (suggests running bd sync)","description":"bd sync error message recommends running bd sync to fix the bd sync error. Fix error handling to provide useful guidance. See GitHub issue #532.","status":"tombstone","priority":2,"issue_type":"bug","created_at":"2025-12-16T01:04:00.543573-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-0a43","title":"Split monolithic sqlite.go into focused files","description":"internal/storage/sqlite/sqlite.go is 1050 lines containing initialization, 20+ CRUD methods, query building, and schema management.\n\nSplit into:\n- store.go: Store struct \u0026 initialization (150 lines)\n- bead_queries.go: Bead CRUD (300 lines)\n- work_queries.go: Work queries (200 lines) \n- stats_queries.go: Statistics (150 lines)\n- schema.go: Schema \u0026 migrations (150 lines)\n- helpers.go: Common utilities (100 lines)\n\nImpact: Impossible to understand at a glance; hard to find specific functionality; high cognitive load\n\nEffort: 6-8 hours","status":"closed","priority":0,"issue_type":"task","created_at":"2025-11-16T14:51:16.520465-08:00","updated_at":"2025-12-17T23:13:40.533947-08:00","closed_at":"2025-12-17T16:51:30.236012-08:00"} +{"id":"bd-0d5p","title":"Fix TestRunSync_Timeout failing on macOS","description":"The hooks timeout test fails because exec.CommandContext doesn't properly terminate child processes of shell scripts on macOS. The test creates a hook that runs 'sleep 60' with a 500ms timeout, but it waits the full 60 seconds.\n\nOptions to fix:\n- Use SysProcAttr{Setpgid: true} to create process group and kill the group\n- Skip test on darwin with build tag\n- Use a different approach for timeout testing\n\nLocation: internal/hooks/hooks_test.go:220-253","status":"closed","priority":1,"issue_type":"bug","created_at":"2025-12-16T20:52:51.771217-08:00","updated_at":"2025-12-17T23:13:40.532688-08:00","closed_at":"2025-12-17T17:23:55.678799-08:00"} +{"id":"bd-0fvq","title":"bd doctor should recommend bd prime migration for existing repos","description":"bd doctor should detect old beads integration patterns and recommend migrating to bd prime approach.\n\n## Current behavior\n- bd doctor checks if Claude hooks are installed globally\n- Doesn't check project-level integration (AGENTS.md, CLAUDE.md)\n- Doesn't recommend migration for repos using old patterns\n\n## Desired behavior\nbd doctor should detect and suggest:\n\n1. **Old slash command pattern detected**\n - Check for /beads:* references in AGENTS.md, CLAUDE.md\n - Suggest: These slash commands are deprecated, use bd prime hooks instead\n \n2. **No agent documentation**\n - Check if AGENTS.md or CLAUDE.md exists\n - Suggest: Run 'bd onboard' or 'bd setup claude' to document workflow\n \n3. **Old MCP-only pattern**\n - Check for instructions to use MCP tools but no bd prime hooks\n - Suggest: Add bd prime hooks for better token efficiency\n\n4. **Migration path**\n - Show: 'Run bd setup claude to add SessionStart/PreCompact hooks'\n - Show: 'Update AGENTS.md to reference bd prime instead of slash commands'\n\n## Example output\n\n⚠ Warning: Old beads integration detected in CLAUDE.md\n Found: /beads:* slash command references (deprecated)\n Recommend: Migrate to bd prime hooks for better token efficiency\n Fix: Run 'bd setup claude' and update CLAUDE.md\n\nšŸ’” Tip: bd prime + hooks reduces token usage by 80-99% vs slash commands\n MCP mode: ~50 tokens vs ~10.5k for full MCP scan\n CLI mode: ~1-2k tokens with automatic context recovery\n\n## Benefits\n- Helps existing repos adopt new best practices\n- Clear migration path for users\n- Better token efficiency messaging","status":"open","priority":2,"issue_type":"feature","created_at":"2025-11-12T03:20:25.567748-08:00","updated_at":"2025-11-12T03:20:25.567748-08:00"} +{"id":"bd-0w5","title":"Fix update-hooks verification in version-bump.yaml","description":"The update-hooks task verification command at version-bump.yaml:358 always succeeds due to '|| echo ...' fallback. Remove the fallback so verification actually fails when hooks aren't installed.","status":"closed","priority":3,"issue_type":"bug","created_at":"2025-12-17T22:23:06.55467-08:00","updated_at":"2025-12-17T22:34:07.290409-08:00","closed_at":"2025-12-17T22:34:07.290409-08:00"} +{"id":"bd-0zp7","title":"Add missing hook calls in mail reply and ack","description":"The mail commands are missing hook calls:\n\n1. runMailReply (mail.go:525-672) creates a message but doesn't call hookRunner.Run(hooks.EventMessage, ...) after creating the reply in direct mode (around line 640)\n\n2. runMailAck (mail.go:432-523) closes messages but doesn't call hookRunner.Run(hooks.EventClose, ...) after closing each message (around line 487 for daemon mode, 493 for direct mode)\n\nThis means GGT hooks won't fire for replies or message acknowledgments.","status":"closed","priority":1,"issue_type":"bug","created_at":"2025-12-16T20:52:53.069412-08:00","updated_at":"2025-12-17T23:13:40.532054-08:00","closed_at":"2025-12-17T17:22:59.368024-08:00"} +{"id":"bd-1slh","title":"Investigate charmbracelet-based TUI for beads","description":"Now that we've merged the create-form command (PR #603) which uses charmbracelet/huh, investigate whether beads should have a more comprehensive TUI.\n\nConsiderations:\n- Should this be in core or a separate binary (bd-tui)?\n- What functionality would benefit from a TUI? (list view, issue details, search, bulk operations)\n- Plugin/extension architecture vs build tags vs separate binary\n- Dependency cost vs user experience tradeoff\n- Target audience: humans who want interactive workflows vs CLI/scripting users\n\nRelated: PR #603 added charmbracelet/huh dependency for create-form command.","status":"open","priority":3,"issue_type":"feature","created_at":"2025-12-17T14:20:51.503563-08:00","updated_at":"2025-12-17T14:20:51.503563-08:00"} +{"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-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"} -{"id":"bd-kwro.8","title":"Hooks System","description":"Implement hook system for extensibility.\n\nHook directory: .beads/hooks/\nHook files (executable scripts):\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\nHook invocation:\n- Pass issue ID as first argument\n- Pass event type as second argument\n- Pass JSON issue data on stdin\n- Run asynchronously (dont block command)\n\nExample hook (GGT notification):\n #!/bin/bash\n gt notify --event=$2 --issue=$1\n\nThis allows GGT to register notification handlers without Beads knowing about GGT.","status":"tombstone","priority":1,"issue_type":"task","created_at":"2025-12-16T03:02:23.086393-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-3sz0","title":"Auto-repair stale merge driver configs with invalid placeholders","description":"Old bd versions (\u003c0.24.0) installed merge driver with invalid placeholders %L %R instead of %A %B. Add detection to bd doctor --fix: check if git config merge.beads.driver contains %L or %R, auto-repair to 'bd merge %A %O %A %B'. One-time migration for users who initialized with old versions.","status":"open","priority":2,"issue_type":"feature","created_at":"2025-11-21T23:16:10.762808-08:00","updated_at":"2025-11-21T23:16:28.892655-08:00","dependencies":[{"issue_id":"bd-3sz0","depends_on_id":"bd-tbz3","type":"parent-child","created_at":"2025-11-21T23:16:10.763612-08:00","created_by":"daemon"}]} +{"id":"bd-411u","title":"Document BEADS_DIR pattern for multi-agent workspaces (Gas Town)","description":"Gas Town and similar multi-agent systems need to configure separate beads databases per workspace/rig, distinct from any project-level beads.\n\n## Use Case\n\nIn Gas Town:\n- Each 'rig' (managed project) has multiple agents (polecats, refinery, witness)\n- All agents in a rig should share a single beads database at the rig level\n- This should be separate from any .beads/ the project itself uses\n- The BEADS_DIR env var enables this\n\n## Documentation Needed\n\n1. Add a section to docs explaining BEADS_DIR for multi-agent setups\n2. Example: setting BEADS_DIR in agent startup scripts/hooks\n3. Clarify interaction with project-level .beads/ (BEADS_DIR takes precedence)\n\n## Current Support\n\nAlready implemented in internal/beads/beads.go:FindDatabasePath():\n- BEADS_DIR env var is checked first (preferred)\n- BEADS_DB env var still supported (deprecated)\n- Falls back to .beads/ search in tree\n\nJust needs documentation for the multi-agent workspace pattern.","status":"open","priority":3,"issue_type":"task","created_at":"2025-12-15T22:08:22.158027-08:00","updated_at":"2025-12-15T22:08:22.158027-08:00"} +{"id":"bd-47tn","title":"Add bd daemon --stop-all command to kill all daemon processes","description":"Currently there's no easy way to stop all running bd daemon processes. Users must resort to pkill -f 'bd daemon' or similar shell commands.\n\nAdd a --stop-all flag to bd daemon that:\n1. Finds all running bd daemon processes (not just the current repo's daemon)\n2. Gracefully stops them all\n3. Reports how many were stopped\n\nThis is useful when:\n- Multiple daemons are running and causing race conditions\n- User wants a clean slate before running bd sync\n- Debugging daemon-related issues","status":"closed","priority":2,"issue_type":"feature","created_at":"2025-12-13T06:34:45.080633-08:00","updated_at":"2025-12-16T01:14:49.501989-08:00","closed_at":"2025-12-14T17:33:03.057089-08:00"} +{"id":"bd-49kw","title":"Workaround for FastMCP outputSchema bug in Claude Code","description":"The beads MCP server (v0.23.1) successfully connects to Claude Code, but all tools fail to load with a schema validation error due to a bug in FastMCP 2.13.1.\n\nError: \"Invalid literal value, expected \\\"object\\\"\" in outputSchema.\n\nRoot Cause: FastMCP generates outputSchema with $ref at root level without \"type\": \"object\" for self-referential models (Issue).\n\nWorkaround: Use slash commands (/beads:ready) or wait for FastMCP fix.\n","status":"open","priority":1,"issue_type":"bug","created_at":"2025-11-20T18:55:39.041831-05:00","updated_at":"2025-11-20T18:55:39.041831-05:00"} +{"id":"bd-4ec8","title":"Widespread double JSON encoding bug in daemon mode RPC calls","description":"Multiple CLI commands had the same double JSON encoding bug found in bd-1048. All commands that called ResolveID via RPC used string(resp.Data) instead of properly unmarshaling the JSON response. This caused IDs to retain JSON quotes (\"bd-1048\" instead of bd-1048), which then got double-encoded when passed to subsequent RPC calls.\n\nAffected commands:\n- bd show (3 instances)\n- bd dep add/remove/tree (5 instances)\n- bd label add/remove/list (3 instances)\n- bd reopen (1 instance)\n\nRoot cause: resp.Data is json.RawMessage (already JSON-encoded), so string() conversion preserves quotes.\n\nFix: Replace all string(resp.Data) with json.Unmarshal(resp.Data, \u0026id) for proper deserialization.\n\nAll commands now tested and working correctly with daemon mode.","status":"closed","priority":0,"issue_type":"bug","created_at":"2025-11-02T22:33:01.632691-08:00","updated_at":"2025-12-17T23:13:40.533631-08:00","closed_at":"2025-12-17T16:26:05.851197-08:00"} {"id":"bd-4hn","title":"wish: list \u0026 ready show issues as hierarchy tree","description":"`bd ready` and `bd list` just show a flat list, and it's up to the reader to parse which ones are dependent or sub-issues of others. It would be much easier to understand if they were shown in a tree format","status":"open","priority":3,"issue_type":"task","created_at":"2025-12-08T06:38:24.016316945-07:00","updated_at":"2025-12-08T06:39:04.065882225-07:00"} -{"id":"bd-z86n","title":"Code Review: PR #551 - Persist close_reason to issues table","description":"Code review of PR #551 which fixes close_reason persistence bug.\n\n## Summary\nThe PR correctly fixes a bug where close_reason was only stored in the events table, not in the issues.close_reason column. This caused `bd show --json` to return empty close_reason.\n\n## What Was Fixed\n- āœ… CloseIssue now updates both close_reason and closed_at\n- āœ… ReOpenIssue clears both close_reason and closed_at\n- āœ… Comprehensive tests added for both storage and CLI layers\n- āœ… Clear documentation in queries.go about dual storage strategy\n\n## Quality Assessment\nāœ… Tests cover both storage layer and CLI JSON output\nāœ… Handles reopen case (clearing close_reason)\nāœ… Good comments explaining dual-storage design\nāœ… No known issues\n\n## Potential Followups\nSee linked issues for suggestions.","status":"open","priority":2,"issue_type":"task","created_at":"2025-12-14T14:25:06.887069-08:00","updated_at":"2025-12-14T14:25:06.887069-08:00"} -{"id":"bd-kwro.3","title":"Graph Link: relates_to for knowledge graph","description":"Implement relates_to link type for loose associations.\n\nNew command:\n- bd relate \u003cid1\u003e \u003cid2\u003e - creates bidirectional relates_to link\n\nQuery support:\n- bd show \u003cid\u003e --related shows related issues\n- bd list --related-to \u003cid\u003e\n\nStorage:\n- relates_to stored as JSON array of issue IDs\n- Consider: separate junction table for efficiency at scale?\n\nThis enables 'see also' connections without blocking or hierarchy.","status":"tombstone","priority":1,"issue_type":"task","created_at":"2025-12-16T03:01:30.793115-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-y2v","title":"Refactor duplicate JSONL-from-git parsing code","description":"Both readFirstIssueFromGit() in init.go and importFromGit() in autoimport.go have similar code patterns for:\n1. Running git show \u003cref\u003e:\u003cpath\u003e\n2. Scanning the output with bufio.Scanner\n3. Parsing JSON lines\n\nCould be refactored to share a helper like:\n- readJSONLFromGit(gitRef, path string) ([]byte, error)\n- Or a streaming version: streamJSONLFromGit(gitRef, path string) (io.Reader, error)\n\nFiles:\n- cmd/bd/autoimport.go:225-256 (importFromGit)\n- cmd/bd/init.go:1212-1243 (readFirstIssueFromGit)\n\nPriority is low since code duplication is minimal and both functions work correctly.","status":"open","priority":2,"issue_type":"task","created_at":"2025-12-05T14:51:18.41124-08:00","updated_at":"2025-12-05T14:51:18.41124-08:00"} +{"id":"bd-4nqq","title":"Remove dead test code in info_test.go","description":"Code health review found cmd/bd/info_test.go has two tests permanently skipped:\n\n- TestInfoCommand\n- TestInfoCommandNoDaemon\n\nBoth skip with: 'Manual test - bd info command is working, see manual testing'\n\nThese are essentially dead code. Either automate them or remove them entirely.","status":"open","priority":3,"issue_type":"task","created_at":"2025-12-16T18:17:27.554019-08:00","updated_at":"2025-12-16T18:17:27.554019-08:00","dependencies":[{"issue_id":"bd-4nqq","depends_on_id":"bd-tggf","type":"blocks","created_at":"2025-12-16T18:19:06.381694-08:00","created_by":"daemon"}]} +{"id":"bd-4q8","title":"bd cleanup --hard should skip tombstone creation for true permanent deletion","description":"## Problem\n\nWhen using bd cleanup --hard --older-than N --force, the command:\n1. Deletes closed issues older than N days (converting them to tombstones with NOW timestamp)\n2. Then tries to prune tombstones older than N days (finds none because they were just created)\n\nThis leaves the database bloated with fresh tombstones that will not be pruned.\n\n## Expected Behavior\n\nIn --hard mode, the deletion should be permanent without creating tombstones, since the user explicitly requested bypassing sync safety.\n\n## Workaround\n\nManually delete from database: sqlite3 .beads/beads.db 'DELETE FROM issues WHERE status=tombstone'\n\n## Fix Options\n\n1. In --hard mode, use a different delete path that does not create tombstones\n2. After deleting, immediately prune the just-created tombstones regardless of age\n3. Pass a skip_tombstone flag to the delete operation\n\nOption 1 is cleanest - --hard should mean permanent delete without tombstone.","status":"tombstone","priority":1,"issue_type":"bug","created_at":"2025-12-16T01:33:36.580657-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-4qfb","title":"Improve bd doctor output formatting for better readability","description":"The current bd doctor output is a wall of text that's hard to process. Consider improvements like:\n- Grouping related checks into collapsible sections\n- Using color/bold for section headers\n- Showing only failures/warnings by default with --verbose for full output\n- Better visual hierarchy between major sections\n- Summary line at top (e.g., '24 checks passed, 0 warnings, 0 errors')","status":"open","priority":2,"issue_type":"task","created_at":"2025-12-13T09:29:27.557578+11:00","updated_at":"2025-12-13T09:29:27.557578+11:00"} +{"id":"bd-4ri","title":"Fix TestFallbackToDirectModeEnablesFlush deadlock causing 10min test timeout","description":"## Problem\n\nTestFallbackToDirectModeEnablesFlush in direct_mode_test.go deadlocks for 9m59s before timing out, causing the entire test suite to take 10+ minutes instead of \u003c10 seconds.\n\n## Root Cause\n\nDatabase lock contention between test cleanup and flushToJSONL():\n- Test cleanup (line 36) tries to close DB via defer\n- flushToJSONL() (line 132) is still accessing DB\n- Results in deadlock: database/sql.(*DB).Close() waits for mutex while GetJSONLFileHash() holds it\n\n## Stack Trace Evidence\n\n```\ngoroutine 512 [sync.Mutex.Lock, 9 minutes]:\ndatabase/sql.(*DB).Close(0x14000643790)\n .../database/sql/sql.go:927 +0x84\ngithub.com/steveyegge/beads/cmd/bd.TestFallbackToDirectModeEnablesFlush.func1()\n .../direct_mode_test.go:36 +0xf4\n\nWhile goroutine running flushToJSONL() holds DB connection via GetJSONLFileHash()\n```\n\n## Impact\n\n- Test suite: 10+ minutes → should be \u003c10 seconds\n- ALL other tests pass in ~4 seconds\n- This ONE test accounts for 99.9% of test runtime\n\n## Related\n\nThis is the EXACT same issue documented in MAIN_TEST_REFACTOR_NOTES.md for why main_test.go refactoring was deferred - global state manipulation + DB cleanup = deadlock.\n\n## Fix Approaches\n\n1. **Add proper cleanup sequencing** - stop flush goroutines BEFORE closing DB\n2. **Use test-specific DB lifecycle** - ensure flush completes before cleanup\n3. **Mock the flush mechanism** - avoid real DB for testing this code path \n4. **Add explicit timeout handling** - fail fast with clear error instead of hanging\n\n## Files\n\n- cmd/bd/direct_mode_test.go:36-132\n- cmd/bd/autoflush.go:353 (validateJSONLIntegrity)\n- cmd/bd/autoflush.go:508 (flushToJSONLWithState)\n\n## Acceptance\n\n- Test passes without timeout\n- Test suite completes in \u003c10 seconds\n- No deadlock between cleanup and flush operations","status":"closed","priority":1,"issue_type":"bug","created_at":"2025-11-21T20:09:00.794372-05:00","updated_at":"2025-12-17T23:13:40.533279-08:00","closed_at":"2025-12-17T17:25:07.626617-08:00"} +{"id":"bd-4uoc","title":"Code Review Followup Summary: PR #481 + PR #551","description":"## Merged PRs Summary\n\n### PR #551: Persist close_reason to issues table\n- āœ… Merged successfully\n- āœ… Bug fix: close_reason now persisted in database column (not just events table)\n- āœ… Comprehensive test coverage added\n- āœ… Handles reopen case (clearing close_reason)\n\n**Followup Issues Filed:**\n- bd-lxzx: Document close_reason in JSONL export format\n- bd-077e: Update CLI documentation for close_reason field\n\n---\n\n### PR #481: Context Engineering Optimizations (80-90% context reduction)\n- āœ… Merged successfully \n- āœ… Lazy tool discovery: discover_tools() + get_tool_info()\n- āœ… Minimal issue models: IssueMinimal (~80% smaller than full Issue)\n- āœ… Result compaction: Auto-compacts results \u003e20 items\n- āœ… All 28 tests passing\n- āš ļø Breaking change: ready() and list() return type changed\n\n**Followup Issues Filed:**\n- bd-b318: Add integration tests for CompactedResult\n- bd-4u2b: Make compaction settings configurable (THRESHOLD, PREVIEW_COUNT)\n- bd-2kf8: Document CompactedResult response format in CONTEXT_ENGINEERING.md\n- bd-pdr2: Document backwards compatibility considerations\n\n---\n\n## Overall Assessment\n\nBoth PRs are production-ready with solid implementations. All critical functionality works and tests pass. Followup issues focus on:\n1. Documentation improvements (5 issues)\n2. Integration test coverage (1 issue)\n3. Configuration flexibility (1 issue)\n4. Backwards compatibility guidance (1 issue)\n\nNo critical bugs or design issues found.\n\n## Review Completed By\nCode review process completed. Issues auto-created for tracking improvements.","status":"open","priority":2,"issue_type":"task","created_at":"2025-12-14T14:25:59.214886-08:00","updated_at":"2025-12-14T14:25:59.214886-08:00","dependencies":[{"issue_id":"bd-4uoc","depends_on_id":"bd-otf4","type":"discovered-from","created_at":"2025-12-14T14:25:59.216884-08:00","created_by":"stevey"},{"issue_id":"bd-4uoc","depends_on_id":"bd-z86n","type":"discovered-from","created_at":"2025-12-14T14:25:59.217296-08:00","created_by":"stevey"}]} +{"id":"bd-56x","title":"Review PR #514: fix plugin install docs","description":"Review and merge PR #514 from aspiers. This PR fixes incorrect docs for installing Claude Code plugin from source in docs/PLUGIN.md. Clarifies shell vs Claude Code commands and fixes the . vs ./beads argument issue. URL: https://github.com/anthropics/beads/pull/514","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-13T08:15:16.865354+11:00","updated_at":"2025-12-13T07:07:19.729213-08:00","closed_at":"2025-12-13T07:07:19.729213-08:00"} +{"id":"bd-581b80b3","title":"bd find-duplicates - AI-powered duplicate detection","description":"Find semantically duplicate issues.\n\nApproaches:\n1. Mechanical: Exact title/description matching\n2. Embeddings: Cosine similarity (cheap, scalable)\n3. AI: LLM-based semantic comparison (expensive, accurate)\n\nUses embeddings by default for \u003e100 issues.\n\nFiles: cmd/bd/find_duplicates.go (new)","status":"closed","priority":1,"issue_type":"task","created_at":"2025-10-29T20:49:49.126801-07:00","updated_at":"2025-12-17T22:58:34.563511-08:00","closed_at":"2025-12-17T22:58:34.563511-08:00","close_reason":"Closed"} +{"id":"bd-5b6e","title":"Add tests for helper functions (GetDirtyIssueHash, GetAllDependencyRecords, export hashes)","description":"Several utility functions have 0% coverage:\n- GetDirtyIssueHash (dirty.go)\n- GetAllDependencyRecords (dependencies.go)\n- GetExportHash, SetExportHash, ClearAllExportHashes (hash.go)\n\nThese are lower priority but should have basic coverage.","status":"open","priority":4,"issue_type":"task","created_at":"2025-11-01T22:40:58.989976-07:00","updated_at":"2025-11-01T22:40:58.989976-07:00"} +{"id":"bd-5qim","title":"Optimize GetReadyWork performance - 752ms on 10K database (target: \u003c50ms)","description":"","notes":"# Performance Analysis (10K Issue Database)\n\nAnalyzed using CPU profiles from benchmark suite on Apple M2 Pro.\n\n## Operation Performance\n\n| Operation | Time | Allocations | Memory |\n|----------------------------------|---------|-------------|--------|\n| bd ready (GetReadyWork) | ~752ms | 167,466 | 16MB |\n| bd list (SearchIssues no filter) | ~11.6ms | 89,214 | 5.8MB |\n| bd list (SearchIssues filtered) | ~9.2ms | 62,365 | 3.5MB |\n| bd create (CreateIssue) | ~2.6ms | 146 | 8.6KB |\n| bd update (UpdateIssue) | ~0.32ms | 364 | 15KB |\n| bd close (UpdateIssue) | ~0.32ms | 364 | 15KB |\n\n**Target: \u003c50ms for all operations on 10K database**\n\n**Current issue: GetReadyWork is 15x over target (752ms vs 50ms)**\n\n## Root Cause\n\nGetReadyWork (internal/storage/sqlite/ready.go:90-128) uses recursive CTE to propagate blocking:\n- 65x slower than SearchIssues\n- Recalculates entire blocked issue tree on every call\n- Algorithm:\n 1. Find directly blocked issues via 'blocks' dependencies\n 2. Recursively propagate blockage to descendants (max depth: 50)\n 3. Exclude all blocked issues from results\n\n## CPU Profile Analysis\n\n- Database syscalls (pthread_cond_signal, syscall6): ~75%\n- SQLite engine overhead: inherent to recursive CTE\n- Application code (query construction): \u003c1%\n\n**Bottleneck is the recursive CTE query execution, not application code.**\n\n## Optimization Recommendations\n\n### High Impact (Likely to achieve \u003c50ms target)\n\n1. **Cache blocked issue calculation**\n - Add `blocked_issues` table updated on dependency changes\n - Trade write complexity for read speed (ready called \u003e\u003e dependency changes)\n - Eliminates recursive CTE on every read\n\n2. **Add/verify database indexes**\n ```sql\n CREATE INDEX IF NOT EXISTS idx_dependencies_blocked \n ON dependencies(issue_id, type, depends_on_id);\n CREATE INDEX IF NOT EXISTS idx_issues_status \n ON issues(status);\n ```\n\n### Medium Impact\n\n3. **Reduce allocations** (167K allocations for GetReadyWork)\n - Profile `scanIssues()` for object pooling opportunities\n - Reuse slice capacity for repeated calls\n\n### Low Impact (Not recommended)\n- Query optimization for CRUD operations (already \u003c3ms)\n- Connection pooling tuning (not showing in profiles)\n\n## Verification\n\nRun benchmarks to validate optimization:\n```bash\nmake bench-quick\ngo tool pprof -http=:8080 internal/storage/sqlite/bench-cpu-*.prof\n```\n\nProfile files automatically generated in `internal/storage/sqlite/`.","status":"closed","priority":0,"issue_type":"bug","created_at":"2025-11-14T09:02:46.507526-08:00","updated_at":"2025-12-17T23:13:40.534258-08:00","closed_at":"2025-12-17T16:21:37.918868-08:00"} +{"id":"bd-6fe4622f","title":"Remove unreachable utility functions","description":"Several small utility functions are unreachable:\n\nFiles to clean:\n1. `internal/storage/sqlite/hash.go` - `computeIssueContentHash` (line 17)\n - Check if entire file can be deleted if only contains this function\n\n2. `internal/config/config.go` - `FileUsed` (line 151)\n - Delete unused config helper\n\n3. `cmd/bd/git_sync_test.go` - `verifyIssueOpen` (line 300)\n - Delete dead test helper\n\n4. `internal/compact/haiku.go` - `HaikuClient.SummarizeTier2` (line 81)\n - Tier 2 summarization not implemented\n - Options: implement feature OR delete method\n\nImpact: Removes 50-100 LOC depending on decisions","status":"closed","priority":2,"issue_type":"task","created_at":"2025-10-28T16:20:02.434573-07:00","updated_at":"2025-12-17T22:58:34.563993-08:00","closed_at":"2025-12-17T22:58:34.563993-08:00","close_reason":"Closed"} +{"id":"bd-6gd","title":"Remove legacy MCP Agent Mail integration","description":"## Summary\n\nRemove the legacy MCP Agent Mail system that requires an external HTTP server. Keep the native `bd mail` system which stores messages as git-synced issues.\n\n## Background\n\nTwo mail systems exist in the codebase:\n1. **Legacy Agent Mail** (`bd message`) - External server dependency, complex setup\n2. **Native bd mail** (`bd mail`) - Built-in, git-synced, no dependencies\n\nThe legacy system causes confusion and is no longer needed. Gas Town's Town Mail will use the native `bd mail` system.\n\n## Files to Delete\n\n### CLI Command\n- [ ] `cmd/bd/message.go` - The `bd message` command implementation\n\n### MCP Integration\n- [ ] `integrations/beads-mcp/src/beads_mcp/mail.py` - HTTP wrapper for Agent Mail server\n- [ ] `integrations/beads-mcp/src/beads_mcp/mail_tools.py` - MCP tool definitions\n- [ ] `integrations/beads-mcp/tests/test_mail.py` - Tests for legacy mail\n\n### Documentation\n- [ ] `docs/AGENT_MAIL.md`\n- [ ] `docs/AGENT_MAIL_QUICKSTART.md`\n- [ ] `docs/AGENT_MAIL_DEPLOYMENT.md`\n- [ ] `docs/AGENT_MAIL_MULTI_WORKSPACE_SETUP.md`\n- [ ] `docs/adr/002-agent-mail-integration.md`\n\n## Code to Update\n\n- [ ] Remove `message` command registration from `cmd/bd/main.go`\n- [ ] Remove mail tool imports/registration from MCP server `__init__.py` or `server.py`\n- [ ] Check for any other references to Agent Mail in the codebase\n\n## Verification\n\n- [ ] `bd message` command no longer exists\n- [ ] `bd mail` command still works\n- [ ] MCP server starts without errors\n- [ ] Tests pass\n","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-17T23:04:04.099935-08:00","updated_at":"2025-12-17T23:13:24.128752-08:00","closed_at":"2025-12-17T23:13:24.128752-08:00","close_reason":"Removed legacy MCP Agent Mail integration. Kept native bd mail system."} +{"id":"bd-6rl","title":"Merge3Way public API does not expose TTL parameter","description":"The public Merge3Way() function in merge.go does not allow callers to configure the tombstone TTL. It hard-codes the default via merge3WayWithTTL(). While merge3WayWithTTL() exists, it is unexported (lowercase). This means the CLI and tests cannot configure TTL at merge time. Use cases: testing with different TTL values, per-repository TTL configuration, debugging with short TTL, supporting --ttl flag in bd merge command (mentioned in design doc bd-zvg). Recommendation: Export Merge3WayWithTTL (rename to uppercase). Files: internal/merge/merge.go:77, 292-298","status":"open","priority":3,"issue_type":"feature","created_at":"2025-12-05T16:36:15.756814-08:00","updated_at":"2025-12-05T16:36:15.756814-08:00"} +{"id":"bd-6sm6","title":"Improve test coverage for internal/export (37.1% → 60%)","description":"The export package has only 37.1% test coverage. Export functionality needs good coverage to ensure data integrity.\n\nCurrent coverage: 37.1%\nTarget coverage: 60%","status":"open","priority":2,"issue_type":"task","created_at":"2025-12-13T20:43:06.802277-08:00","updated_at":"2025-12-13T21:01:19.08088-08:00"} +{"id":"bd-746","title":"Fix resolvePartialID stub in workflow.go","description":"The resolvePartialID function at workflow.go:921-925 is a stub that just returns the ID unchanged. Should use utils.ResolvePartialID for proper partial ID resolution in direct mode (non-daemon).","status":"closed","priority":2,"issue_type":"bug","created_at":"2025-12-17T22:22:57.586917-08:00","updated_at":"2025-12-17T22:34:07.270168-08:00","closed_at":"2025-12-17T22:34:07.270168-08:00"} +{"id":"bd-74w1","title":"Consolidate duplicate path-finding utilities (findJSONLPath, findBeadsDir, findGitRoot)","description":"Code health review found these functions defined in multiple places:\n\n- findJSONLPath() in autoflush.go:45-73 and doctor/fix/migrate.go\n- findBeadsDir() in autoimport.go:197-239 (with git worktree handling)\n- findGitRoot() in autoimport.go:242-269 (Windows path conversion)\n\nThe beads package has public FindBeadsDir() and FindJSONLPath() APIs that should be used consistently.\n\nImpact: Bug fixes need to be applied in multiple places. Git worktree handling may not be replicated everywhere.\n\nFix: Consolidate all implementations to use the beads package APIs. Remove duplicates.","status":"open","priority":1,"issue_type":"task","created_at":"2025-12-16T18:17:16.694293-08:00","updated_at":"2025-12-16T18:17:16.694293-08:00","dependencies":[{"issue_id":"bd-74w1","depends_on_id":"bd-tggf","type":"blocks","created_at":"2025-12-16T18:19:05.526122-08:00","created_by":"daemon"}]} +{"id":"bd-77gm","title":"Import reports misleading '0 created, 0 updated' when actually importing all issues","description":"When running 'bd import' on a fresh database (no existing issues), the command reports 'Import complete: 0 created, 0 updated' even though it successfully imported all issues from the JSONL file.\n\n**Steps to reproduce:**\n1. Delete .beads/beads.db\n2. Run: bd import .beads/issues.jsonl\n3. Observe output: 'Import complete: 0 created, 0 updated'\n4. Run: bd list\n5. Confirm: All issues are actually present in the database\n\n**Expected behavior:**\nReport the actual number of issues imported, e.g., 'Import complete: 523 created, 0 updated'\n\n**Actual behavior:**\n'Import complete: 0 created, 0 updated' (misleading - makes user think import failed)\n\n**Impact:**\n- Users think import failed when it succeeded\n- Confusing during database sync operations (e.g., after git pull)\n- Makes debugging harder (can't tell if import actually worked)\n\n**Context:**\nDiscovered during VC session when syncing database after git pull. The misleading message caused confusion about whether the database was properly synced with the canonical JSONL file.","status":"open","priority":2,"issue_type":"bug","created_at":"2025-11-09T16:20:13.191156-08:00","updated_at":"2025-11-09T16:20:13.191156-08:00"} +{"id":"bd-7b7h","title":"bd sync --merge fails due to chicken-and-egg: .beads/ always dirty","description":"## Problem\n\nWhen sync.branch is configured (e.g., beads-sync), the bd sync workflow creates a chicken-and-egg problem:\n\n1. `bd sync` commits changes to beads-sync via worktree\n2. `bd sync` copies JSONL to main working dir via `copyJSONLToMainRepo()` (sync.go line 364, worktree.go line 678-685)\n3. The copy is NOT committed to main - it just updates the working tree\n4. `bd sync --merge` checks for clean working dir (sync.go line 1547-1548)\n5. `bd sync --merge` FAILS because .beads/issues.jsonl is uncommitted!\n\n## Impact\n\n- sync.branch workflow is fundamentally broken\n- Users cannot periodically merge beads-sync → main\n- Main branch always shows as dirty\n- Creates confusion about git state\n\n## Root Cause\n\nsync.go:1547-1548:\n```go\nif len(strings.TrimSpace(string(statusOutput))) \u003e 0 {\n return fmt.Errorf(\"main branch has uncommitted changes, please commit or stash them first\")\n}\n```\n\nThis check blocks merge when ANY uncommitted changes exist, including the .beads/ changes that `bd sync` itself created.\n\n## Proposed Fix\n\nOption A: Exclude .beads/ from the clean check in `mergeSyncBranch`:\n```go\n// Check if there are non-beads uncommitted changes\nstatusCmd := exec.CommandContext(ctx, \"git\", \"status\", \"--porcelain\", \"--\", \":!.beads/\")\n```\n\nOption B: Auto-stash .beads/ changes before merge, restore after\n\nOption C: Change the workflow - do not copy JSONL to main working dir, instead always read from worktree\n\n## Files to Modify\n\n- cmd/bd/sync.go:1540-1549 (mergeSyncBranch function)\n- Possibly internal/syncbranch/worktree.go (copyJSONLToMainRepo)","notes":"## Fix Implemented\n\nModified cmd/bd/sync.go mergeSyncBranch function:\n\n1. **Exclude .beads/ from dirty check** (line 1543):\n Changed `git status --porcelain` to `git status --porcelain -- :!.beads/`\n This allows merge to proceed when only .beads/ has uncommitted changes.\n\n2. **Restore .beads/ to HEAD before merge** (lines 1553-1561):\n Added `git checkout HEAD -- .beads/` before merge to prevent\n \"Your local changes would be overwritten by merge\" errors.\n The .beads/ changes are redundant since they came FROM beads-sync.\n\n## Testing\n\n- All cmd/bd sync/merge tests pass\n- All internal/syncbranch tests pass\n- Manual verification needed for full workflow","status":"tombstone","priority":0,"issue_type":"bug","created_at":"2025-12-16T23:06:06.97703-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-7bbc4e6a","title":"Add MCP server functions for repair commands","description":"**Summary:** Added MCP server repair functions for agent dependency management, system validation, and pollution detection. Implemented across BdClientBase, BdCliClient, and daemon clients to enhance system diagnostics and self-healing capabilities.\n\n**Key Decisions:** \n- Expose repair_deps(), detect_pollution(), validate() via MCP server\n- Create abstract method stubs with fallback to CLI execution\n- Use @mcp.tool decorators for function registration\n\n**Resolution:** Successfully implemented comprehensive repair command infrastructure, enabling more robust system health monitoring and automated remediation with full CLI and daemon support.","notes":"Implemented all three MCP server functions:\n\n1. **repair_deps(fix=False)** - Find/fix orphaned dependencies\n2. **detect_pollution(clean=False)** - Detect/clean test issues \n3. **validate(checks=None, fix_all=False)** - Run comprehensive health checks\n\nChanges:\n- Added abstract methods to BdClientBase\n- Implemented in BdCliClient (CLI execution)\n- Added NotImplementedError stubs in BdDaemonClient (falls back to CLI)\n- Created wrapper functions in tools.py\n- Registered @mcp.tool decorators in server.py\n\nAll commands tested and working with --no-daemon flag.","status":"closed","priority":2,"issue_type":"task","created_at":"2025-10-28T19:37:55.72639-07:00","updated_at":"2025-12-16T01:08:11.983953-08:00","closed_at":"2025-11-07T19:38:12.152437-08:00"} +{"id":"bd-7di","title":"worktree: any bd command is slow","description":"in a git worktree any bd command is slow, with a 2-3s pause before any results are shown. The identical command with `--no-daemon` is near instant.","status":"open","priority":2,"issue_type":"task","created_at":"2025-12-05T15:33:42.924618693-07:00","updated_at":"2025-12-05T15:33:42.924618693-07:00"} +{"id":"bd-7h7","title":"bd init should stop running daemon to avoid stale cache","description":"When running bd init, any running daemon continues with stale cached data, causing bd stats and other commands to show old counts.\n\nRepro:\n1. Have daemon running with 788 issues cached\n2. Clean JSONL to 128 issues, delete db, run bd init\n3. bd stats still shows 788 (daemon cache)\n4. Must manually run bd daemon --stop\n\nFix: bd init should automatically stop any running daemon before reinitializing.","status":"tombstone","priority":2,"issue_type":"bug","created_at":"2025-12-16T13:26:47.117226-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-7m16","title":"GH#519: bd sync fails when sync.branch is currently checked-out branch","description":"bd sync tries to create worktree for sync.branch even when already on that branch. Should commit directly instead. See GitHub issue #519.","status":"tombstone","priority":2,"issue_type":"bug","created_at":"2025-12-16T01:03:36.613211-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-7yg","title":"Git merge driver uses invalid placeholders (%L, %R instead of %A, %B)","description":"## Problem\n\nThe beads git merge driver is configured with invalid Git placeholders:\n\n```\ngit config merge.beads.driver \"bd merge %A %O %L %R\"\n```\n\nGit doesn't recognize `%L` or `%R` as valid merge driver placeholders. The valid placeholders are:\n- `%O` = base (common ancestor)\n- `%A` = current version (ours)\n- `%B` = other version (theirs)\n\n## Impact\n\n- Affects ALL users when they have `.beads/beads.jsonl` merge conflicts\n- Automatic JSONL merge fails with error: \"error reading left file: failed to open file: open 7: no such file or directory\"\n- Users must manually resolve conflicts instead of getting automatic merge\n\n## Root Cause\n\nThe `bd init` command (or wherever the merge driver is configured) is using non-standard placeholders. When Git encounters `%L` and `%R`, it either passes them literally or interprets them incorrectly.\n\n## Fix\n\nUpdate the merge driver configuration to:\n```\ngit config merge.beads.driver \"bd merge %A %O %A %B\"\n```\n\nWhere:\n- 1st `%A` = output file (current file, will be overwritten)\n- `%O` = base (common ancestor)\n- 2nd `%A` = left/current version\n- `%B` = right/other version\n\n## Action Items\n\n1. Fix `bd init` (or equivalent setup command) to use correct placeholders\n2. Add migration/warning for existing users with misconfigured merge driver\n3. Update documentation with correct merge driver setup\n4. Consider adding validation when `bd init` is run","status":"closed","priority":1,"issue_type":"bug","created_at":"2025-11-21T19:51:55.747608-05:00","updated_at":"2025-12-17T23:13:40.532368-08:00","closed_at":"2025-12-17T17:24:52.678668-08:00"} +{"id":"bd-89f89fc0","title":"Remove unreachable RPC methods","description":"Several RPC server and client methods are unreachable and should be removed:\n\nServer methods (internal/rpc/server.go):\n- `Server.GetLastImportTime` (line 2116)\n- `Server.SetLastImportTime` (line 2123)\n- `Server.findJSONLPath` (line 2255)\n\nClient methods (internal/rpc/client.go):\n- `Client.Import` (line 311) - RPC import not used (daemon uses autoimport)\n\nEvidence:\n```bash\ngo run golang.org/x/tools/cmd/deadcode@latest -test ./...\n```\n\nImpact: Removes ~80 LOC of unused RPC code","status":"closed","priority":2,"issue_type":"task","created_at":"2025-10-28T16:20:02.432202-07:00","updated_at":"2025-12-17T22:58:34.564401-08:00","closed_at":"2025-12-17T22:58:34.564401-08:00","close_reason":"Closed"} +{"id":"bd-8fgn","title":"test hash length","description":"","status":"tombstone","priority":2,"issue_type":"task","created_at":"2025-12-16T13:49:32.113843-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-8g8","title":"Fix G304 potential file inclusion in cmd/bd/tips.go:259","description":"Linting issue: G304: Potential file inclusion via variable (gosec) at cmd/bd/tips.go:259:18. Error: if data, err := os.ReadFile(settingsPath); err == nil {","status":"closed","priority":0,"issue_type":"bug","created_at":"2025-12-07T15:34:57.189730843-07:00","updated_at":"2025-12-17T23:13:40.534569-08:00","closed_at":"2025-12-17T16:46:11.029837-08:00"} +{"id":"bd-90v","title":"bd prime: AI context loading and Claude Code integration","description":"Implement `bd prime` command and Claude Code hooks for context recovery. Hooks work with BOTH MCP server and CLI approaches - they solve the context memory problem (keeping bd workflow fresh after compaction) not the tool access problem (MCP vs CLI).","status":"open","priority":2,"issue_type":"epic","created_at":"2025-11-11T23:31:12.119012-08:00","updated_at":"2025-11-12T00:11:07.743189-08:00"} +{"id":"bd-9cdc","title":"Update docs for import bug fix","description":"Update AGENTS.md, README.md, TROUBLESHOOTING.md with import.orphan_handling config documentation. Document resurrection behavior, tombstones, config modes. Add troubleshooting section for import failures with deleted parents.","status":"open","priority":2,"issue_type":"task","created_at":"2025-11-04T12:32:30.770415-08:00","updated_at":"2025-11-04T12:32:30.770415-08:00"} +{"id":"bd-9g1z","title":"Fix or remove TestFindJSONLPathDefault (issue #356)","description":"Code health review found .test-skip permanently skips TestFindJSONLPathDefault.\n\nThe test references issue #356 about wrong JSONL filename expectations (issues.jsonl vs beads.jsonl).\n\nTest file: internal/beads/beads_test.go\n\nThe underlying migration from beads.jsonl to issues.jsonl may be complete, so either:\n1. Fix the test expectations\n2. Remove the test if no longer needed\n3. Document why it remains skipped","status":"open","priority":2,"issue_type":"bug","created_at":"2025-12-16T18:17:31.33975-08:00","updated_at":"2025-12-16T18:17:31.33975-08:00","dependencies":[{"issue_id":"bd-9g1z","depends_on_id":"bd-tggf","type":"blocks","created_at":"2025-12-16T18:19:06.169617-08:00","created_by":"daemon"}]} +{"id":"bd-9usz","title":"Test suite hangs/never finishes","description":"Running 'go test ./... -count=1' hangs indefinitely. The full test suite never completes, making it difficult to verify changes. Need to investigate which tests are hanging and fix or add timeouts.","status":"open","priority":2,"issue_type":"bug","created_at":"2025-12-16T21:56:27.80191-08:00","updated_at":"2025-12-16T21:56:27.80191-08:00"} +{"id":"bd-a0cp","title":"Consider using types.Status in merge package for type safety","description":"The merge package uses string for status comparison (e.g., result.Status == closed, issue.Status == StatusTombstone). The types package defines Status as a type alias with validation. While the merge package needs its own Issue struct for JSONL flexibility, it could import and use types.Status for constants to get compile-time type checking. Current code: if left == closed || right == closed. Could be: if left == string(types.StatusClosed). This is low priority since string comparison works correctly. Files: internal/merge/merge.go:44, 488, 501-521","status":"open","priority":4,"issue_type":"task","created_at":"2025-12-05T16:37:10.690424-08:00","updated_at":"2025-12-05T16:37:10.690424-08:00"} +{"id":"bd-a15d","title":"Add test files for internal/storage","description":"The internal/storage package has no test files at all. This package provides the storage interface abstraction.\n\nCurrent coverage: N/A (no test files)\nTarget: Add basic interface tests","status":"open","priority":2,"issue_type":"task","created_at":"2025-12-13T20:43:11.363017-08:00","updated_at":"2025-12-13T21:01:20.925779-08:00"} +{"id":"bd-aay","title":"Warn on invalid depends_on references in workflow templates","description":"workflow.go:780-781 silently skips invalid dependency names. Should log a warning when a depends_on reference doesn't match any task ID in the template.","status":"closed","priority":4,"issue_type":"task","created_at":"2025-12-17T22:23:04.325253-08:00","updated_at":"2025-12-17T22:34:07.309495-08:00","closed_at":"2025-12-17T22:34:07.309495-08:00"} +{"id":"bd-abjw","title":"Consider consolidating config.yaml parsing into shared utility","description":"Multiple places parse config.yaml with custom structs:\n\n1. **autoimport.go:148** - `localConfig{SyncBranch}`\n2. **main.go:310** - strings.Contains for no-db (fragile, see bd-r6k2)\n3. **doctor.go:863** - strings.Contains for no-db (fragile, see bd-r6k2)\n4. **internal/config/config.go** - Uses viper (but caches at startup, problematic for tests)\n\nConsider creating a shared utility in `internal/configfile/` or extending the viper config:\n\n```go\n// internal/configfile/yaml.go\ntype YAMLConfig struct {\n SyncBranch string `yaml:\"sync-branch\"`\n NoDb bool `yaml:\"no-db\"`\n IssuePrefix string `yaml:\"issue-prefix\"`\n Author string `yaml:\"author\"`\n}\n\nfunc LoadYAML(beadsDir string) (*YAMLConfig, error) {\n // Parse config.yaml with proper YAML library\n}\n```\n\nBenefits:\n- Single source of truth for config.yaml structure\n- Proper YAML parsing everywhere\n- Easier to add new config fields\n\nTrade-off: May add complexity for simple one-off reads.","status":"open","priority":4,"issue_type":"task","created_at":"2025-12-07T02:03:26.067311-08:00","updated_at":"2025-12-07T02:03:26.067311-08:00"} +{"id":"bd-adoe","title":"Add --hard flag to bd cleanup to permanently cull tombstones before cutoff date","description":"Currently tombstones persist for 30 days before cleanup prunes them. Need an official way to force-cull tombstones earlier than the default TTL, for scenarios like cleaning house after extended absence where resurrection from old clones is not a concern. Proposed: bd cleanup --hard --older-than N to bypass the 30-day tombstone TTL.","status":"tombstone","priority":2,"issue_type":"feature","created_at":"2025-12-16T01:17:31.064914-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-aec5439f","title":"Update LINTING.md with current baseline","description":"After cleanup, document the remaining acceptable baseline in LINTING.md so we can track regression.","status":"closed","priority":2,"issue_type":"task","created_at":"2025-10-27T18:53:10.38679-07:00","updated_at":"2025-12-17T22:58:34.564854-08:00","closed_at":"2025-12-17T22:58:34.564854-08:00","close_reason":"Closed"} +{"id":"bd-au0","title":"Command Set Standardization \u0026 Flag Consistency","description":"Comprehensive improvements to bd command set based on 2025 audit findings.\n\n## Background\nSee docs/command-audit-2025.md for detailed analysis.\n\n## Goals\n1. Standardize flag naming and behavior across all commands\n2. Add missing flags for feature parity\n3. Fix naming confusion\n4. Improve consistency in JSON output\n\n## Success Criteria\n- All mutating commands support --dry-run (no --preview variants)\n- bd update supports label operations\n- bd search has filter parity with bd list\n- Priority flags accept both int and P0-P4 format everywhere\n- JSON output is consistent across all commands","status":"open","priority":2,"issue_type":"epic","created_at":"2025-11-21T21:05:55.672749-05:00","updated_at":"2025-11-21T21:05:55.672749-05:00"} +{"id":"bd-au0.10","title":"Add global verbosity flags (--verbose, --quiet)","description":"Add consistent verbosity controls across all commands.\n\n**Current state:**\n- bd init has --quiet flag\n- No other commands have verbosity controls\n- Debug output controlled by BD_VERBOSE env var\n\n**Proposal:**\nAdd persistent flags:\n- --verbose / -v: Enable debug output\n- --quiet / -q: Suppress non-essential output\n\n**Implementation:**\n- Add to rootCmd.PersistentFlags()\n- Replace BD_VERBOSE checks with flag checks\n- Standardize output levels:\n * Quiet: Errors only\n * Normal: Errors + success messages\n * Verbose: Errors + success + debug info\n\n**Files to modify:**\n- cmd/bd/main.go (add flags)\n- internal/debug/debug.go (respect flags)\n- Update all commands to respect quiet mode\n\n**Testing:**\n- Verify --verbose shows debug output\n- Verify --quiet suppresses normal output\n- Ensure errors always show regardless of mode","status":"open","priority":3,"issue_type":"task","created_at":"2025-11-21T21:08:21.600209-05:00","updated_at":"2025-11-21T21:08:21.600209-05:00","dependencies":[{"issue_id":"bd-au0.10","depends_on_id":"bd-au0","type":"parent-child","created_at":"2025-11-21T21:08:21.602557-05:00","created_by":"daemon"}]} +{"id":"bd-au0.5","title":"Add date and priority filters to bd search","description":"Add filter parity with bd list for consistent querying.\n\n**Missing filters to add:**\n- --priority, --priority-min, --priority-max\n- --created-after, --created-before\n- --updated-after, --updated-before\n- --closed-after, --closed-before\n- --empty-description, --no-assignee, --no-labels\n- --desc-contains, --notes-contains\n\n**Files to modify:**\n- cmd/bd/search.go\n- internal/rpc/protocol.go (SearchArgs)\n- internal/storage/storage.go (Search method)\n\n**Testing:**\n- All filter combinations\n- Date format parsing\n- Daemon and direct mode","status":"open","priority":1,"issue_type":"task","created_at":"2025-11-21T21:07:05.496726-05:00","updated_at":"2025-11-21T21:07:05.496726-05:00","dependencies":[{"issue_id":"bd-au0.5","depends_on_id":"bd-au0","type":"parent-child","created_at":"2025-11-21T21:07:05.497762-05:00","created_by":"daemon"}]} +{"id":"bd-au0.6","title":"Add comprehensive filters to bd export","description":"Enhance bd export with filtering options for selective exports.\n\n**Currently only has:**\n- --status\n\n**Add filters:**\n- --label, --label-any\n- --assignee\n- --type\n- --priority, --priority-min, --priority-max\n- --created-after, --created-before\n- --updated-after, --updated-before\n\n**Use case:**\n- Export only open issues: bd export --status open\n- Export high-priority bugs: bd export --type bug --priority-max 1\n- Export recent issues: bd export --created-after 2025-01-01\n\n**Files to modify:**\n- cmd/bd/export.go\n- Reuse filter logic from list.go","status":"open","priority":1,"issue_type":"task","created_at":"2025-11-21T21:07:19.431307-05:00","updated_at":"2025-11-21T21:07:19.431307-05:00","dependencies":[{"issue_id":"bd-au0.6","depends_on_id":"bd-au0","type":"parent-child","created_at":"2025-11-21T21:07:19.432983-05:00","created_by":"daemon"}]} +{"id":"bd-au0.7","title":"Audit and standardize JSON output across all commands","description":"Ensure consistent JSON format and error handling when --json flag is used.\n\n**Scope:**\n1. Verify all commands respect --json flag\n2. Standardize success response format\n3. Standardize error response format\n4. Document JSON schemas\n\n**Commands to audit:**\n- Core CRUD: create, update, delete, show, list, search āœ“\n- Queries: ready, blocked, stale, count, stats, status\n- Deps: dep add/remove/tree/cycles\n- Labels: label commands\n- Comments: comments add/list/delete\n- Epics: epic status/close-eligible\n- Export/import: already support --json āœ“\n\n**Testing:**\n- Success cases return valid JSON\n- Error cases return valid JSON (not plain text)\n- Consistent field naming (snake_case vs camelCase)\n- Array vs object wrapping consistency","status":"open","priority":1,"issue_type":"task","created_at":"2025-11-21T21:07:35.304424-05:00","updated_at":"2025-11-21T21:07:35.304424-05:00","dependencies":[{"issue_id":"bd-au0.7","depends_on_id":"bd-au0","type":"parent-child","created_at":"2025-11-21T21:07:35.305663-05:00","created_by":"daemon"}]} +{"id":"bd-au0.8","title":"Improve clean vs cleanup command naming/documentation","description":"Clarify the difference between bd clean and bd cleanup to reduce user confusion.\n\n**Current state:**\n- bd clean: Remove temporary artifacts (.beads/bd.sock, logs, etc.)\n- bd cleanup: Delete old closed issues from database\n\n**Options:**\n1. Rename for clarity:\n - bd clean → bd clean-temp\n - bd cleanup → bd cleanup-issues\n \n2. Keep names but improve help text and documentation\n\n3. Add prominent warnings in help output\n\n**Preferred approach:** Option 2 (improve documentation)\n- Update short/long descriptions in commands\n- Add examples to help text\n- Update README.md\n- Add cross-references in help output\n\n**Files to modify:**\n- cmd/bd/clean.go\n- cmd/bd/cleanup.go\n- README.md or ADVANCED.md","status":"open","priority":2,"issue_type":"task","created_at":"2025-11-21T21:07:49.960534-05:00","updated_at":"2025-11-21T21:07:49.960534-05:00","dependencies":[{"issue_id":"bd-au0.8","depends_on_id":"bd-au0","type":"parent-child","created_at":"2025-11-21T21:07:49.962743-05:00","created_by":"daemon"}]} +{"id":"bd-au0.9","title":"Review and document rarely-used commands","description":"Document use cases or consider deprecation for infrequently-used commands.\n\n**Commands to review:**\n1. bd rename-prefix - How often is this used? Document use cases\n2. bd detect-pollution - Consider integrating into bd validate\n3. bd migrate-hash-ids - One-time migration, keep but document as legacy\n\n**For each command:**\n- Document typical use cases\n- Add examples to help text\n- Consider if it should be a subcommand instead\n- Add deprecation warning if appropriate\n\n**Not changing:**\n- duplicates āœ“ (useful for data quality)\n- repair-deps āœ“ (useful for fixing broken refs)\n- restore āœ“ (critical for compacted issues)\n- compact āœ“ (performance feature)\n\n**Deliverable:**\n- Updated help text\n- Documentation in ADVANCED.md\n- Deprecation plan if needed","status":"open","priority":3,"issue_type":"task","created_at":"2025-11-21T21:08:05.588275-05:00","updated_at":"2025-11-21T21:08:05.588275-05:00","dependencies":[{"issue_id":"bd-au0.9","depends_on_id":"bd-au0","type":"parent-child","created_at":"2025-11-21T21:08:05.59003-05:00","created_by":"daemon"}]} +{"id":"bd-aydr","title":"Add bd reset command for clean slate restart","description":"Implement a `bd reset` command to reset beads to a clean starting state.\n\n## Context\nGitHub 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## Design\n\n### Command Interface\n```\nbd reset [--hard] [--force] [--backup] [--dry-run] [--no-init]\n```\n\n| Flag | Effect |\n|------|--------|\n| `--hard` | Also remove from git index and commit |\n| `--force` | Skip confirmation prompt |\n| `--backup` | Create `.beads-backup-{timestamp}/` first |\n| `--dry-run` | Preview what would happen |\n| `--no-init` | Don't re-initialize after clearing |\n\n### Reset Levels\n1. **Soft Reset (default)** - Kill daemons, clear .beads/, re-init. Git history unchanged.\n2. **Hard Reset (`--hard`)** - Also git rm and commit the removal, then commit fresh state.\n\n### Implementation Flow\n1. Validate .beads/ exists\n2. If not --force: show impact summary, prompt confirmation\n3. If --backup: copy .beads/ to .beads-backup-{timestamp}/\n4. Kill daemons\n5. If --hard: git rm + commit\n6. rm -rf .beads/*\n7. If not --no-init: bd init (and git add+commit if --hard)\n8. Print summary\n\n### Safety Mechanisms\n- Confirmation prompt (skip with --force)\n- Impact summary (issue/tombstone counts)\n- Backup option\n- Dry-run preview\n- Git dirty check warning\n\n### Code Structure\n- `cmd/bd/reset.go` - CLI command\n- `internal/reset/` - Core logic package","status":"closed","priority":2,"issue_type":"epic","created_at":"2025-12-13T08:44:01.38379+11:00","updated_at":"2025-12-13T06:24:29.561294-08:00","closed_at":"2025-12-13T10:18:19.965287+11:00"} +{"id":"bd-aydr.1","title":"Implement core reset package (internal/reset)","description":"Create the core reset logic in internal/reset/ package.\n\n## Responsibilities\n- ResetOptions struct with all flag options\n- CountImpact() - count issues/tombstones that will be deleted\n- ValidateState() - check .beads/ exists, check git dirty state\n- ExecuteReset() - main reset logic (without CLI concerns)\n- Integrate with daemon killall\n\n## Interface Design\n```go\ntype ResetOptions struct {\n Hard bool // Include git operations (git rm, commit)\n Backup bool // Create backup before reset\n DryRun bool // Preview only, don't execute\n SkipInit bool // Don't re-initialize after reset\n}\n\ntype ResetResult struct {\n IssuesDeleted int\n TombstonesDeleted int\n BackupPath string // if backup was created\n DaemonsKilled int\n}\n\ntype ImpactSummary struct {\n IssueCount int\n OpenCount int\n ClosedCount int\n TombstoneCount int\n HasUncommitted bool // git dirty state\n}\n\nfunc Reset(opts ResetOptions) (*ResetResult, error)\nfunc CountImpact() (*ImpactSummary, error)\nfunc ValidateState() error\n```\n\n## IMPORTANT: CLI vs Core Separation\n- `Force` (skip confirmation) is NOT in ResetOptions - that's a CLI concern\n- Core always executes when called; CLI decides whether to prompt first\n- Keep CLI-agnostic: no prompts, no colored output, no user interaction\n- Return errors for CLI to handle with user-friendly messages\n- Unit testable in isolation\n\n## Dependencies\n- Uses daemon.KillAllDaemons() from internal/daemon/\n- Calls bd init logic after reset (unless SkipInit)","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-13T08:44:50.145364+11:00","updated_at":"2025-12-13T10:13:32.610253+11:00","closed_at":"2025-12-13T09:20:06.184893+11:00","dependencies":[{"issue_id":"bd-aydr.1","depends_on_id":"bd-aydr","type":"parent-child","created_at":"2025-12-13T08:44:50.145775+11:00","created_by":"daemon"}]} +{"id":"bd-aydr.2","title":"Implement backup functionality for reset","description":"Add backup capability that can be used by reset command.\n\n## Functionality\n- Copy .beads/ to .beads-backup-{timestamp}/\n- Timestamp format: YYYYMMDD-HHMMSS\n- Preserve file permissions\n- Return backup path for user feedback\n\n## Location\n`internal/reset/backup.go` - keep with reset package for now (YAGNI)\n\n## Interface\n```go\nfunc CreateBackup(beadsDir string) (backupPath string, err error)\n```\n\n## Notes\n- Simple recursive file copy, no compression needed\n- Error if backup dir already exists (unlikely with timestamp)\n- Backup directories SHOULD be gitignored\n- Add `.beads-backup-*/` pattern to .beads/.gitignore template in doctor package\n- Consider: ListBackups() for future `bd backup list` command (not for this PR)","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-13T08:44:51.306103+11:00","updated_at":"2025-12-13T10:13:32.610819+11:00","closed_at":"2025-12-13T09:20:20.590488+11:00","dependencies":[{"issue_id":"bd-aydr.2","depends_on_id":"bd-aydr","type":"parent-child","created_at":"2025-12-13T08:44:51.306474+11:00","created_by":"daemon"}]} +{"id":"bd-aydr.3","title":"Add git operations for --hard reset","description":"Implement git integration for hard reset mode.\n\n## Operations Needed\n1. `git rm -rf .beads/*.jsonl` - remove data files from index\n2. `git commit -m 'beads: reset to clean state'` - commit removal\n3. After re-init: `git add .beads/` and commit fresh state\n\n## Edge Cases to Handle\n- Uncommitted changes in .beads/ - warn or error\n- Detached HEAD state - warn, maybe block\n- Git not initialized - skip git ops, warn\n- Git operations fail mid-way - clear error messaging\n\n## Interface\n```go\ntype GitState struct {\n IsRepo bool\n IsDirty bool // uncommitted changes in .beads/\n IsDetached bool // detached HEAD\n Branch string // current branch name\n}\n\nfunc CheckGitState(beadsDir string) (*GitState, error)\nfunc GitRemoveBeads(beadsDir string) error\nfunc GitCommitReset(message string) error\nfunc GitAddAndCommit(beadsDir, message string) error\n```\n\n## Location\n`internal/reset/git.go` - keep with reset package for now\n\nNote: Codebase has no central git package. internal/compact/git.go is compact-specific.\nFuture refactoring could extract shared git utilities, but YAGNI for now.","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-13T08:44:52.798312+11:00","updated_at":"2025-12-13T10:13:32.611131+11:00","closed_at":"2025-12-13T09:17:40.785927+11:00","dependencies":[{"issue_id":"bd-aydr.3","depends_on_id":"bd-aydr","type":"parent-child","created_at":"2025-12-13T08:44:52.798715+11:00","created_by":"daemon"}]} +{"id":"bd-aydr.4","title":"Implement CLI command (cmd/bd/reset.go)","description":"Wire up the reset command with Cobra CLI.\n\n## Responsibilities\n- Define command and all flags\n- User confirmation prompt (unless --force)\n- Display impact summary before confirmation\n- Colored output and progress indicators\n- Call core reset package\n- Handle errors with user-friendly messages\n- Register command with rootCmd in init()\n\n## Flags\n```go\n--hard bool \"Also remove from git and commit\"\n--force bool \"Skip confirmation prompt\"\n--backup bool \"Create backup before reset\"\n--dry-run bool \"Preview what would happen\"\n--skip-init bool \"Do not re-initialize after reset\"\n--verbose bool \"Show detailed progress output\"\n```\n\n## Output Format\n```\nāš ļø This will reset beads to a clean state.\n\nWill be deleted:\n • 47 issues (23 open, 24 closed)\n • 12 tombstones\n\nContinue? [y/N] y\n\n→ Stopping daemons... āœ“\n→ Removing .beads/... āœ“\n→ Initializing fresh... āœ“\n\nāœ“ Reset complete. Run 'bd onboard' to set up hooks.\n```\n\n## Implementation Notes\n- Confirmation logic lives HERE, not in core package\n- Use color package (github.com/fatih/color) for output\n- Follow patterns from other commands (init.go, doctor.go)\n- Add to rootCmd in init() function\n\n## File Location\n`cmd/bd/reset.go`","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-13T08:44:54.318854+11:00","updated_at":"2025-12-13T10:13:32.611434+11:00","closed_at":"2025-12-13T09:59:41.72638+11:00","dependencies":[{"issue_id":"bd-aydr.4","depends_on_id":"bd-aydr","type":"parent-child","created_at":"2025-12-13T08:44:54.319237+11:00","created_by":"daemon"},{"issue_id":"bd-aydr.4","depends_on_id":"bd-aydr.1","type":"blocks","created_at":"2025-12-13T08:45:09.762138+11:00","created_by":"daemon"},{"issue_id":"bd-aydr.4","depends_on_id":"bd-aydr.2","type":"blocks","created_at":"2025-12-13T08:45:09.817854+11:00","created_by":"daemon"},{"issue_id":"bd-aydr.4","depends_on_id":"bd-aydr.3","type":"blocks","created_at":"2025-12-13T08:45:09.883658+11:00","created_by":"daemon"}]} +{"id":"bd-aydr.5","title":"Enhance bd doctor to suggest reset for broken states","description":"Update bd doctor to detect severely broken states and suggest reset.\n\n## Detection Criteria\nSuggest reset when:\n- Multiple unfixable errors detected\n- Corrupted JSONL that can't be repaired\n- Schema version mismatch that can't be migrated\n- Daemon state inconsistent and unkillable\n\n## Implementation\nAdd to doctor's check/fix flow:\n```go\nif unfixableErrors \u003e threshold {\n suggest('State may be too broken to fix. Consider: bd reset')\n}\n```\n\n## Output Example\n```\nāœ— Found 5 unfixable errors\n \n Your beads state may be too corrupted to repair.\n Consider running 'bd reset' to start fresh.\n (Use 'bd reset --backup' to save current state first)\n```\n\n## Notes\n- Don't auto-run reset, just suggest\n- This is lower priority, can be done in parallel with main work","status":"closed","priority":3,"issue_type":"task","created_at":"2025-12-13T08:44:55.591986+11:00","updated_at":"2025-12-13T06:24:29.561624-08:00","closed_at":"2025-12-13T10:17:23.4522+11:00","dependencies":[{"issue_id":"bd-aydr.5","depends_on_id":"bd-aydr","type":"parent-child","created_at":"2025-12-13T08:44:55.59239+11:00","created_by":"daemon"}]} +{"id":"bd-aydr.6","title":"Add unit tests for reset package","description":"Comprehensive unit tests for internal/reset package.\n\n## Test Cases\n\n### ValidateState tests\n- .beads/ exists → success\n- .beads/ missing → appropriate error\n- git dirty state detection\n\n### CountImpact tests \n- Empty .beads/ → zero counts\n- With issues → correct count (open vs closed)\n- With tombstones → correct count\n- Returns HasUncommitted correctly\n\n### Backup tests\n- Creates backup with correct timestamp format\n- Preserves all files and permissions\n- Returns correct path\n- Handles missing .beads/ gracefully\n- Errors on pre-existing backup dir\n\n### Git operation tests\n- CheckGitState detects dirty, detached, not-a-repo\n- GitRemoveBeads removes correct files\n- GitCommitReset creates commit with message\n- Operations skip gracefully when not in git repo\n\n### Reset tests (with mocks/temp dirs)\n- Soft reset removes files, calls init\n- Hard reset includes git operations\n- Dry run doesn't modify anything\n- SkipInit flag prevents re-initialization\n- Daemon killall is called\n- Backup is created when requested\n\n## Approach\n- Can start with interface definitions (TDD style)\n- Use testify for assertions\n- Create temp directories for isolation\n- Mock git operations where needed\n- Test completion depends on implementation tasks\n\n## File Location\n`internal/reset/reset_test.go`\n`internal/reset/backup_test.go`\n`internal/reset/git_test.go`","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-13T08:44:57.01739+11:00","updated_at":"2025-12-13T10:13:32.611698+11:00","closed_at":"2025-12-13T09:59:20.820314+11:00","dependencies":[{"issue_id":"bd-aydr.6","depends_on_id":"bd-aydr","type":"parent-child","created_at":"2025-12-13T08:44:57.017813+11:00","created_by":"daemon"}]} +{"id":"bd-aydr.7","title":"Add integration tests for bd reset command","description":"End-to-end integration tests for the reset command.\n\n## Test Scenarios\n\n### Basic reset\n1. Init beads, create some issues\n2. Run bd reset --force\n3. Verify .beads/ is fresh, issues gone\n\n### Hard reset\n1. Init beads, create issues, commit\n2. Run bd reset --hard --force \n3. Verify git history has reset commits\n\n### Backup functionality\n1. Init beads, create issues\n2. Run bd reset --backup --force\n3. Verify backup exists with correct contents\n4. Verify main .beads/ is reset\n\n### Dry run\n1. Init beads, create issues\n2. Run bd reset --dry-run\n3. Verify nothing changed\n\n### Confirmation prompt\n1. Init beads\n2. Run bd reset (no --force)\n3. Verify prompts for confirmation\n4. Test both y and n responses\n\n## Location\ntests/integration/reset_test.go or similar","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-13T08:44:58.479282+11:00","updated_at":"2025-12-13T06:24:29.561908-08:00","closed_at":"2025-12-13T10:15:59.221637+11:00","dependencies":[{"issue_id":"bd-aydr.7","depends_on_id":"bd-aydr","type":"parent-child","created_at":"2025-12-13T08:44:58.479686+11:00","created_by":"daemon"},{"issue_id":"bd-aydr.7","depends_on_id":"bd-aydr.4","type":"blocks","created_at":"2025-12-13T08:45:11.15972+11:00","created_by":"daemon"}]} +{"id":"bd-aydr.8","title":"Respond to GitHub issue #479 with solution","description":"Once bd reset is implemented and released, respond to GitHub issue #479.\n\n## Response should include\n- Announce the new bd reset command\n- Show basic usage examples\n- Link to any documentation\n- Thank the user for the feedback\n\n## Example response\n```\nThanks for raising this! We've added a `bd reset` command to handle this case.\n\nUsage:\n- `bd reset` - Reset to clean state (prompts for confirmation)\n- `bd reset --backup` - Create backup first\n- `bd reset --hard` - Also clean up git history\n\nThis is available in version X.Y.Z.\n```\n\n## Notes\n- Wait until feature is merged and released\n- Consider if issue should be closed or left for user confirmation","status":"closed","priority":3,"issue_type":"task","created_at":"2025-12-13T08:45:00.112351+11:00","updated_at":"2025-12-13T06:24:29.562177-08:00","closed_at":"2025-12-13T10:18:06.646796+11:00","dependencies":[{"issue_id":"bd-aydr.8","depends_on_id":"bd-aydr","type":"parent-child","created_at":"2025-12-13T08:45:00.112732+11:00","created_by":"daemon"},{"issue_id":"bd-aydr.8","depends_on_id":"bd-aydr.7","type":"blocks","created_at":"2025-12-13T08:45:12.640243+11:00","created_by":"daemon"}]} +{"id":"bd-aydr.9","title":"Add .beads-backup-* pattern to gitignore template","description":"Update the gitignore template in doctor package to include backup directories.\n\n## Change\nAdd `.beads-backup-*/` to the GitignoreTemplate in `cmd/bd/doctor/gitignore.go`\n\n## Why\nBackup directories created by `bd reset --backup` should not be committed to git.\nThey are local-only recovery tools.\n\n## File\n`cmd/bd/doctor/gitignore.go` - look for GitignoreTemplate constant","status":"closed","priority":3,"issue_type":"task","created_at":"2025-12-13T08:49:42.453483+11:00","updated_at":"2025-12-13T09:16:44.201889+11:00","closed_at":"2025-12-13T09:16:44.201889+11:00","dependencies":[{"issue_id":"bd-aydr.9","depends_on_id":"bd-aydr","type":"parent-child","created_at":"2025-12-13T08:49:42.453886+11:00","created_by":"daemon"}]} +{"id":"bd-b3og","title":"Fix TestImportBugIntegration deadlock in importer_test.go","description":"Code health review found internal/importer/importer_test.go has TestImportBugIntegration skipped with:\n\nTODO: Test hangs due to database deadlock - needs investigation\n\nThis indicates a potential unresolved concurrency issue in the importer. The test has been skipped for an unknown duration.\n\nFix: Investigate the deadlock, fix the underlying issue, and re-enable the test.","status":"closed","priority":1,"issue_type":"bug","created_at":"2025-12-16T18:17:22.103838-08:00","updated_at":"2025-12-17T23:13:40.529671-08:00","closed_at":"2025-12-17T17:25:26.645901-08:00","dependencies":[{"issue_id":"bd-b3og","depends_on_id":"bd-tggf","type":"blocks","created_at":"2025-12-16T18:19:05.740642-08:00","created_by":"daemon"}]} +{"id":"bd-b6xo","title":"Remove or fix ClearDirtyIssues() - race condition risk (bd-52)","description":"Code health review found internal/storage/sqlite/dirty.go still exposes old ClearDirtyIssues() method (lines 103-108) which clears ALL dirty issues without checking what was actually exported.\n\nData loss risk: If export fails after some issues written to JSONL but before ClearDirtyIssues called, changes to remaining dirty issues will be lost.\n\nThe safer ClearDirtyIssuesByID() (lines 113-132) exists and clears only exported issues.\n\nFix: Either remove old method or mark it deprecated and ensure no code paths use it.","status":"closed","priority":1,"issue_type":"bug","created_at":"2025-12-16T18:17:20.534625-08:00","updated_at":"2025-12-17T23:13:40.530703-08:00","closed_at":"2025-12-17T18:59:18.693791-08:00","dependencies":[{"issue_id":"bd-b6xo","depends_on_id":"bd-tggf","type":"blocks","created_at":"2025-12-16T18:19:05.633738-08:00","created_by":"daemon"}]} +{"id":"bd-bgm","title":"Fix unparam unused parameter in cmd/bd/doctor.go:1879","description":"Linting issue: checkGitHooks - path is unused (unparam) at cmd/bd/doctor.go:1879:20. Error: func checkGitHooks(path string) doctorCheck {","status":"closed","priority":0,"issue_type":"bug","created_at":"2025-12-07T15:35:25.270293252-07:00","updated_at":"2025-12-17T23:13:40.532991-08:00","closed_at":"2025-12-17T16:46:11.026693-08:00"} +{"id":"bd-bgr","title":"Test stdin 2","description":"Description from stdin test\n","status":"closed","priority":4,"issue_type":"task","created_at":"2025-12-17T17:28:05.41434-08:00","updated_at":"2025-12-17T17:28:33.833288-08:00","closed_at":"2025-12-17T17:28:33.833288-08:00"} +{"id":"bd-bw6","title":"Fix G104 errors unhandled in internal/storage/sqlite/queries.go:1181","description":"Linting issue: G104: Errors unhandled (gosec) at internal/storage/sqlite/queries.go:1181:4. Error: rows.Close()","status":"closed","priority":0,"issue_type":"bug","created_at":"2025-12-07T15:35:09.008444133-07:00","updated_at":"2025-12-17T23:13:40.536627-08:00","closed_at":"2025-12-17T16:46:11.029355-08:00"} +{"id":"bd-bwk2","title":"Centralize error handling patterns in storage layer","description":"80+ instances of inconsistent error handling across sqlite.go with mix of %w, %v, and no wrapping.\n\nLocation: internal/storage/sqlite/sqlite.go (throughout)\n\nProblem:\n- Some use fmt.Errorf(\"op failed: %w\", err) - correct wrapping\n- Some use fmt.Errorf(\"op failed: %v\", err) - loses error chain\n- Some return err directly - no context\n- Hard to debug production issues\n- Can't distinguish error types\n\nSolution: Create internal/storage/sqlite/errors.go:\n- Define sentinel errors (ErrNotFound, ErrInvalidID, etc.)\n- Create wrapDBError(op string, err error) helper\n- Convert sql.ErrNoRows to ErrNotFound\n- Always wrap with operation context\n\nImpact: Lost error context; inconsistent messages; hard to debug\n\nEffort: 5-7 hours","status":"open","priority":1,"issue_type":"task","created_at":"2025-11-16T14:51:54.974909-08:00","updated_at":"2025-11-16T14:51:54.974909-08:00"} +{"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-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"} +{"id":"bd-d73u","title":"Re: Thread Test 2","description":"Great! Thread is working well.","status":"tombstone","priority":2,"issue_type":"message","created_at":"2025-12-16T18:21:46.655093-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":"message"} +{"id":"bd-de6","title":"Fix FindBeadsDir to prioritize main repo .beads for worktrees","description":"The FindBeadsDir function should prioritize finding .beads in the main repository root when accessed from a worktree, rather than finding worktree-local .beads directories. This ensures proper sharing of the database across all worktrees.","status":"open","priority":2,"issue_type":"bug","created_at":"2025-12-07T16:48:36.883117467-07:00","updated_at":"2025-12-07T16:48:36.883117467-07:00"} +{"id":"bd-dhza","title":"Reduce global state in cmd/bd/main.go (25+ variables)","description":"Code health review found main.go has 25+ global variables (lines 57-112):\n\n- dbPath, actor, store, jsonOutput, daemonClient, noDaemon\n- rootCtx, rootCancel, autoFlushEnabled\n- isDirty (marked 'USED BY LEGACY CODE')\n- needsFullExport (marked 'USED BY LEGACY CODE')\n- flushTimer (marked 'DEPRECATED')\n- flushMutex, storeMutex, storeActive\n- flushFailureCount, lastFlushError, flushManager\n- skipFinalFlush, autoImportEnabled\n- versionUpgradeDetected, previousVersion, upgradeAcknowledged\n\nImpact:\n- Hard to test individual commands\n- Race conditions possible\n- State leakage between commands\n\nFix: Move toward dependency injection. Remove deprecated variables. Consider cmd/bd/internal package.","status":"open","priority":3,"issue_type":"task","created_at":"2025-12-16T18:17:29.643293-08:00","updated_at":"2025-12-16T18:17:29.643293-08:00","dependencies":[{"issue_id":"bd-dhza","depends_on_id":"bd-tggf","type":"blocks","created_at":"2025-12-16T18:19:06.492112-08:00","created_by":"daemon"}]} +{"id":"bd-dp4w","title":"Test message","description":"This is a test message body","status":"tombstone","priority":2,"issue_type":"message","created_at":"2025-12-16T18:11:58.467876-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":"message"} +{"id":"bd-dsdh","title":"Document sync.branch 'always dirty' working tree behavior","description":"## Context\n\nWhen sync.branch is configured, the .beads/issues.jsonl file in main's working tree is ALWAYS dirty. This is by design:\n\n1. bd sync commits to beads-sync branch (via worktree)\n2. bd sync copies JSONL to main's working tree (so CLI commands work)\n3. This copy is NOT committed to main (to reduce commit noise)\n\nContributors who watch main branch history pushed for sync.branch to avoid constant beads commit noise. But users need to understand the trade-off.\n\n## Documentation Needed\n\nUpdate README.md sync.branch section with:\n\n1. **Clear explanation** of why .beads/ is always dirty on main\n2. **\"Be Zen about it\"** - this is expected, not a bug\n3. **Workflow options:**\n - Accept dirty state, use `bd sync --merge` periodically to snapshot to main\n - Or disable sync.branch if clean working tree is more important\n4. **Shell alias tip** to hide beads from git status:\n ```bash\n alias gs='git status -- \":!.beads/\"'\n ```\n5. **When to merge**: releases, milestones, or periodic snapshots\n\n## Related\n\n- bd-7b7h: Fix that allows bd sync --merge to work with dirty .beads/\n- bd-elqd: Investigation that identified this as expected behavior","status":"tombstone","priority":2,"issue_type":"task","created_at":"2025-12-16T23:16:12.253559-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-dsp","title":"Test stdin body-file","description":"","status":"closed","priority":4,"issue_type":"task","created_at":"2025-12-17T17:27:32.098806-08:00","updated_at":"2025-12-17T17:28:33.832749-08:00","closed_at":"2025-12-17T17:28:33.832749-08:00"} +{"id":"bd-dwh","title":"Implement or remove ExpectExit/ExpectStdout verification fields","description":"The Verification struct in internal/types/workflow.go has ExpectExit and ExpectStdout fields that are never used by workflowVerifyCmd. Either implement the functionality or remove the dead fields.","status":"closed","priority":3,"issue_type":"task","created_at":"2025-12-17T22:23:02.708627-08:00","updated_at":"2025-12-17T22:34:07.300348-08:00","closed_at":"2025-12-17T22:34:07.300348-08:00"} +{"id":"bd-dyy","title":"Review PR #513: fix hooks install docs","description":"Review and merge PR #513 from aspiers. This PR fixes incorrect docs for how to install git hooks - updates README to use bd hooks install instead of removed install.sh. Simple 1-line change. URL: https://github.com/anthropics/beads/pull/513","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-13T08:15:14.838772+11:00","updated_at":"2025-12-13T07:07:19.718544-08:00","closed_at":"2025-12-13T07:07:19.718544-08:00"} +{"id":"bd-e1085716","title":"bd validate - Comprehensive health check","description":"Run all validation checks in one command.\n\nChecks:\n- Duplicates\n- Orphaned dependencies\n- Test pollution\n- Git conflicts\n\nSupports --fix-all for auto-repair.\n\nDepends on bd-cbed9619.1, bd-0dcea000, bd-31aab707, bd-9826b69a.\n\nFiles: cmd/bd/validate.go (new)","status":"closed","priority":1,"issue_type":"task","created_at":"2025-10-29T23:05:13.980679-07:00","updated_at":"2025-12-17T22:58:34.562008-08:00","closed_at":"2025-12-17T22:58:34.562008-08:00","close_reason":"Closed"} +{"id":"bd-elqd","title":"Systematic bd sync stability investigation","description":"## Context\n\nbd sync has chronic instability issues that have persisted since inception:\n- issues.jsonl is always dirty after push\n- bd sync often creates messes requiring manual cleanup\n- Problems escalating despite accumulated bug fixes\n- Workarounds are getting increasingly draconian\n\n## Goal\n\nSystematically observe and diagnose bd sync failures rather than applying band-aid fixes.\n\n## Approach\n\n1. Start fresh session with latest binary (all fixes applied)\n2. Run bd sync and carefully observe what happens\n3. Document exact sequence of events when things go wrong\n4. File specific issues for each discrete problem identified\n5. Track the root causes, not just symptoms\n\n## Test Environment\n\n- Fresh clone or clean state\n- Latest bd binary with all bug fixes\n- Monitor both local and remote JSONL state\n- Check for timing issues, race conditions, merge conflicts\n\n## Success Criteria\n\n- Identify root causes of sync instability\n- Create actionable issues for each problem\n- Eventually achieve stable bd sync (no manual intervention needed)","status":"tombstone","priority":1,"issue_type":"task","created_at":"2025-12-16T22:57:25.35289-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-eyto","title":"Time-dependent tests may be flaky near TTL boundary","description":"Several tombstone merge tests use time.Now() to create test data: time.Now().Add(-24 * time.Hour), time.Now().Add(-60 * 24 * time.Hour), etc. While these work reliably in practice (24h vs 30d TTL has large margin), they could theoretically be flaky if: 1) Tests run slowly, 2) System clock changes during test, 3) TTL constants change. Recommendation: Consider using a fixed reference time or time injection for deterministic tests. Lower priority since current margin is large. Files: internal/merge/merge_test.go:1337-1338, 1352-1353, 1548-1549, 1590-1591","status":"open","priority":3,"issue_type":"task","created_at":"2025-12-05T16:37:02.348143-08:00","updated_at":"2025-12-05T16:37:02.348143-08:00"} +{"id":"bd-f5cc","title":"Thread Test","description":"Testing the thread feature","status":"tombstone","priority":2,"issue_type":"message","created_at":"2025-12-16T18:21:01.244501-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":"message"} +{"id":"bd-fi05","title":"bd sync fails with orphaned issues and duplicate ID conflict","description":"After fixing the deleted_at TEXT column scanning bug (commit 18b1eb2), bd sync still fails with two issues:\n\n1. Orphan Detection Warning: 12 orphaned child issues whose parents no longer exist (bd-cb64c226.* and bd-cbed9619.*)\n\n2. Import Failure: UNIQUE constraint failed for bd-360 - this tombstone exists in both DB and JSONL\n\nError: \"Import failed: error creating depth-0 issues: bulk insert issues: failed to insert issue bd-360: sqlite3: constraint failed: UNIQUE constraint failed: issues.id\"\n\nFix options:\n- Delete orphaned child issues with bd delete\n- Resolve bd-360 duplicate (in deletions.jsonl vs tombstone in DB)\n- Reset sync branch: git branch -f beads-sync main \u0026\u0026 git push --force-with-lease origin beads-sync","notes":"Fixed tombstone constraint violation bug. When deleting closed issues, the CHECK constraint (status = 'closed') = (closed_at IS NOT NULL) was violated because CreateTombstone didn't clear closed_at. Fix: set closed_at = NULL in tombstone creation SQL.\n\nThe sync data corruption (orphaned issues in beads-sync branch) requires manual cleanup: reset sync branch with 'git branch -f beads-sync main \u0026\u0026 git push --force-with-lease origin beads-sync'","status":"closed","priority":2,"issue_type":"bug","created_at":"2025-12-13T07:14:33.831346-08:00","updated_at":"2025-12-13T10:50:48.545465-08:00","closed_at":"2025-12-13T07:30:33.843986-08:00"} +{"id":"bd-fom","title":"Remove all deletions.jsonl code except migration","description":"There's deletions manifest code spread across the entire codebase that should have been removed after tombstone migration:\n\nFiles with deletions code (non-migration):\n- internal/deletions/ - entire package\n- cmd/bd/sync.go - 25+ references, auto-compact, sanitize\n- cmd/bd/delete.go - dual-writes to deletions.jsonl\n- internal/importer/importer.go - checks deletions manifest\n- internal/syncbranch/worktree.go - merges deletions.jsonl\n- cmd/bd/doctor/fix/sync.go - cleanupDeletionsManifest\n- cmd/bd/doctor/fix/deletions.go - HydrateDeletionsManifest\n- cmd/bd/integrity.go - checks deletions for data loss\n- cmd/bd/deleted.go - entire command\n- cmd/bd/compact.go - pruneDeletionsManifest\n- cmd/bd/doctor.go - checkDeletionsManifest\n- Plus many more\n\nAction: Aggressively remove all non-migration deletions code. Tombstones are the only deletion mechanism now.","status":"tombstone","priority":1,"issue_type":"task","created_at":"2025-12-16T13:29:04.960863-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-fu83","title":"Fix daemon/direct mode inconsistency in relate and duplicate commands","description":"The relate.go and duplicate.go commands have inconsistent daemon/direct mode handling:\n\nWhen daemonClient is connected, they resolve IDs via RPC but then perform updates directly via store.UpdateIssue(), bypassing the daemon.\n\nAffected locations:\n- relate.go:125-139 (runRelate update)\n- relate.go:235-246 (runUnrelate update) \n- duplicate.go:120 (runDuplicate update)\n- duplicate.go:207 (runSupersede update)\n\nShould either use RPC for updates when daemon is running, or document why direct access is intentional.","status":"open","priority":2,"issue_type":"bug","created_at":"2025-12-16T20:52:54.164189-08:00","updated_at":"2025-12-16T20:52:54.164189-08:00"} +{"id":"bd-fx7v","title":"Improve test coverage for cmd/bd/doctor/fix (23.9% → 50%)","description":"The doctor/fix package has only 23.9% test coverage. The doctor fix functionality is important for troubleshooting.\n\nCurrent coverage: 23.9%\nTarget coverage: 50%","status":"open","priority":2,"issue_type":"task","created_at":"2025-12-13T20:43:05.67127-08:00","updated_at":"2025-12-13T21:01:20.839298-08:00"} +{"id":"bd-g9eu","title":"Investigate TestRoutingIntegration failure","description":"TestRoutingIntegration/maintainer_with_SSH_remote failed during pre-commit check with \"expected role maintainer, got contributor\".\nThis occurred while running `go test -short ./...` on darwin/arm64.\nThe failure appears unrelated to storage/sqlite changes.\nNeed to investigate if this is a flaky test or environmental issue.","status":"open","priority":2,"issue_type":"task","created_at":"2025-11-20T15:55:19.337094-08:00","updated_at":"2025-11-20T15:55:19.337094-08:00"} +{"id":"bd-gjla","title":"Test Thread","description":"Initial message for threading test","status":"tombstone","priority":2,"issue_type":"message","created_at":"2025-12-16T18:19:51.704324-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":"message"} +{"id":"bd-hkr6","title":"GH#518: Document bd setup command","description":"bd setup is undiscoverable. Add to README/docs. Currently only findable by grepping source. See GitHub issue #518.","status":"tombstone","priority":2,"issue_type":"task","created_at":"2025-12-16T01:03:54.664668-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-hlsw","title":"Add sync resilience guardrails for forced pushes and prefix mismatches","description":"Beads can get into unrecoverable sync states when remote forces pushes occur (e.g., rebases) combined with prefix mismatches from multi-worker scenarios. Add detection, prevention, and auto-recovery features to handle this gracefully.","status":"open","priority":2,"issue_type":"epic","created_at":"2025-12-14T10:40:14.872875259-07:00","updated_at":"2025-12-14T10:40:14.872875259-07:00"} +{"id":"bd-hlsw.3","title":"Auto-recovery mode (bd sync --auto-recover)","description":"Add bd sync --auto-recover flag that: detects problematic sync state, backs up .beads/issues.db with timestamp, rebuilds DB from JSONL atomically, verifies consistency, reports what was fixed. Provides safety valve when sync integrity fails.","status":"open","priority":3,"issue_type":"task","created_at":"2025-12-14T10:40:20.599836875-07:00","updated_at":"2025-12-14T10:40:20.599836875-07:00","dependencies":[{"issue_id":"bd-hlsw.3","depends_on_id":"bd-hlsw","type":"parent-child","created_at":"2025-12-14T10:40:20.600435888-07:00","created_by":"daemon"}]} +{"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-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-0zp7","title":"Add missing hook calls in mail reply and ack","description":"The mail commands are missing hook calls:\n\n1. runMailReply (mail.go:525-672) creates a message but doesn't call hookRunner.Run(hooks.EventMessage, ...) after creating the reply in direct mode (around line 640)\n\n2. runMailAck (mail.go:432-523) closes messages but doesn't call hookRunner.Run(hooks.EventClose, ...) after closing each message (around line 487 for daemon mode, 493 for direct mode)\n\nThis means GGT hooks won't fire for replies or message acknowledgments.","status":"open","priority":1,"issue_type":"bug","created_at":"2025-12-16T20:52:53.069412-08:00","updated_at":"2025-12-16T20:52:53.069412-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-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"} +{"id":"bd-kwro.11","title":"Documentation for messaging and graph links","description":"Document all new features.\n\nFiles to update:\n- README.md - brief mention of messaging capability\n- AGENTS.md - update for AI agents using bd mail\n- docs/messaging.md (new) - full messaging reference\n- docs/graph-links.md (new) - graph link reference\n- CHANGELOG.md - v0.30.2 release notes\n\nTopics to cover:\n- Mail commands with examples\n- Graph link types and use cases\n- Identity configuration\n- Hooks setup for notifications\n- Migration notes","status":"tombstone","priority":2,"issue_type":"task","created_at":"2025-12-16T03:02:39.548518-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.2","title":"Graph Link: replies_to for conversation threading","description":"Implement replies_to link type for message threading.\n\nNew command:\n- bd mail reply \u003cid\u003e -m 'Response' creates a message with replies_to set\n\nQuery support:\n- bd show \u003cid\u003e --thread shows full conversation thread\n- Thread traversal in storage layer\n\nStorage:\n- replies_to column in issues table\n- Index for efficient thread queries\n\nThis enables Reddit-style nested threads where messages reply to other messages.","status":"tombstone","priority":1,"issue_type":"task","created_at":"2025-12-16T03:01:25.292728-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.3","title":"Graph Link: relates_to for knowledge graph","description":"Implement relates_to link type for loose associations.\n\nNew command:\n- bd relate \u003cid1\u003e \u003cid2\u003e - creates bidirectional relates_to link\n\nQuery support:\n- bd show \u003cid\u003e --related shows related issues\n- bd list --related-to \u003cid\u003e\n\nStorage:\n- relates_to stored as JSON array of issue IDs\n- Consider: separate junction table for efficiency at scale?\n\nThis enables 'see also' connections without blocking or hierarchy.","status":"tombstone","priority":1,"issue_type":"task","created_at":"2025-12-16T03:01:30.793115-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.4","title":"Graph Link: duplicates for deduplication","description":"Implement duplicates link type for marking issues as duplicates.\n\nNew command:\n- bd duplicate \u003cid\u003e --of \u003ccanonical\u003e - marks id as duplicate of canonical\n- Auto-closes the duplicate issue\n\nQuery support:\n- bd show \u003cid\u003e shows 'Duplicate of: \u003ccanonical\u003e'\n- bd list --duplicates shows all duplicate pairs\n\nStorage:\n- duplicates column pointing to canonical issue ID\n\nEssential for large issue databases with many similar reports.","status":"tombstone","priority":1,"issue_type":"task","created_at":"2025-12-16T03:01:36.257223-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.5","title":"Graph Link: supersedes for version chains","description":"Implement supersedes link type for version tracking.\n\nNew command:\n- bd supersede \u003cid\u003e --with \u003cnew\u003e - marks id as superseded by new\n- Auto-closes the superseded issue\n\nQuery support:\n- bd show \u003cid\u003e shows 'Superseded by: \u003cnew\u003e'\n- bd show \u003cnew\u003e shows 'Supersedes: \u003cid\u003e'\n- bd list --superseded shows version chains\n\nStorage:\n- superseded_by column pointing to replacement issue\n\nUseful for design docs, specs, and evolving artifacts.","status":"tombstone","priority":1,"issue_type":"task","created_at":"2025-12-16T03:01:41.749294-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.6","title":"Mail Commands: bd mail send/inbox/read/ack","description":"Implement core mail commands in cmd/bd/mail.go\n\nCommands:\n- bd mail send \u003crecipient\u003e -s 'Subject' -m 'Body' [--urgent]\n - Creates issue with type=message, sender=identity, assignee=recipient\n - --urgent sets priority=0\n \n- bd mail inbox [--from \u003csender\u003e] [--priority \u003cn\u003e]\n - Lists open messages where assignee=my identity\n - Sorted by priority, then date\n \n- bd mail read \u003cid\u003e\n - Shows full message content (subject, body, sender, timestamp)\n - Does NOT close (separate from ack)\n \n- bd mail ack \u003cid\u003e\n - Marks message as read by closing it\n - Can ack multiple: bd mail ack \u003cid1\u003e \u003cid2\u003e ...\n\nRequires: Identity configuration (bd-kwro.7)","status":"tombstone","priority":0,"issue_type":"task","created_at":"2025-12-16T03:02:12.103755-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.7","title":"Identity Configuration","description":"Implement identity system for sender field.\n\nConfiguration sources (in priority order):\n1. --identity flag on commands\n2. BEADS_IDENTITY environment variable\n3. .beads/config.json: {\"identity\": \"worker-name\"}\n4. Default: git user.name or hostname\n\nNew config file support:\n- .beads/config.json for per-repo settings\n- identity field for messaging\n\nHelper function:\n- GetIdentity() string - resolves identity from sources\n\nUpdate bd mail send to use GetIdentity() for sender field.","status":"tombstone","priority":1,"issue_type":"task","created_at":"2025-12-16T03:02:17.603608-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.8","title":"Hooks System","description":"Implement hook system for extensibility.\n\nHook directory: .beads/hooks/\nHook files (executable scripts):\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\nHook invocation:\n- Pass issue ID as first argument\n- Pass event type as second argument\n- Pass JSON issue data on stdin\n- Run asynchronously (dont block command)\n\nExample hook (GGT notification):\n #!/bin/bash\n gt notify --event=$2 --issue=$1\n\nThis allows GGT to register notification handlers without Beads knowing about GGT.","status":"tombstone","priority":1,"issue_type":"task","created_at":"2025-12-16T03:02:23.086393-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.9","title":"Cleanup: --ephemeral flag","description":"Update bd cleanup to handle ephemeral issues.\n\nNew flag:\n- bd cleanup --ephemeral - deletes all CLOSED issues with ephemeral=true\n\nBehavior:\n- Only deletes if status=closed AND ephemeral=true\n- Respects --dry-run flag\n- Reports count of deleted ephemeral issues\n\nThis allows swarm cleanup to remove transient messages without affecting permanent issues.","status":"tombstone","priority":2,"issue_type":"task","created_at":"2025-12-16T03:02:28.563871-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-lfak","title":"bd preflight: PR readiness checks for contributors","description":"## Vision\n\nEncode project-specific institutional knowledge into executable checks. CONTRIBUTING.md is documentation that's read once and forgotten; `bd preflight` is documentation that runs at exactly the right moment.\n\n## Problem Statement\n\nContributors face a \"last mile\" problem - they do the work but stumble on project-specific gotchas at PR time:\n- Nix vendorHash gets stale when go.sum changes\n- Beads artifacts leak into PRs (see bd-umbf for namespace solution)\n- Version mismatches between version.go and default.nix\n- Tests/lint not run locally before pushing\n- Other project-specific checks that only surface when CI fails\n\nThese are too obscure to remember, exist in docs nobody reads end-to-end, and waste CI round-trips.\n\n## Why beads?\n\nBeads already has a foothold in the contributor workflow. It knows:\n- Git state (staged files, branch, dirty status)\n- Project structure\n- The specific issue being worked on\n- Project-specific configuration\n\n## Proposed Interface\n\n### Tier 1: Checklist Mode (v1)\n\n $ bd preflight\n PR Readiness Checklist:\n\n [ ] Tests pass: go test -short ./...\n [ ] Lint passes: golangci-lint run ./...\n [ ] No beads pollution: check .beads/issues.jsonl diff\n [ ] Nix hash current: go.sum unchanged or vendorHash updated\n [ ] Version sync: version.go matches default.nix\n\n Run 'bd preflight --check' to validate automatically.\n\n### Tier 2: Check Mode (v2)\n\n $ bd preflight --check\n āœ“ Tests pass\n āœ“ Lint passes\n ⚠ Beads pollution: 3 issues in diff - are these project issues or personal?\n āœ— Nix hash stale: go.sum changed, vendorHash needs update\n Fix: sha256-KRR6dXzsSw8OmEHGBEVDBOoIgfoZ2p0541T9ayjGHlI=\n āœ“ Version sync\n\n 1 error, 1 warning. Run 'bd preflight --fix' to auto-fix where possible.\n\n### Tier 3: Fix Mode (v3)\n\n $ bd preflight --fix\n āœ“ Updated vendorHash in default.nix\n ⚠ Cannot auto-fix beads pollution - manual review needed\n\n## Checks to Implement\n\n| Check | Description | Auto-fixable |\n|-------|-------------|--------------|\n| tests | Run go test -short ./... | No |\n| lint | Run golangci-lint | Partial (gofmt) |\n| beads-pollution | Detect personal issues in diff | No (see bd-umbf) |\n| nix-hash | Detect stale vendorHash | Yes (if nix available) |\n| version-sync | version.go matches default.nix | Yes |\n| no-debug | No TODO/FIXME/console.log | Warn only |\n| clean-stage | No unintended files staged | Warn only |\n\n## Future: Configuration\n\nMake checks configurable per-project via .beads/preflight.yaml:\n\n preflight:\n checks:\n - name: tests\n run: go test -short ./...\n required: true\n - name: no-secrets\n pattern: \"**/*.env\"\n staged: deny\n - name: custom-check\n run: ./scripts/validate.sh\n\nThis lets any project using beads define their own preflight checks.\n\n## Implementation Phases\n\n### Phase 1: Static Checklist\n- Implement bd preflight with hardcoded checklist for beads\n- No execution, just prints what to check\n- Update CONTRIBUTING.md to reference it\n\n### Phase 2: Automated Checks\n- Implement bd preflight --check\n- Run tests, lint, detect stale hashes\n- Clear pass/fail/warn output\n\n### Phase 3: Auto-fix\n- Implement bd preflight --fix\n- Fix vendorHash, version sync\n- Integrate with bd-umbf solution for pollution\n\n### Phase 4: Configuration\n- .beads/preflight.yaml support\n- Make it useful for other projects using beads\n- Plugin/hook system for custom checks\n\n## Dependencies\n\n- bd-umbf: Namespace isolation for beads pollution (blocking for full solution)\n\n## Success Metrics\n\n- Fewer CI failures on first PR push\n- Reduced \"fix nix hash\" commits\n- Contributors report preflight caught issues before CI","status":"open","priority":2,"issue_type":"epic","created_at":"2025-12-13T18:01:39.587078-08:00","updated_at":"2025-12-13T18:01:39.587078-08:00","dependencies":[{"issue_id":"bd-lfak","depends_on_id":"bd-umbf","type":"blocks","created_at":"2025-12-13T18:01:46.059901-08:00","created_by":"daemon"}]} +{"id":"bd-llfl","title":"Improve test coverage for cmd/bd CLI (26.2% → 50%)","description":"The main CLI package (cmd/bd) has only 26.2% test coverage. CLI commands should have at least 50% coverage to ensure reliability.\n\nKey areas with low/no coverage:\n- daemon_autostart.go (multiple 0% functions)\n- compact.go (several 0% functions)\n- Various command handlers\n\nCurrent coverage: 26.2%\nTarget coverage: 50%","status":"open","priority":2,"issue_type":"task","created_at":"2025-12-13T20:43:03.123341-08:00","updated_at":"2025-12-13T21:01:18.901944-08:00"} +{"id":"bd-lsv4","title":"GH#444: Fix inconsistent status naming in_progress vs in-progress","description":"Documentation uses in-progress (hyphen) but code expects in_progress (underscore). Update all docs to use canonical in_progress. See GitHub issue #444.","status":"tombstone","priority":2,"issue_type":"bug","created_at":"2025-12-16T01:03:14.349425-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-lw0x","title":"Fix bd sync race condition with daemon causing dirty working directory","description":"After bd sync completes with sync.branch mode, subsequent bd commands or daemon file watcher would see a hash mismatch and trigger auto-import, which then schedules re-export, dirtying the working directory.\n\n**Root cause:**\n1. bd sync exports JSONL with NEW content (hash H1)\n2. bd sync updates jsonl_content_hash = H1 in DB\n3. bd sync restores JSONL from HEAD (OLD content, hash H0)\n4. Now: file hash = H0, DB hash = H1 (MISMATCH)\n5. Daemon or next CLI command sees mismatch, imports from OLD JSONL\n6. Import triggers re-export → file is dirty\n\n**Fix:**\nAfter restoreBeadsDirFromBranch(), update jsonl_content_hash to match the restored file's hash. This ensures daemon and CLI see file hash = DB hash → no spurious import/export cycle.\n\nRelated: bd-c83r (multiple daemon prevention)","status":"closed","priority":2,"issue_type":"bug","created_at":"2025-12-13T06:42:17.130839-08:00","updated_at":"2025-12-13T06:43:33.329042-08:00","closed_at":"2025-12-13T06:43:33.329042-08:00"} +{"id":"bd-lxzx","title":"Add close_reason to JSONL export format documentation","description":"PR #551 now persists close_reason to the database, but there's a question about whether this field should be exported to JSONL format.\n\n## Current State\n- close_reason is stored in issues.close_reason column\n- close_reason is also stored in events table (audit trail)\n- The JSONL export format may or may not include close_reason\n\n## Questions\n1. Should close_reason be exported to JSONL format?\n2. If yes, where should it go (root level or nested in events)?\n3. Should there be any special handling to avoid duplication?\n4. How should close_reason be handled during JSONL import?\n\n## Why This Matters\n- JSONL is the git-friendly sync format\n- Other beads instances import from JSONL\n- close_reason is meaningful data that should be preserved across clones\n\n## Suggested Action\n- Check if close_reason is currently exported in JSONL\n- If not, add it to the export schema\n- Document the field in JSONL format spec\n- Add tests for round-trip (export -\u003e import -\u003e verify close_reason)","status":"open","priority":2,"issue_type":"task","created_at":"2025-12-14T14:25:17.414916-08:00","updated_at":"2025-12-14T14:25:17.414916-08:00","dependencies":[{"issue_id":"bd-lxzx","depends_on_id":"bd-z86n","type":"discovered-from","created_at":"2025-12-14T14:25:17.416131-08:00","created_by":"stevey"}]} +{"id":"bd-m8ro","title":"Improve test coverage for internal/rpc (47.5% → 60%)","description":"The RPC package has only 47.5% test coverage. RPC is the communication layer for daemon operations.\n\nCurrent coverage: 47.5%\nTarget coverage: 60%","status":"open","priority":2,"issue_type":"task","created_at":"2025-12-13T20:43:09.515299-08:00","updated_at":"2025-12-13T21:01:17.17404-08:00"} +{"id":"bd-mql4","title":"getLocalSyncBranch silently ignores YAML parse errors","description":"In autoimport.go:170-172, YAML parsing errors are silently ignored. If a user has malformed YAML in config.yaml, sync-branch will just silently be empty with no feedback.\n\nRecommendation: Add debug logging since this function is only called during auto-import, and debugging silent failures is painful.\n\nAdd: debug.Logf(\"Warning: failed to parse config.yaml: %v\", err)","status":"open","priority":4,"issue_type":"task","created_at":"2025-12-07T02:03:44.217728-08:00","updated_at":"2025-12-07T02:03:44.217728-08:00"} +{"id":"bd-muw","title":"Add empty tasks validation in workflow create","description":"workflow.go:321 will panic if wf.Tasks is empty. Add validation that len(wf.Tasks) \u003e 0 before accessing wf.Tasks[0].","status":"closed","priority":3,"issue_type":"bug","created_at":"2025-12-17T22:23:00.75707-08:00","updated_at":"2025-12-17T22:34:07.281133-08:00","closed_at":"2025-12-17T22:34:07.281133-08:00"} +{"id":"bd-n386","title":"Improve test coverage for internal/daemon (27.3% → 60%)","description":"The daemon package has only 27.3% test coverage. The daemon is critical for background operations and reliability.\n\nKey areas needing tests:\n- Daemon autostart logic\n- Socket handling\n- PID file management\n- Health checks\n\nCurrent coverage: 27.3%\nTarget coverage: 60%","status":"open","priority":2,"issue_type":"task","created_at":"2025-12-13T20:43:00.895238-08:00","updated_at":"2025-12-13T21:01:17.274438-08:00"} +{"id":"bd-n3v","title":"Error committing to sync branch: failed to create worktree","description":"\u003e bd sync --no-daemon\n→ Exporting pending changes to JSONL...\n→ Committing changes to sync branch 'beads-sync'...\nError committing to sync branch: failed to create worktree: failed to create worktree parent directory: mkdir /var/home/matt/dev/beads/fix-ci/.git: not a directory","notes":"**Problem Diagnosed**: The `bd sync` command was failing with \"mkdir /var/home/matt/dev/beads/fix-ci/.git: not a directory\" because it was being executed from the wrong directory.\n\n**Root Cause**: The command was run from `/var/home/matt/dev/beads` (where the `fix-ci` worktree exists) instead of the main repository directory `/var/home/matt/dev/beads/main`. Since `fix-ci` is a git worktree with a `.git` file (not directory), the worktree creation logic failed when trying to create `\u003ccurrent_dir\u003e/.git/beads-worktrees/\u003cbranch\u003e`.\n\n**Solution Verified**: Execute `bd sync` from the main repository directory:\n```bash\ncd main \u0026\u0026 bd sync --dry-run\n```\n","status":"open","priority":2,"issue_type":"task","created_at":"2025-12-05T15:25:24.514998248-07:00","updated_at":"2025-12-05T15:42:32.910166956-07:00"} +{"id":"bd-n4td","title":"Add warning when staleness check errors","description":"## Problem\n\nWhen ensureDatabaseFresh() calls CheckStaleness() and it errors (corrupted metadata, permission issues, etc.), we silently proceed with potentially stale data.\n\n**Location:** cmd/bd/staleness.go:27-32\n\n**Scenarios:**\n- Corrupted metadata table\n- Database locked by another process \n- Permission issues reading JSONL file\n- Invalid last_import_time format in DB\n\n## Current Code\n\n```go\nisStale, err := autoimport.CheckStaleness(ctx, store, dbPath)\nif err \\!= nil {\n // If we can't determine staleness, allow operation to proceed\n // (better to show potentially stale data than block user)\n return nil\n}\n```\n\n## Fix\n\n```go\nisStale, err := autoimport.CheckStaleness(ctx, store, dbPath)\nif err \\!= nil {\n fmt.Fprintf(os.Stderr, \"Warning: Could not verify database freshness: %v\\n\", err)\n fmt.Fprintf(os.Stderr, \"Proceeding anyway. Data may be stale.\\n\\n\")\n return nil\n}\n```\n\n## Impact\nMedium - users should know when staleness check fails\n\n## Effort\nEasy - 5 minutes","status":"closed","priority":2,"issue_type":"bug","created_at":"2025-11-20T20:16:34.889997-05:00","updated_at":"2025-12-17T23:13:40.531031-08:00","closed_at":"2025-12-17T19:11:12.950618-08:00","dependencies":[{"issue_id":"bd-n4td","depends_on_id":"bd-2q6d","type":"blocks","created_at":"2025-11-20T20:18:20.154723-05:00","created_by":"stevey"}]} +{"id":"bd-ncwo","title":"Ghost resurrection: remote status:closed wins during git merge","description":"During bd sync, the 3-way git merge sometimes keeps remote's status:closed instead of local's status:tombstone. This causes ghost issues to resurrect even when tombstones exist.\n\nRoot cause: Git 3-way merge doesn't understand tombstone semantics. If base had closed, local changed to tombstone, and remote has closed, git might keep remote's version.\n\nThe early tombstone check in importer.go only prevents CREATION when tombstones exist in DB. But if applyDeletionsFromMerge hard-deletes the tombstones before import runs (because they're not in the merged result), the check doesn't help.\n\nPotential fixes:\n1. Make tombstones 'win' in the beads merge driver (internal/merge/merge.go)\n2. Don't hard-delete tombstones in applyDeletionsFromMerge if they're in the DB\n3. Export tombstones to a separate file that's not subject to merge\n\nGhost issues: bd-cb64c226.*, bd-cbed9619.*","status":"tombstone","priority":1,"issue_type":"bug","created_at":"2025-12-16T22:01:03.56423-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-nl2","title":"No logging/debugging for tombstone resurrection events","description":"Per the design document bd-zvg Open Question 1: Should resurrection log a warning? Recommendation was Yes. Currently, when an expired tombstone loses to a live issue (resurrection), there is no logging or debugging output. This makes it hard to understand why an issue reappeared. Recommendation: Add optional debug logging when resurrection occurs, e.g., Issue bd-abc resurrected (tombstone expired). Files: internal/merge/merge.go:359-366, 371-378, 400-405, 410-415","status":"open","priority":4,"issue_type":"feature","created_at":"2025-12-05T16:36:52.27525-08:00","updated_at":"2025-12-05T16:36:52.27525-08:00"} +{"id":"bd-nuh1","title":"GH#403: bd doctor --fix circular error message","description":"bd doctor --fix suggests running bd doctor --fix for deletions manifest issue. Fix to provide actual resolution. See GitHub issue #403.","status":"tombstone","priority":2,"issue_type":"bug","created_at":"2025-12-16T01:03:16.290018-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-o4qy","title":"Improve CheckStaleness error handling","description":"## Problem\n\nCheckStaleness returns 'false' (not stale) for multiple error conditions instead of returning errors. This masks problems.\n\n**Location:** internal/autoimport/autoimport.go:253-285\n\n## Edge Cases That Return False\n\n1. **Invalid last_import_time format** (line 259-262)\n2. **No JSONL file found** (line 267-277) \n3. **JSONL stat fails** (line 279-282)\n\n## Fix\n\nReturn errors for abnormal conditions:\n\n```go\nlastImportTime, err := time.Parse(time.RFC3339, lastImportStr)\nif err != nil {\n return false, fmt.Errorf(\"corrupted last_import_time: %w\", err)\n}\n\nif jsonlPath == \"\" {\n return false, fmt.Errorf(\"no JSONL file found\")\n}\n\nstat, err := os.Stat(jsonlPath)\nif err != nil {\n return false, fmt.Errorf(\"cannot stat JSONL: %w\", err)\n}\n```\n\n## Impact\nMedium - edge cases are rare but should be handled\n\n## Effort \n30 minutes - requires updating callers in RPC server","status":"closed","priority":2,"issue_type":"bug","created_at":"2025-11-20T20:17:27.606219-05:00","updated_at":"2025-12-17T23:13:40.536905-08:00","closed_at":"2025-12-17T19:11:12.965289-08:00","dependencies":[{"issue_id":"bd-o4qy","depends_on_id":"bd-2q6d","type":"blocks","created_at":"2025-11-20T20:18:26.81065-05:00","created_by":"stevey"}]} +{"id":"bd-o55a","title":"GH#509: bd doesn't find .beads when running from nested worktrees","description":"When worktrees are nested under main repo (.worktrees/feature/), bd stops at worktree git root instead of continuing to find .beads in parent. See GitHub issue #509 for detailed fix suggestion.","status":"tombstone","priority":2,"issue_type":"bug","created_at":"2025-12-16T01:03:20.281591-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-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-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"} +{"id":"bd-qioh","title":"Standardize error handling: replace direct fmt.Fprintf+os.Exit with FatalError","description":"Code health review found inconsistent error handling patterns:\n\nPattern A (preferred): FatalError() from errors.go:21-24\nPattern B (inconsistent): Direct fmt.Fprintf(os.Stderr) + os.Exit(1)\n\nAffected files:\n- delete.go:35-49 uses direct pattern\n- create.go:213 ignores error silently\n- Various other commands\n\nFix: Standardize on FatalError() wrapper for consistent error messages and testability.","status":"open","priority":2,"issue_type":"task","created_at":"2025-12-16T18:17:19.309394-08:00","updated_at":"2025-12-16T18:17:19.309394-08:00","dependencies":[{"issue_id":"bd-qioh","depends_on_id":"bd-tggf","type":"blocks","created_at":"2025-12-16T18:19:05.953559-08:00","created_by":"daemon"}]} +{"id":"bd-r46","title":"Support --reason flag in daemon mode for reopen command","description":"The reopen.go command has a TODO at line 61 to add reason as a comment once RPC supports AddComment. Currently --reason flag is ignored in daemon mode with a warning.","status":"open","priority":2,"issue_type":"feature","created_at":"2025-11-21T18:55:10.773626-05:00","updated_at":"2025-11-21T18:55:10.773626-05:00"} +{"id":"bd-r6a","title":"Redesign workflow system: templates as native Beads","description":"## Problem\n\nThe current workflow system (YAML templates in cmd/bd/templates/workflows/) is architecturally flawed:\n\n1. **Out-of-band data plane** - YAML files are a parallel system outside Beads itself\n2. **Heavyweight DSL** - YAML is gross; even TOML would have been better, but neither is ideal\n3. **Not graph-native** - Beads IS already a dependency graph with priorities, so why reinvent it?\n4. **Can't use bd commands on templates** - They're opaque YAML, not viewable/editable Beads\n\n## The Right Design\n\n**Templates should be Beads themselves.**\n\nA \"workflow template\" should be:\n- An epic marked as a template (via label, type, or prefix like `tpl-`)\n- Child issues with dependencies between them (using normal bd dep)\n- Titles and descriptions containing `{{variable}}` placeholders\n- Normal priorities that control serialization order\n\n\"Instantiation\" becomes:\n1. Clone the template subgraph (epic + children + dependencies)\n2. Substitute variables in titles/descriptions\n3. Generate new IDs for all cloned issues\n4. Return the new epic ID\n\n## Benefits\n\n- **No YAML** - Templates are just Beads\n- **Use existing tools** - `bd show`, `bd edit`, `bd dep` work on templates\n- **Graph-native** - Dependencies are real Beads dependencies\n- **Simpler codebase** - Remove all the YAML parsing/workflow code\n- **Composable** - Templates can reference other templates\n\n## Tasks\n\n1. Delete the YAML workflow system code (revert recent push + remove existing workflow code)\n2. Design template marking convention (label? type? id prefix?)\n3. Implement `bd template create` or `bd clone --as-template`\n4. Implement `bd template instantiate \u003ctemplate-id\u003e --var key=value`\n5. Migrate version-bump workflow to native Beads template\n6. Update documentation","status":"open","priority":1,"issue_type":"epic","created_at":"2025-12-17T22:41:57.359643-08:00","updated_at":"2025-12-17T22:41:57.359643-08:00"} +{"id":"bd-r6a.1","title":"Revert/remove YAML workflow system","description":"Revert the recent commit and remove all YAML workflow code:\n\n1. `git revert aae8407a` (the commit we just pushed with workflow fixes)\n2. Remove `cmd/bd/templates/workflows/` directory\n3. Remove workflow.go or gut it to minimal stub\n4. Remove WorkflowTemplate types from internal/types/workflow.go\n5. Remove any workflow-related RPC handlers\n\nKeep only minimal scaffolding if needed for the new template system.","status":"closed","priority":0,"issue_type":"task","created_at":"2025-12-17T22:42:07.339684-08:00","updated_at":"2025-12-17T22:46:08.606088-08:00","closed_at":"2025-12-17T22:46:08.606088-08:00","dependencies":[{"issue_id":"bd-r6a.1","depends_on_id":"bd-r6a","type":"parent-child","created_at":"2025-12-17T22:42:07.340117-08:00","created_by":"daemon"}]} +{"id":"bd-r6a.2","title":"Implement subgraph cloning with variable substitution","description":"Core implementation of template instantiation:\n\n1. Add `bd template instantiate \u003ctemplate-id\u003e [--var key=value]...` command\n2. Implement subgraph loading:\n - Load template epic\n - Recursively load all children (and their children)\n - Load all dependencies between issues in the subgraph\n3. Implement variable substitution:\n - Scan titles and descriptions for `{{name}}` patterns\n - Replace with provided values\n - Error on missing required variables (or prompt interactively)\n4. Implement cloning:\n - Generate new IDs for all issues\n - Create cloned issues with substituted text\n - Remap and create dependencies\n5. Return the new epic ID\n\nConsider adding `--dry-run` flag to preview what would be created.","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-17T22:43:25.179848-08:00","updated_at":"2025-12-17T23:02:29.034444-08:00","closed_at":"2025-12-17T23:02:29.034444-08:00","dependencies":[{"issue_id":"bd-r6a.2","depends_on_id":"bd-r6a","type":"parent-child","created_at":"2025-12-17T22:43:25.180286-08:00","created_by":"daemon"},{"issue_id":"bd-r6a.2","depends_on_id":"bd-r6a.1","type":"blocks","created_at":"2025-12-17T22:44:03.15413-08:00","created_by":"daemon"}]} +{"id":"bd-r6a.3","title":"Create version-bump template as native Beads","description":"Migrate the version-bump workflow from YAML to a native Beads template:\n\n1. Create epic with template label: Release {{version}}\n2. Create child tasks for each step (update version files, changelog, commit, push, publish)\n3. Set up dependencies between tasks\n4. Add verification commands in task descriptions\n\nThis serves as both migration and validation of the new system.","status":"open","priority":2,"issue_type":"task","created_at":"2025-12-17T22:43:40.694931-08:00","updated_at":"2025-12-17T22:43:40.694931-08:00","dependencies":[{"issue_id":"bd-r6a.3","depends_on_id":"bd-r6a","type":"parent-child","created_at":"2025-12-17T22:43:40.695392-08:00","created_by":"daemon"},{"issue_id":"bd-r6a.3","depends_on_id":"bd-r6a.2","type":"blocks","created_at":"2025-12-17T22:44:03.311902-08:00","created_by":"daemon"}]} +{"id":"bd-r6a.4","title":"Add bd template list command","description":"Add a convenience command to list available templates:\n\nbd template list\n\nThis is equivalent to 'bd list --label=template' but more discoverable.\nCould also show variable placeholders found in each template.","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-17T22:43:47.525316-08:00","updated_at":"2025-12-17T23:02:45.700582-08:00","closed_at":"2025-12-17T23:02:45.700582-08:00","dependencies":[{"issue_id":"bd-r6a.4","depends_on_id":"bd-r6a","type":"parent-child","created_at":"2025-12-17T22:43:47.525743-08:00","created_by":"daemon"},{"issue_id":"bd-r6a.4","depends_on_id":"bd-r6a.2","type":"blocks","created_at":"2025-12-17T22:44:03.474353-08:00","created_by":"daemon"}]} +{"id":"bd-r6a.5","title":"Update documentation for template system","description":"Update AGENTS.md and help text to document the new template system:\n\n- How to create a template (epic + template label + child issues)\n- How to define variables (just use {{name}} placeholders)\n- How to instantiate (bd template instantiate)\n- Migration from YAML workflows (if any users had custom ones)","status":"open","priority":3,"issue_type":"task","created_at":"2025-12-17T22:43:55.461345-08:00","updated_at":"2025-12-17T22:43:55.461345-08:00","dependencies":[{"issue_id":"bd-r6a.5","depends_on_id":"bd-r6a","type":"parent-child","created_at":"2025-12-17T22:43:55.461763-08:00","created_by":"daemon"},{"issue_id":"bd-r6a.5","depends_on_id":"bd-r6a.3","type":"blocks","created_at":"2025-12-17T22:44:03.632404-08:00","created_by":"daemon"},{"issue_id":"bd-r6a.5","depends_on_id":"bd-r6a.4","type":"blocks","created_at":"2025-12-17T22:44:03.788517-08:00","created_by":"daemon"}]} +{"id":"bd-rgyd","title":"Split internal/storage/sqlite/queries.go (1586 lines)","description":"Code health review found queries.go is too large at 1586 lines with:\n\n- Issue scanning logic duplicated between transaction.go and queries.go\n- Timestamp defensive fixes duplicated in CreateIssue, CreateIssues, batch_ops.go\n- Multiple similar patterns that should be consolidated\n\nAlso: transaction.go at 1284 lines, dependencies.go at 893 lines\n\nRecommendation:\n1. Extract common scanning into shared function\n2. Consolidate timestamp defensive fixes\n3. Split queries.go by domain (CRUD, search, batch, events)","status":"open","priority":2,"issue_type":"task","created_at":"2025-12-16T18:17:23.85869-08:00","updated_at":"2025-12-16T18:17:23.85869-08:00","dependencies":[{"issue_id":"bd-rgyd","depends_on_id":"bd-tggf","type":"blocks","created_at":"2025-12-16T18:19:06.062038-08:00","created_by":"daemon"}]} +{"id":"bd-rp4o","title":"Deleted issues resurrect during bd sync (tombstones not propagating)","description":"## Problem\n\nWhen issues are deleted with bd delete --force, they get deleted from the local DB but resurrect during the next bd sync.\n\n## Reproduction\n\n1. Observe orphan warnings (bd-cb64c226.*, bd-cbed9619.*)\n2. Delete them: bd delete bd-cb64c226.1 ... --force\n3. Run bd sync\n4. Orphan warnings reappear - issues were resurrected!\n\n## Root Cause Hypothesis\n\nThe sync branch workflow (beads-sync) has the old state before deletions. When bd sync pulls from beads-sync and copies JSONL to main, the deleted issues are re-imported.\n\nTombstones may not be properly:\n1. Written to beads-sync during export\n2. Propagated during pull/merge\n3. Honored during import\n\n## Related\n\n- bd-7b7h: chicken-and-egg sync.branch bug (same workflow)\n- bd-ncwo: ID-based fallback matching to prevent ghost resurrection\n\n## Files to Investigate\n\n- cmd/bd/sync.go (export/import flow)\n- internal/syncbranch/worktree.go (PullFromSyncBranch, copyJSONLToMainRepo)\n- internal/importer/ (tombstone handling)","status":"tombstone","priority":1,"issue_type":"bug","created_at":"2025-12-16T23:09:43.072696-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-s0qf","title":"GH#405: Fix prefix parsing with hyphens - multi-hyphen prefixes parsed incorrectly","description":"Fixed: ExtractIssuePrefix was falling back to first-hyphen for word-like suffixes, breaking multi-hyphen prefixes like 'hacker-news' and 'me-py-toolkit'.","status":"tombstone","priority":2,"issue_type":"bug","created_at":"2025-12-16T01:13:56.951359-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-s2t","title":"wish: a 'continue' or similar cmd/flag which means alter last issue","description":"so many time I create an issue and then have another thought: 'oh, before I did X and it crashed there was ZZZ happening' or 'actually that is P4 not P2'. It would be nice if when `bd {cmd}` is used without a {title} or {id} it just adds or updates the most recently touched issue.","status":"open","priority":3,"issue_type":"task","created_at":"2025-12-08T06:46:37.529160416-07:00","updated_at":"2025-12-08T06:46:37.529160416-07:00"} +{"id":"bd-sh4c","title":"Improve test coverage for cmd/bd/setup (28.4% → 50%)","description":"The setup package has only 28.4% test coverage. Setup commands are critical for first-time user experience.\n\nCurrent coverage: 28.4%\nTarget coverage: 50%","status":"open","priority":2,"issue_type":"task","created_at":"2025-12-13T20:43:04.409346-08:00","updated_at":"2025-12-13T21:01:18.98833-08:00"} +{"id":"bd-siz1","title":"GH#532: bd sync circular error (suggests running bd sync)","description":"bd sync error message recommends running bd sync to fix the bd sync error. Fix error handling to provide useful guidance. See GitHub issue #532.","status":"tombstone","priority":2,"issue_type":"bug","created_at":"2025-12-16T01:04:00.543573-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-svb5","title":"GH#505: Add bd reset/wipe command","description":"Add command to cleanly reset/wipe beads database. User reports painful manual process to start fresh. See GitHub issue #505.","status":"tombstone","priority":2,"issue_type":"feature","created_at":"2025-12-16T01:03:42.160966-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-t4u1","title":"False positive detection by Kaspersky Antivirus (Trojan)","description":"Kaspersky Antivirus falsely detects beads (bd.exe v0.23.1) as a Trojan (PDM:Trojan.Win32.Generic) and removes it.\nEvent: Malicious object detected\nComponent: System Watcher\nObject name: bd.exe\n","status":"open","priority":1,"issue_type":"task","created_at":"2025-11-20T18:56:12.498187-05:00","updated_at":"2025-11-20T18:56:12.498187-05:00"} +{"id":"bd-tbz3","title":"bd init UX Improvements","description":"bd init leaves users with incomplete setup, requiring manual bd doctor --fix. Issues found: (1) git hooks not installed if user declines prompt, (2) no auto-migration when CLI is upgraded, (3) stale merge driver configs from old versions. Fix by making bd init more robust with better defaults and auto-migration.","status":"open","priority":1,"issue_type":"epic","created_at":"2025-11-21T23:16:00.333543-08:00","updated_at":"2025-11-21T23:16:37.811233-08:00"} +{"id":"bd-tggf","title":"Code Health Review Dec 2025: Technical Debt Cleanup","description":"Epic grouping technical debt identified in the Dec 16, 2025 code health review.\n\n## Overall Health Grade: B (Solid foundation, needs cleanup)\n\n### P1 (High Priority):\n- bd-74w1: Consolidate duplicate path-finding utilities\n- bd-b6xo: Remove/fix ClearDirtyIssues() race condition\n- bd-b3og: Fix TestImportBugIntegration deadlock\n\n### P2 (Medium Priority):\n- bd-05a8: Split large files (doctor.go, sync.go)\n- bd-qioh: Standardize error handling patterns\n- bd-rgyd: Split queries.go (1586 lines)\n- bd-9g1z: Fix/remove TestFindJSONLPathDefault\n\n### P3 (Low Priority):\n- bd-ork0: Add comments to 30+ ignored errors\n- bd-4nqq: Remove dead test code in info_test.go\n- bd-dhza: Reduce global state in main.go\n\n## Key Areas:\n1. Code duplication in path utilities\n2. Large monolithic files (5 files \u003e1000 lines)\n3. Global state (25+ variables, 3 deprecated)\n4. Silent error suppression (30+ instances)\n5. Test gaps and dead test code\n6. Atomicity risks in batch operations","status":"open","priority":2,"issue_type":"epic","created_at":"2025-12-16T18:18:58.115507-08:00","updated_at":"2025-12-16T18:21:50.561709-08:00"} +{"id":"bd-thgk","title":"Improve test coverage for internal/compact (18.2% → 70%)","description":"The compact package has only 18.2% test coverage. This is a core package handling issue compaction and should have at least 70% coverage.\n\nKey functions needing tests:\n- runCompactSingle (0%)\n- runCompactAll (0%)\n- runCompactRPC (0%)\n- runCompactStatsRPC (0%)\n- runCompactAnalyze (0%)\n- runCompactApply (0%)\n- pruneDeletionsManifest (0%)\n\nCurrent coverage: 18.2%\nTarget coverage: 70%","status":"open","priority":1,"issue_type":"task","created_at":"2025-12-13T20:42:58.455767-08:00","updated_at":"2025-12-13T21:01:15.070551-08:00"} +{"id":"bd-tm2p","title":"Polecats get stuck on interactive shell prompts (cp/mv/rm -i)","description":"During swarm operations, polecats frequently get stuck waiting for interactive prompts from shell commands like:\n- cp prompting 'overwrite file? (y/n)'\n- mv prompting 'overwrite file? (y/n)' \n- rm prompting 'remove file?'\n\nThis happens because macOS aliases or shell configs may have -i flags set by default.\n\nRoot cause: Claude Code runs commands that trigger interactive confirmation prompts, but cannot respond to them, causing the agent to hang indefinitely.\n\nObserved in: Multiple polecats during GH issues swarm (Dec 2024)\n- Derrick, Roustabout, Prospector, Warboy all got stuck on y/n prompts\n\nSuggested fixes:\n1. AGENTS.md should instruct agents to always use -f flag with cp/mv/rm\n2. Polecat startup could set shell aliases to use non-interactive versions\n3. bd prime hook could include guidance about non-interactive commands\n4. Consider detecting stuck prompts and auto-recovering","status":"closed","priority":1,"issue_type":"bug","created_at":"2025-12-14T16:51:24.572271-08:00","updated_at":"2025-12-17T23:13:40.536312-08:00","closed_at":"2025-12-17T19:13:04.074424-08:00"} +{"id":"bd-toy3","title":"Test hook","description":"","status":"tombstone","priority":2,"issue_type":"task","created_at":"2025-12-16T18:33:39.717036-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-tvu3","title":"Improve test coverage for internal/beads (48.1% → 70%)","description":"The beads package is a core package with 48.1% coverage. As the primary package for issue management, it should have at least 70% coverage.\n\nCurrent coverage: 48.1%\nTarget coverage: 70%","status":"open","priority":1,"issue_type":"task","created_at":"2025-12-13T20:42:59.739142-08:00","updated_at":"2025-12-13T21:01:14.874359-08:00"} {"id":"bd-u0g9","title":"GH#405: Prefix parsing with hyphens treats first segment as prefix","description":"Prefix me-py-toolkit gets parsed as just me- when detecting mismatches. Fix prefix parsing to handle multi-hyphen prefixes. See GitHub issue #405.","status":"tombstone","priority":2,"issue_type":"bug","created_at":"2025-12-16T01:03:18.354066-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-llfl","title":"Improve test coverage for cmd/bd CLI (26.2% → 50%)","description":"The main CLI package (cmd/bd) has only 26.2% test coverage. CLI commands should have at least 50% coverage to ensure reliability.\n\nKey areas with low/no coverage:\n- daemon_autostart.go (multiple 0% functions)\n- compact.go (several 0% functions)\n- Various command handlers\n\nCurrent coverage: 26.2%\nTarget coverage: 50%","status":"open","priority":2,"issue_type":"task","created_at":"2025-12-13T20:43:03.123341-08:00","updated_at":"2025-12-13T21:01:18.901944-08:00"} -{"id":"bd-411u","title":"Document BEADS_DIR pattern for multi-agent workspaces (Gas Town)","description":"Gas Town and similar multi-agent systems need to configure separate beads databases per workspace/rig, distinct from any project-level beads.\n\n## Use Case\n\nIn Gas Town:\n- Each 'rig' (managed project) has multiple agents (polecats, refinery, witness)\n- All agents in a rig should share a single beads database at the rig level\n- This should be separate from any .beads/ the project itself uses\n- The BEADS_DIR env var enables this\n\n## Documentation Needed\n\n1. Add a section to docs explaining BEADS_DIR for multi-agent setups\n2. Example: setting BEADS_DIR in agent startup scripts/hooks\n3. Clarify interaction with project-level .beads/ (BEADS_DIR takes precedence)\n\n## Current Support\n\nAlready implemented in internal/beads/beads.go:FindDatabasePath():\n- BEADS_DIR env var is checked first (preferred)\n- BEADS_DB env var still supported (deprecated)\n- Falls back to .beads/ search in tree\n\nJust needs documentation for the multi-agent workspace pattern.","status":"open","priority":3,"issue_type":"task","created_at":"2025-12-15T22:08:22.158027-08:00","updated_at":"2025-12-15T22:08:22.158027-08:00"} -{"id":"bd-t4u1","title":"False positive detection by Kaspersky Antivirus (Trojan)","description":"Kaspersky Antivirus falsely detects beads (bd.exe v0.23.1) as a Trojan (PDM:Trojan.Win32.Generic) and removes it.\nEvent: Malicious object detected\nComponent: System Watcher\nObject name: bd.exe\n","status":"open","priority":1,"issue_type":"task","created_at":"2025-11-20T18:56:12.498187-05:00","updated_at":"2025-11-20T18:56:12.498187-05:00"} -{"id":"bd-r46","title":"Support --reason flag in daemon mode for reopen command","description":"The reopen.go command has a TODO at line 61 to add reason as a comment once RPC supports AddComment. Currently --reason flag is ignored in daemon mode with a warning.","status":"open","priority":2,"issue_type":"feature","created_at":"2025-11-21T18:55:10.773626-05:00","updated_at":"2025-11-21T18:55:10.773626-05:00"} -{"id":"bd-dyy","title":"Review PR #513: fix hooks install docs","description":"Review and merge PR #513 from aspiers. This PR fixes incorrect docs for how to install git hooks - updates README to use bd hooks install instead of removed install.sh. Simple 1-line change. URL: https://github.com/anthropics/beads/pull/513","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-13T08:15:14.838772+11:00","updated_at":"2025-12-13T07:07:19.718544-08:00","closed_at":"2025-12-13T07:07:19.718544-08:00"} -{"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-1slh","title":"Investigate charmbracelet-based TUI for beads","description":"Now that we've merged the create-form command (PR #603) which uses charmbracelet/huh, investigate whether beads should have a more comprehensive TUI.\n\nConsiderations:\n- Should this be in core or a separate binary (bd-tui)?\n- What functionality would benefit from a TUI? (list view, issue details, search, bulk operations)\n- Plugin/extension architecture vs build tags vs separate binary\n- Dependency cost vs user experience tradeoff\n- Target audience: humans who want interactive workflows vs CLI/scripting users\n\nRelated: PR #603 added charmbracelet/huh dependency for create-form command.","status":"open","priority":3,"issue_type":"feature","created_at":"2025-12-17T14:20:51.503563-08:00","updated_at":"2025-12-17T14:20:51.503563-08:00"} -{"id":"bd-05a8","title":"Split large cmd/bd files: doctor.go (2948 lines), sync.go (2121 lines)","description":"Code health review found several oversized files:\n\n1. doctor.go - 2948 lines, 48 functions mixed together\n - Should split into doctor/checks/*.go for individual diagnostics\n - applyFixes() and previewFixes() are nearly identical\n\n2. sync.go - 2121 lines\n - ZFC (Zero Flush Check) logic embedded inline (lines 213-247)\n - Multiple mode handlers should be extracted\n\n3. init.go - 1732 lines\n4. compact.go - 1097 lines\n5. show.go - 1069 lines\n\nRecommendation: Extract into focused sub-packages or split into logical files.","status":"open","priority":2,"issue_type":"task","created_at":"2025-12-16T18:17:18.169927-08:00","updated_at":"2025-12-16T18:17:18.169927-08:00","dependencies":[{"issue_id":"bd-05a8","depends_on_id":"bd-tggf","type":"blocks","created_at":"2025-12-16T18:19:05.846503-08:00","created_by":"daemon"}]} -{"id":"bd-aydr.1","title":"Implement core reset package (internal/reset)","description":"Create the core reset logic in internal/reset/ package.\n\n## Responsibilities\n- ResetOptions struct with all flag options\n- CountImpact() - count issues/tombstones that will be deleted\n- ValidateState() - check .beads/ exists, check git dirty state\n- ExecuteReset() - main reset logic (without CLI concerns)\n- Integrate with daemon killall\n\n## Interface Design\n```go\ntype ResetOptions struct {\n Hard bool // Include git operations (git rm, commit)\n Backup bool // Create backup before reset\n DryRun bool // Preview only, don't execute\n SkipInit bool // Don't re-initialize after reset\n}\n\ntype ResetResult struct {\n IssuesDeleted int\n TombstonesDeleted int\n BackupPath string // if backup was created\n DaemonsKilled int\n}\n\ntype ImpactSummary struct {\n IssueCount int\n OpenCount int\n ClosedCount int\n TombstoneCount int\n HasUncommitted bool // git dirty state\n}\n\nfunc Reset(opts ResetOptions) (*ResetResult, error)\nfunc CountImpact() (*ImpactSummary, error)\nfunc ValidateState() error\n```\n\n## IMPORTANT: CLI vs Core Separation\n- `Force` (skip confirmation) is NOT in ResetOptions - that's a CLI concern\n- Core always executes when called; CLI decides whether to prompt first\n- Keep CLI-agnostic: no prompts, no colored output, no user interaction\n- Return errors for CLI to handle with user-friendly messages\n- Unit testable in isolation\n\n## Dependencies\n- Uses daemon.KillAllDaemons() from internal/daemon/\n- Calls bd init logic after reset (unless SkipInit)","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-13T08:44:50.145364+11:00","updated_at":"2025-12-13T10:13:32.610253+11:00","closed_at":"2025-12-13T09:20:06.184893+11:00","dependencies":[{"issue_id":"bd-aydr.1","depends_on_id":"bd-aydr","type":"parent-child","created_at":"2025-12-13T08:44:50.145775+11:00","created_by":"daemon"}]} -{"id":"bd-rp4o","title":"Deleted issues resurrect during bd sync (tombstones not propagating)","description":"## Problem\n\nWhen issues are deleted with bd delete --force, they get deleted from the local DB but resurrect during the next bd sync.\n\n## Reproduction\n\n1. Observe orphan warnings (bd-cb64c226.*, bd-cbed9619.*)\n2. Delete them: bd delete bd-cb64c226.1 ... --force\n3. Run bd sync\n4. Orphan warnings reappear - issues were resurrected!\n\n## Root Cause Hypothesis\n\nThe sync branch workflow (beads-sync) has the old state before deletions. When bd sync pulls from beads-sync and copies JSONL to main, the deleted issues are re-imported.\n\nTombstones may not be properly:\n1. Written to beads-sync during export\n2. Propagated during pull/merge\n3. Honored during import\n\n## Related\n\n- bd-7b7h: chicken-and-egg sync.branch bug (same workflow)\n- bd-ncwo: ID-based fallback matching to prevent ghost resurrection\n\n## Files to Investigate\n\n- cmd/bd/sync.go (export/import flow)\n- internal/syncbranch/worktree.go (PullFromSyncBranch, copyJSONLToMainRepo)\n- internal/importer/ (tombstone handling)","status":"tombstone","priority":1,"issue_type":"bug","created_at":"2025-12-16T23:09:43.072696-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-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-16T01:00:46.831373-08:00","closed_at":"2025-10-28T20:21:39.529971-07:00","dependencies":[{"issue_id":"bd-cbed9619.2","depends_on_id":"bd-325da116","type":"parent-child","created_at":"2025-10-28T18:39:20.616846-07:00","created_by":"daemon"},{"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-dp4w","title":"Test message","description":"This is a test message body","status":"tombstone","priority":2,"issue_type":"message","created_at":"2025-12-16T18:11:58.467876-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":"message"} -{"id":"bd-elqd","title":"Systematic bd sync stability investigation","description":"## Context\n\nbd sync has chronic instability issues that have persisted since inception:\n- issues.jsonl is always dirty after push\n- bd sync often creates messes requiring manual cleanup\n- Problems escalating despite accumulated bug fixes\n- Workarounds are getting increasingly draconian\n\n## Goal\n\nSystematically observe and diagnose bd sync failures rather than applying band-aid fixes.\n\n## Approach\n\n1. Start fresh session with latest binary (all fixes applied)\n2. Run bd sync and carefully observe what happens\n3. Document exact sequence of events when things go wrong\n4. File specific issues for each discrete problem identified\n5. Track the root causes, not just symptoms\n\n## Test Environment\n\n- Fresh clone or clean state\n- Latest bd binary with all bug fixes\n- Monitor both local and remote JSONL state\n- Check for timing issues, race conditions, merge conflicts\n\n## Success Criteria\n\n- Identify root causes of sync instability\n- Create actionable issues for each problem\n- Eventually achieve stable bd sync (no manual intervention needed)","status":"tombstone","priority":1,"issue_type":"task","created_at":"2025-12-16T22:57:25.35289-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-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-16T01:00:40.320263-08:00","closed_at":"2025-10-28T14:08:38.059615-07:00"} -{"id":"bd-vs9","title":"Fix unparam unused parameter in cmd/bd/doctor.go:541","description":"Linting issue: checkHooksQuick - path is unused (unparam) at cmd/bd/doctor.go:541:22. Error: func checkHooksQuick(path string) string {","status":"open","issue_type":"bug","created_at":"2025-12-07T15:35:17.02177046-07:00","updated_at":"2025-12-07T15:35:17.02177046-07:00"} -{"id":"bd-49kw","title":"Workaround for FastMCP outputSchema bug in Claude Code","description":"The beads MCP server (v0.23.1) successfully connects to Claude Code, but all tools fail to load with a schema validation error due to a bug in FastMCP 2.13.1.\n\nError: \"Invalid literal value, expected \\\"object\\\"\" in outputSchema.\n\nRoot Cause: FastMCP generates outputSchema with $ref at root level without \"type\": \"object\" for self-referential models (Issue).\n\nWorkaround: Use slash commands (/beads:ready) or wait for FastMCP fix.\n","status":"open","priority":1,"issue_type":"bug","created_at":"2025-11-20T18:55:39.041831-05:00","updated_at":"2025-11-20T18:55:39.041831-05:00"} -{"id":"bd-5b6e","title":"Add tests for helper functions (GetDirtyIssueHash, GetAllDependencyRecords, export hashes)","description":"Several utility functions have 0% coverage:\n- GetDirtyIssueHash (dirty.go)\n- GetAllDependencyRecords (dependencies.go)\n- GetExportHash, SetExportHash, ClearAllExportHashes (hash.go)\n\nThese are lower priority but should have basic coverage.","status":"open","priority":4,"issue_type":"task","created_at":"2025-11-01T22:40:58.989976-07:00","updated_at":"2025-11-01T22:40:58.989976-07:00"} -{"id":"bd-au0.9","title":"Review and document rarely-used commands","description":"Document use cases or consider deprecation for infrequently-used commands.\n\n**Commands to review:**\n1. bd rename-prefix - How often is this used? Document use cases\n2. bd detect-pollution - Consider integrating into bd validate\n3. bd migrate-hash-ids - One-time migration, keep but document as legacy\n\n**For each command:**\n- Document typical use cases\n- Add examples to help text\n- Consider if it should be a subcommand instead\n- Add deprecation warning if appropriate\n\n**Not changing:**\n- duplicates āœ“ (useful for data quality)\n- repair-deps āœ“ (useful for fixing broken refs)\n- restore āœ“ (critical for compacted issues)\n- compact āœ“ (performance feature)\n\n**Deliverable:**\n- Updated help text\n- Documentation in ADVANCED.md\n- Deprecation plan if needed","status":"open","priority":3,"issue_type":"task","created_at":"2025-11-21T21:08:05.588275-05:00","updated_at":"2025-11-21T21:08:05.588275-05:00","dependencies":[{"issue_id":"bd-au0.9","depends_on_id":"bd-au0","type":"parent-child","created_at":"2025-11-21T21:08:05.59003-05: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-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-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-b3og","title":"Fix TestImportBugIntegration deadlock in importer_test.go","description":"Code health review found internal/importer/importer_test.go has TestImportBugIntegration skipped with:\n\nTODO: Test hangs due to database deadlock - needs investigation\n\nThis indicates a potential unresolved concurrency issue in the importer. The test has been skipped for an unknown duration.\n\nFix: Investigate the deadlock, fix the underlying issue, and re-enable the test.","status":"open","priority":1,"issue_type":"bug","created_at":"2025-12-16T18:17:22.103838-08:00","updated_at":"2025-12-16T18:17:22.103838-08:00","dependencies":[{"issue_id":"bd-b3og","depends_on_id":"bd-tggf","type":"blocks","created_at":"2025-12-16T18:19:05.740642-08:00","created_by":"daemon"}]} -{"id":"bd-hlsw.3","title":"Auto-recovery mode (bd sync --auto-recover)","description":"Add bd sync --auto-recover flag that: detects problematic sync state, backs up .beads/issues.db with timestamp, rebuilds DB from JSONL atomically, verifies consistency, reports what was fixed. Provides safety valve when sync integrity fails.","status":"open","priority":3,"issue_type":"task","created_at":"2025-12-14T10:40:20.599836875-07:00","updated_at":"2025-12-14T10:40:20.599836875-07:00","dependencies":[{"issue_id":"bd-hlsw.3","depends_on_id":"bd-hlsw","type":"parent-child","created_at":"2025-12-14T10:40:20.600435888-07:00","created_by":"daemon"}]} -{"id":"bd-ncwo","title":"Ghost resurrection: remote status:closed wins during git merge","description":"During bd sync, the 3-way git merge sometimes keeps remote's status:closed instead of local's status:tombstone. This causes ghost issues to resurrect even when tombstones exist.\n\nRoot cause: Git 3-way merge doesn't understand tombstone semantics. If base had closed, local changed to tombstone, and remote has closed, git might keep remote's version.\n\nThe early tombstone check in importer.go only prevents CREATION when tombstones exist in DB. But if applyDeletionsFromMerge hard-deletes the tombstones before import runs (because they're not in the merged result), the check doesn't help.\n\nPotential fixes:\n1. Make tombstones 'win' in the beads merge driver (internal/merge/merge.go)\n2. Don't hard-delete tombstones in applyDeletionsFromMerge if they're in the DB\n3. Export tombstones to a separate file that's not subject to merge\n\nGhost issues: bd-cb64c226.*, bd-cbed9619.*","status":"tombstone","priority":1,"issue_type":"bug","created_at":"2025-12-16T22:01:03.56423-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-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-0d5p","title":"Fix TestRunSync_Timeout failing on macOS","description":"The hooks timeout test fails because exec.CommandContext doesn't properly terminate child processes of shell scripts on macOS. The test creates a hook that runs 'sleep 60' with a 500ms timeout, but it waits the full 60 seconds.\n\nOptions to fix:\n- Use SysProcAttr{Setpgid: true} to create process group and kill the group\n- Skip test on darwin with build tag\n- Use a different approach for timeout testing\n\nLocation: internal/hooks/hooks_test.go:220-253","status":"open","priority":1,"issue_type":"bug","created_at":"2025-12-16T20:52:51.771217-08:00","updated_at":"2025-12-16T20:52:51.771217-08:00"} -{"id":"bd-0a43","title":"Split monolithic sqlite.go into focused files","description":"internal/storage/sqlite/sqlite.go is 1050 lines containing initialization, 20+ CRUD methods, query building, and schema management.\n\nSplit into:\n- store.go: Store struct \u0026 initialization (150 lines)\n- bead_queries.go: Bead CRUD (300 lines)\n- work_queries.go: Work queries (200 lines) \n- stats_queries.go: Statistics (150 lines)\n- schema.go: Schema \u0026 migrations (150 lines)\n- helpers.go: Common utilities (100 lines)\n\nImpact: Impossible to understand at a glance; hard to find specific functionality; high cognitive load\n\nEffort: 6-8 hours","status":"open","issue_type":"task","created_at":"2025-11-16T14:51:16.520465-08:00","updated_at":"2025-11-16T14:51:16.520465-08:00"} -{"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-16T01:00:42.937398-08:00","closed_at":"2025-10-28T14:08:38.06569-07:00"} -{"id":"bd-n386","title":"Improve test coverage for internal/daemon (27.3% → 60%)","description":"The daemon package has only 27.3% test coverage. The daemon is critical for background operations and reliability.\n\nKey areas needing tests:\n- Daemon autostart logic\n- Socket handling\n- PID file management\n- Health checks\n\nCurrent coverage: 27.3%\nTarget coverage: 60%","status":"open","priority":2,"issue_type":"task","created_at":"2025-12-13T20:43:00.895238-08:00","updated_at":"2025-12-13T21:01:17.274438-08:00"} -{"id":"bd-fi05","title":"bd sync fails with orphaned issues and duplicate ID conflict","description":"After fixing the deleted_at TEXT column scanning bug (commit 18b1eb2), bd sync still fails with two issues:\n\n1. Orphan Detection Warning: 12 orphaned child issues whose parents no longer exist (bd-cb64c226.* and bd-cbed9619.*)\n\n2. Import Failure: UNIQUE constraint failed for bd-360 - this tombstone exists in both DB and JSONL\n\nError: \"Import failed: error creating depth-0 issues: bulk insert issues: failed to insert issue bd-360: sqlite3: constraint failed: UNIQUE constraint failed: issues.id\"\n\nFix options:\n- Delete orphaned child issues with bd delete\n- Resolve bd-360 duplicate (in deletions.jsonl vs tombstone in DB)\n- Reset sync branch: git branch -f beads-sync main \u0026\u0026 git push --force-with-lease origin beads-sync","notes":"Fixed tombstone constraint violation bug. When deleting closed issues, the CHECK constraint (status = 'closed') = (closed_at IS NOT NULL) was violated because CreateTombstone didn't clear closed_at. Fix: set closed_at = NULL in tombstone creation SQL.\n\nThe sync data corruption (orphaned issues in beads-sync branch) requires manual cleanup: reset sync branch with 'git branch -f beads-sync main \u0026\u0026 git push --force-with-lease origin beads-sync'","status":"closed","priority":2,"issue_type":"bug","created_at":"2025-12-13T07:14:33.831346-08:00","updated_at":"2025-12-13T10:50:48.545465-08:00","closed_at":"2025-12-13T07:30:33.843986-08:00"} -{"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-06px","title":"bd sync --from-main fails: unknown flag --no-git-history","status":"open","priority":1,"issue_type":"bug","created_at":"2025-12-17T14:32:02.998106-08:00","updated_at":"2025-12-17T14:32:02.998106-08:00"} -{"id":"bd-aydr.9","title":"Add .beads-backup-* pattern to gitignore template","description":"Update the gitignore template in doctor package to include backup directories.\n\n## Change\nAdd `.beads-backup-*/` to the GitignoreTemplate in `cmd/bd/doctor/gitignore.go`\n\n## Why\nBackup directories created by `bd reset --backup` should not be committed to git.\nThey are local-only recovery tools.\n\n## File\n`cmd/bd/doctor/gitignore.go` - look for GitignoreTemplate constant","status":"closed","priority":3,"issue_type":"task","created_at":"2025-12-13T08:49:42.453483+11:00","updated_at":"2025-12-13T09:16:44.201889+11:00","closed_at":"2025-12-13T09:16:44.201889+11:00","dependencies":[{"issue_id":"bd-aydr.9","depends_on_id":"bd-aydr","type":"parent-child","created_at":"2025-12-13T08:49:42.453886+11:00","created_by":"daemon"}]} -{"id":"bd-bw6","title":"Fix G104 errors unhandled in internal/storage/sqlite/queries.go:1181","description":"Linting issue: G104: Errors unhandled (gosec) at internal/storage/sqlite/queries.go:1181:4. Error: rows.Close()","status":"open","issue_type":"bug","created_at":"2025-12-07T15:35:09.008444133-07:00","updated_at":"2025-12-07T15:35:09.008444133-07:00"} -{"id":"bd-7b7h","title":"bd sync --merge fails due to chicken-and-egg: .beads/ always dirty","description":"## Problem\n\nWhen sync.branch is configured (e.g., beads-sync), the bd sync workflow creates a chicken-and-egg problem:\n\n1. `bd sync` commits changes to beads-sync via worktree\n2. `bd sync` copies JSONL to main working dir via `copyJSONLToMainRepo()` (sync.go line 364, worktree.go line 678-685)\n3. The copy is NOT committed to main - it just updates the working tree\n4. `bd sync --merge` checks for clean working dir (sync.go line 1547-1548)\n5. `bd sync --merge` FAILS because .beads/issues.jsonl is uncommitted!\n\n## Impact\n\n- sync.branch workflow is fundamentally broken\n- Users cannot periodically merge beads-sync → main\n- Main branch always shows as dirty\n- Creates confusion about git state\n\n## Root Cause\n\nsync.go:1547-1548:\n```go\nif len(strings.TrimSpace(string(statusOutput))) \u003e 0 {\n return fmt.Errorf(\"main branch has uncommitted changes, please commit or stash them first\")\n}\n```\n\nThis check blocks merge when ANY uncommitted changes exist, including the .beads/ changes that `bd sync` itself created.\n\n## Proposed Fix\n\nOption A: Exclude .beads/ from the clean check in `mergeSyncBranch`:\n```go\n// Check if there are non-beads uncommitted changes\nstatusCmd := exec.CommandContext(ctx, \"git\", \"status\", \"--porcelain\", \"--\", \":!.beads/\")\n```\n\nOption B: Auto-stash .beads/ changes before merge, restore after\n\nOption C: Change the workflow - do not copy JSONL to main working dir, instead always read from worktree\n\n## Files to Modify\n\n- cmd/bd/sync.go:1540-1549 (mergeSyncBranch function)\n- Possibly internal/syncbranch/worktree.go (copyJSONLToMainRepo)","notes":"## Fix Implemented\n\nModified cmd/bd/sync.go mergeSyncBranch function:\n\n1. **Exclude .beads/ from dirty check** (line 1543):\n Changed `git status --porcelain` to `git status --porcelain -- :!.beads/`\n This allows merge to proceed when only .beads/ has uncommitted changes.\n\n2. **Restore .beads/ to HEAD before merge** (lines 1553-1561):\n Added `git checkout HEAD -- .beads/` before merge to prevent\n \"Your local changes would be overwritten by merge\" errors.\n The .beads/ changes are redundant since they came FROM beads-sync.\n\n## Testing\n\n- All cmd/bd sync/merge tests pass\n- All internal/syncbranch tests pass\n- Manual verification needed for full workflow","status":"tombstone","issue_type":"bug","created_at":"2025-12-16T23:06:06.97703-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-bwk2","title":"Centralize error handling patterns in storage layer","description":"80+ instances of inconsistent error handling across sqlite.go with mix of %w, %v, and no wrapping.\n\nLocation: internal/storage/sqlite/sqlite.go (throughout)\n\nProblem:\n- Some use fmt.Errorf(\"op failed: %w\", err) - correct wrapping\n- Some use fmt.Errorf(\"op failed: %v\", err) - loses error chain\n- Some return err directly - no context\n- Hard to debug production issues\n- Can't distinguish error types\n\nSolution: Create internal/storage/sqlite/errors.go:\n- Define sentinel errors (ErrNotFound, ErrInvalidID, etc.)\n- Create wrapDBError(op string, err error) helper\n- Convert sql.ErrNoRows to ErrNotFound\n- Always wrap with operation context\n\nImpact: Lost error context; inconsistent messages; hard to debug\n\nEffort: 5-7 hours","status":"open","priority":1,"issue_type":"task","created_at":"2025-11-16T14:51:54.974909-08:00","updated_at":"2025-11-16T14:51:54.974909-08:00"} -{"id":"bd-o55a","title":"GH#509: bd doesn't find .beads when running from nested worktrees","description":"When worktrees are nested under main repo (.worktrees/feature/), bd stops at worktree git root instead of continuing to find .beads in parent. See GitHub issue #509 for detailed fix suggestion.","status":"tombstone","priority":2,"issue_type":"bug","created_at":"2025-12-16T01:03:20.281591-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-9cdc","title":"Update docs for import bug fix","description":"Update AGENTS.md, README.md, TROUBLESHOOTING.md with import.orphan_handling config documentation. Document resurrection behavior, tombstones, config modes. Add troubleshooting section for import failures with deleted parents.","status":"open","priority":2,"issue_type":"task","created_at":"2025-11-04T12:32:30.770415-08:00","updated_at":"2025-11-04T12:32:30.770415-08:00"} -{"id":"bd-adoe","title":"Add --hard flag to bd cleanup to permanently cull tombstones before cutoff date","description":"Currently tombstones persist for 30 days before cleanup prunes them. Need an official way to force-cull tombstones earlier than the default TTL, for scenarios like cleaning house after extended absence where resurrection from old clones is not a concern. Proposed: bd cleanup --hard --older-than N to bypass the 30-day tombstone TTL.","status":"tombstone","priority":2,"issue_type":"feature","created_at":"2025-12-16T01:17:31.064914-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-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-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":"open","priority":1,"issue_type":"feature","created_at":"2025-11-22T00:02:08.762684-08:00","updated_at":"2025-11-22T00:02:08.762684-08:00"} -{"id":"bd-56x","title":"Review PR #514: fix plugin install docs","description":"Review and merge PR #514 from aspiers. This PR fixes incorrect docs for installing Claude Code plugin from source in docs/PLUGIN.md. Clarifies shell vs Claude Code commands and fixes the . vs ./beads argument issue. URL: https://github.com/anthropics/beads/pull/514","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-13T08:15:16.865354+11:00","updated_at":"2025-12-13T07:07:19.729213-08:00","closed_at":"2025-12-13T07:07:19.729213-08:00"} -{"id":"bd-abjw","title":"Consider consolidating config.yaml parsing into shared utility","description":"Multiple places parse config.yaml with custom structs:\n\n1. **autoimport.go:148** - `localConfig{SyncBranch}`\n2. **main.go:310** - strings.Contains for no-db (fragile, see bd-r6k2)\n3. **doctor.go:863** - strings.Contains for no-db (fragile, see bd-r6k2)\n4. **internal/config/config.go** - Uses viper (but caches at startup, problematic for tests)\n\nConsider creating a shared utility in `internal/configfile/` or extending the viper config:\n\n```go\n// internal/configfile/yaml.go\ntype YAMLConfig struct {\n SyncBranch string `yaml:\"sync-branch\"`\n NoDb bool `yaml:\"no-db\"`\n IssuePrefix string `yaml:\"issue-prefix\"`\n Author string `yaml:\"author\"`\n}\n\nfunc LoadYAML(beadsDir string) (*YAMLConfig, error) {\n // Parse config.yaml with proper YAML library\n}\n```\n\nBenefits:\n- Single source of truth for config.yaml structure\n- Proper YAML parsing everywhere\n- Easier to add new config fields\n\nTrade-off: May add complexity for simple one-off reads.","status":"open","priority":4,"issue_type":"task","created_at":"2025-12-07T02:03:26.067311-08:00","updated_at":"2025-12-07T02:03:26.067311-08:00"} -{"id":"bd-tggf","title":"Code Health Review Dec 2025: Technical Debt Cleanup","description":"Epic grouping technical debt identified in the Dec 16, 2025 code health review.\n\n## Overall Health Grade: B (Solid foundation, needs cleanup)\n\n### P1 (High Priority):\n- bd-74w1: Consolidate duplicate path-finding utilities\n- bd-b6xo: Remove/fix ClearDirtyIssues() race condition\n- bd-b3og: Fix TestImportBugIntegration deadlock\n\n### P2 (Medium Priority):\n- bd-05a8: Split large files (doctor.go, sync.go)\n- bd-qioh: Standardize error handling patterns\n- bd-rgyd: Split queries.go (1586 lines)\n- bd-9g1z: Fix/remove TestFindJSONLPathDefault\n\n### P3 (Low Priority):\n- bd-ork0: Add comments to 30+ ignored errors\n- bd-4nqq: Remove dead test code in info_test.go\n- bd-dhza: Reduce global state in main.go\n\n## Key Areas:\n1. Code duplication in path utilities\n2. Large monolithic files (5 files \u003e1000 lines)\n3. Global state (25+ variables, 3 deprecated)\n4. Silent error suppression (30+ instances)\n5. Test gaps and dead test code\n6. Atomicity risks in batch operations","status":"open","priority":2,"issue_type":"epic","created_at":"2025-12-16T18:18:58.115507-08:00","updated_at":"2025-12-16T18:21:50.561709-08:00"} -{"id":"bd-90v","title":"bd prime: AI context loading and Claude Code integration","description":"Implement `bd prime` command and Claude Code hooks for context recovery. Hooks work with BOTH MCP server and CLI approaches - they solve the context memory problem (keeping bd workflow fresh after compaction) not the tool access problem (MCP vs CLI).","status":"open","priority":2,"issue_type":"epic","created_at":"2025-11-11T23:31:12.119012-08:00","updated_at":"2025-11-12T00:11:07.743189-08:00"} -{"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-aec5439f","title":"Update LINTING.md with current baseline","description":"After cleanup, document the remaining acceptable baseline in LINTING.md so we can track regression.","status":"open","priority":2,"issue_type":"task","created_at":"2025-10-27T18:53:10.38679-07:00","updated_at":"2025-10-30T17:12:58.194901-07:00"} -{"id":"bd-aydr.2","title":"Implement backup functionality for reset","description":"Add backup capability that can be used by reset command.\n\n## Functionality\n- Copy .beads/ to .beads-backup-{timestamp}/\n- Timestamp format: YYYYMMDD-HHMMSS\n- Preserve file permissions\n- Return backup path for user feedback\n\n## Location\n`internal/reset/backup.go` - keep with reset package for now (YAGNI)\n\n## Interface\n```go\nfunc CreateBackup(beadsDir string) (backupPath string, err error)\n```\n\n## Notes\n- Simple recursive file copy, no compression needed\n- Error if backup dir already exists (unlikely with timestamp)\n- Backup directories SHOULD be gitignored\n- Add `.beads-backup-*/` pattern to .beads/.gitignore template in doctor package\n- Consider: ListBackups() for future `bd backup list` command (not for this PR)","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-13T08:44:51.306103+11:00","updated_at":"2025-12-13T10:13:32.610819+11:00","closed_at":"2025-12-13T09:20:20.590488+11:00","dependencies":[{"issue_id":"bd-aydr.2","depends_on_id":"bd-aydr","type":"parent-child","created_at":"2025-12-13T08:44:51.306474+11:00","created_by":"daemon"}]} -{"id":"bd-7bbc4e6a","title":"Add MCP server functions for repair commands","description":"**Summary:** Added MCP server repair functions for agent dependency management, system validation, and pollution detection. Implemented across BdClientBase, BdCliClient, and daemon clients to enhance system diagnostics and self-healing capabilities.\n\n**Key Decisions:** \n- Expose repair_deps(), detect_pollution(), validate() via MCP server\n- Create abstract method stubs with fallback to CLI execution\n- Use @mcp.tool decorators for function registration\n\n**Resolution:** Successfully implemented comprehensive repair command infrastructure, enabling more robust system health monitoring and automated remediation with full CLI and daemon support.","notes":"Implemented all three MCP server functions:\n\n1. **repair_deps(fix=False)** - Find/fix orphaned dependencies\n2. **detect_pollution(clean=False)** - Detect/clean test issues \n3. **validate(checks=None, fix_all=False)** - Run comprehensive health checks\n\nChanges:\n- Added abstract methods to BdClientBase\n- Implemented in BdCliClient (CLI execution)\n- Added NotImplementedError stubs in BdDaemonClient (falls back to CLI)\n- Created wrapper functions in tools.py\n- Registered @mcp.tool decorators in server.py\n\nAll commands tested and working with --no-daemon flag.","status":"closed","priority":2,"issue_type":"task","created_at":"2025-10-28T19:37:55.72639-07:00","updated_at":"2025-12-16T01:08:11.983953-08:00","closed_at":"2025-11-07T19:38:12.152437-08:00"} -{"id":"bd-7h7","title":"bd init should stop running daemon to avoid stale cache","description":"When running bd init, any running daemon continues with stale cached data, causing bd stats and other commands to show old counts.\n\nRepro:\n1. Have daemon running with 788 issues cached\n2. Clean JSONL to 128 issues, delete db, run bd init\n3. bd stats still shows 788 (daemon cache)\n4. Must manually run bd daemon --stop\n\nFix: bd init should automatically stop any running daemon before reinitializing.","status":"tombstone","priority":2,"issue_type":"bug","created_at":"2025-12-16T13:26:47.117226-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-bgm","title":"Fix unparam unused parameter in cmd/bd/doctor.go:1879","description":"Linting issue: checkGitHooks - path is unused (unparam) at cmd/bd/doctor.go:1879:20. Error: func checkGitHooks(path string) doctorCheck {","status":"open","issue_type":"bug","created_at":"2025-12-07T15:35:25.270293252-07:00","updated_at":"2025-12-07T15:35:25.270293252-07:00"} -{"id":"bd-b6xo","title":"Remove or fix ClearDirtyIssues() - race condition risk (bd-52)","description":"Code health review found internal/storage/sqlite/dirty.go still exposes old ClearDirtyIssues() method (lines 103-108) which clears ALL dirty issues without checking what was actually exported.\n\nData loss risk: If export fails after some issues written to JSONL but before ClearDirtyIssues called, changes to remaining dirty issues will be lost.\n\nThe safer ClearDirtyIssuesByID() (lines 113-132) exists and clears only exported issues.\n\nFix: Either remove old method or mark it deprecated and ensure no code paths use it.","status":"open","priority":1,"issue_type":"bug","created_at":"2025-12-16T18:17:20.534625-08:00","updated_at":"2025-12-16T18:17:20.534625-08:00","dependencies":[{"issue_id":"bd-b6xo","depends_on_id":"bd-tggf","type":"blocks","created_at":"2025-12-16T18:19:05.633738-08:00","created_by":"daemon"}]} -{"id":"bd-tbz3","title":"bd init UX Improvements","description":"bd init leaves users with incomplete setup, requiring manual bd doctor --fix. Issues found: (1) git hooks not installed if user declines prompt, (2) no auto-migration when CLI is upgraded, (3) stale merge driver configs from old versions. Fix by making bd init more robust with better defaults and auto-migration.","status":"open","priority":1,"issue_type":"epic","created_at":"2025-11-21T23:16:00.333543-08:00","updated_at":"2025-11-21T23:16:37.811233-08:00"} -{"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-aydr.8","title":"Respond to GitHub issue #479 with solution","description":"Once bd reset is implemented and released, respond to GitHub issue #479.\n\n## Response should include\n- Announce the new bd reset command\n- Show basic usage examples\n- Link to any documentation\n- Thank the user for the feedback\n\n## Example response\n```\nThanks for raising this! We've added a `bd reset` command to handle this case.\n\nUsage:\n- `bd reset` - Reset to clean state (prompts for confirmation)\n- `bd reset --backup` - Create backup first\n- `bd reset --hard` - Also clean up git history\n\nThis is available in version X.Y.Z.\n```\n\n## Notes\n- Wait until feature is merged and released\n- Consider if issue should be closed or left for user confirmation","status":"closed","priority":3,"issue_type":"task","created_at":"2025-12-13T08:45:00.112351+11:00","updated_at":"2025-12-13T06:24:29.562177-08:00","closed_at":"2025-12-13T10:18:06.646796+11:00","dependencies":[{"issue_id":"bd-aydr.8","depends_on_id":"bd-aydr","type":"parent-child","created_at":"2025-12-13T08:45:00.112732+11:00","created_by":"daemon"},{"issue_id":"bd-aydr.8","depends_on_id":"bd-aydr.7","type":"blocks","created_at":"2025-12-13T08:45:12.640243+11:00","created_by":"daemon"}]} -{"id":"bd-g9eu","title":"Investigate TestRoutingIntegration failure","description":"TestRoutingIntegration/maintainer_with_SSH_remote failed during pre-commit check with \"expected role maintainer, got contributor\".\nThis occurred while running `go test -short ./...` on darwin/arm64.\nThe failure appears unrelated to storage/sqlite changes.\nNeed to investigate if this is a flaky test or environmental issue.","status":"open","priority":2,"issue_type":"task","created_at":"2025-11-20T15:55:19.337094-08:00","updated_at":"2025-11-20T15:55:19.337094-08:00"} -{"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-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-au0","title":"Command Set Standardization \u0026 Flag Consistency","description":"Comprehensive improvements to bd command set based on 2025 audit findings.\n\n## Background\nSee docs/command-audit-2025.md for detailed analysis.\n\n## Goals\n1. Standardize flag naming and behavior across all commands\n2. Add missing flags for feature parity\n3. Fix naming confusion\n4. Improve consistency in JSON output\n\n## Success Criteria\n- All mutating commands support --dry-run (no --preview variants)\n- bd update supports label operations\n- bd search has filter parity with bd list\n- Priority flags accept both int and P0-P4 format everywhere\n- JSON output is consistent across all commands","status":"open","priority":2,"issue_type":"epic","created_at":"2025-11-21T21:05:55.672749-05:00","updated_at":"2025-11-21T21:05:55.672749-05:00"} -{"id":"bd-vpan","title":"Re: Thread Test 2","description":"Got your message. Testing reply feature.","status":"tombstone","priority":2,"issue_type":"message","created_at":"2025-12-16T18:21:29.144352-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":"message"} -{"id":"bd-4ec8","title":"Widespread double JSON encoding bug in daemon mode RPC calls","description":"Multiple CLI commands had the same double JSON encoding bug found in bd-1048. All commands that called ResolveID via RPC used string(resp.Data) instead of properly unmarshaling the JSON response. This caused IDs to retain JSON quotes (\"bd-1048\" instead of bd-1048), which then got double-encoded when passed to subsequent RPC calls.\n\nAffected commands:\n- bd show (3 instances)\n- bd dep add/remove/tree (5 instances)\n- bd label add/remove/list (3 instances)\n- bd reopen (1 instance)\n\nRoot cause: resp.Data is json.RawMessage (already JSON-encoded), so string() conversion preserves quotes.\n\nFix: Replace all string(resp.Data) with json.Unmarshal(resp.Data, \u0026id) for proper deserialization.\n\nAll commands now tested and working correctly with daemon mode.","status":"open","issue_type":"bug","created_at":"2025-11-02T22:33:01.632691-08:00","updated_at":"2025-11-02T22:33:01.632691-08:00"} -{"id":"bd-au0.6","title":"Add comprehensive filters to bd export","description":"Enhance bd export with filtering options for selective exports.\n\n**Currently only has:**\n- --status\n\n**Add filters:**\n- --label, --label-any\n- --assignee\n- --type\n- --priority, --priority-min, --priority-max\n- --created-after, --created-before\n- --updated-after, --updated-before\n\n**Use case:**\n- Export only open issues: bd export --status open\n- Export high-priority bugs: bd export --type bug --priority-max 1\n- Export recent issues: bd export --created-after 2025-01-01\n\n**Files to modify:**\n- cmd/bd/export.go\n- Reuse filter logic from list.go","status":"open","priority":1,"issue_type":"task","created_at":"2025-11-21T21:07:19.431307-05:00","updated_at":"2025-11-21T21:07:19.431307-05:00","dependencies":[{"issue_id":"bd-au0.6","depends_on_id":"bd-au0","type":"parent-child","created_at":"2025-11-21T21:07:19.432983-05:00","created_by":"daemon"}]} -{"id":"bd-4q8","title":"bd cleanup --hard should skip tombstone creation for true permanent deletion","description":"## Problem\n\nWhen using bd cleanup --hard --older-than N --force, the command:\n1. Deletes closed issues older than N days (converting them to tombstones with NOW timestamp)\n2. Then tries to prune tombstones older than N days (finds none because they were just created)\n\nThis leaves the database bloated with fresh tombstones that will not be pruned.\n\n## Expected Behavior\n\nIn --hard mode, the deletion should be permanent without creating tombstones, since the user explicitly requested bypassing sync safety.\n\n## Workaround\n\nManually delete from database: sqlite3 .beads/beads.db 'DELETE FROM issues WHERE status=tombstone'\n\n## Fix Options\n\n1. In --hard mode, use a different delete path that does not create tombstones\n2. After deleting, immediately prune the just-created tombstones regardless of age\n3. Pass a skip_tombstone flag to the delete operation\n\nOption 1 is cleanest - --hard should mean permanent delete without tombstone.","status":"tombstone","priority":1,"issue_type":"bug","created_at":"2025-12-16T01:33:36.580657-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-5qim","title":"Optimize GetReadyWork performance - 752ms on 10K database (target: \u003c50ms)","notes":"# Performance Analysis (10K Issue Database)\n\nAnalyzed using CPU profiles from benchmark suite on Apple M2 Pro.\n\n## Operation Performance\n\n| Operation | Time | Allocations | Memory |\n|----------------------------------|---------|-------------|--------|\n| bd ready (GetReadyWork) | ~752ms | 167,466 | 16MB |\n| bd list (SearchIssues no filter) | ~11.6ms | 89,214 | 5.8MB |\n| bd list (SearchIssues filtered) | ~9.2ms | 62,365 | 3.5MB |\n| bd create (CreateIssue) | ~2.6ms | 146 | 8.6KB |\n| bd update (UpdateIssue) | ~0.32ms | 364 | 15KB |\n| bd close (UpdateIssue) | ~0.32ms | 364 | 15KB |\n\n**Target: \u003c50ms for all operations on 10K database**\n\n**Current issue: GetReadyWork is 15x over target (752ms vs 50ms)**\n\n## Root Cause\n\nGetReadyWork (internal/storage/sqlite/ready.go:90-128) uses recursive CTE to propagate blocking:\n- 65x slower than SearchIssues\n- Recalculates entire blocked issue tree on every call\n- Algorithm:\n 1. Find directly blocked issues via 'blocks' dependencies\n 2. Recursively propagate blockage to descendants (max depth: 50)\n 3. Exclude all blocked issues from results\n\n## CPU Profile Analysis\n\n- Database syscalls (pthread_cond_signal, syscall6): ~75%\n- SQLite engine overhead: inherent to recursive CTE\n- Application code (query construction): \u003c1%\n\n**Bottleneck is the recursive CTE query execution, not application code.**\n\n## Optimization Recommendations\n\n### High Impact (Likely to achieve \u003c50ms target)\n\n1. **Cache blocked issue calculation**\n - Add `blocked_issues` table updated on dependency changes\n - Trade write complexity for read speed (ready called \u003e\u003e dependency changes)\n - Eliminates recursive CTE on every read\n\n2. **Add/verify database indexes**\n ```sql\n CREATE INDEX IF NOT EXISTS idx_dependencies_blocked \n ON dependencies(issue_id, type, depends_on_id);\n CREATE INDEX IF NOT EXISTS idx_issues_status \n ON issues(status);\n ```\n\n### Medium Impact\n\n3. **Reduce allocations** (167K allocations for GetReadyWork)\n - Profile `scanIssues()` for object pooling opportunities\n - Reuse slice capacity for repeated calls\n\n### Low Impact (Not recommended)\n- Query optimization for CRUD operations (already \u003c3ms)\n- Connection pooling tuning (not showing in profiles)\n\n## Verification\n\nRun benchmarks to validate optimization:\n```bash\nmake bench-quick\ngo tool pprof -http=:8080 internal/storage/sqlite/bench-cpu-*.prof\n```\n\nProfile files automatically generated in `internal/storage/sqlite/`.","status":"open","issue_type":"bug","created_at":"2025-11-14T09:02:46.507526-08:00","updated_at":"2025-11-14T09:03:44.073236-08:00"} -{"id":"bd-d73u","title":"Re: Thread Test 2","description":"Great! Thread is working well.","status":"tombstone","priority":2,"issue_type":"message","created_at":"2025-12-16T18:21:46.655093-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":"message"} -{"id":"bd-zwtq","title":"Run bd doctor at end of bd init to verify setup","description":"Run bd doctor diagnostics at end of bd init (after line 398 in init.go). If issues found, warn user immediately: '⚠ Setup incomplete. Run bd doctor --fix to complete setup.' Catches configuration problems before user encounters them in normal workflow.","status":"open","priority":1,"issue_type":"feature","created_at":"2025-11-21T23:16:09.596778-08:00","updated_at":"2025-11-21T23:16:27.781976-08:00","dependencies":[{"issue_id":"bd-zwtq","depends_on_id":"bd-tbz3","type":"parent-child","created_at":"2025-11-21T23:16:09.597617-08:00","created_by":"daemon"}]} -{"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":"open","issue_type":"feature","created_at":"2025-11-21T23:16:09.004619-08:00","updated_at":"2025-11-21T23:16:27.229388-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-o4qy","title":"Improve CheckStaleness error handling","description":"## Problem\n\nCheckStaleness returns 'false' (not stale) for multiple error conditions instead of returning errors. This masks problems.\n\n**Location:** internal/autoimport/autoimport.go:253-285\n\n## Edge Cases That Return False\n\n1. **Invalid last_import_time format** (line 259-262)\n2. **No JSONL file found** (line 267-277) \n3. **JSONL stat fails** (line 279-282)\n\n## Fix\n\nReturn errors for abnormal conditions:\n\n```go\nlastImportTime, err := time.Parse(time.RFC3339, lastImportStr)\nif err != nil {\n return false, fmt.Errorf(\"corrupted last_import_time: %w\", err)\n}\n\nif jsonlPath == \"\" {\n return false, fmt.Errorf(\"no JSONL file found\")\n}\n\nstat, err := os.Stat(jsonlPath)\nif err != nil {\n return false, fmt.Errorf(\"cannot stat JSONL: %w\", err)\n}\n```\n\n## Impact\nMedium - edge cases are rare but should be handled\n\n## Effort \n30 minutes - requires updating callers in RPC server","status":"open","priority":2,"issue_type":"bug","created_at":"2025-11-20T20:17:27.606219-05:00","updated_at":"2025-11-20T20:17:27.606219-05:00","dependencies":[{"issue_id":"bd-o4qy","depends_on_id":"bd-2q6d","type":"blocks","created_at":"2025-11-20T20:18:26.81065-05:00","created_by":"stevey"}]} -{"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-47tn","title":"Add bd daemon --stop-all command to kill all daemon processes","description":"Currently there's no easy way to stop all running bd daemon processes. Users must resort to pkill -f 'bd daemon' or similar shell commands.\n\nAdd a --stop-all flag to bd daemon that:\n1. Finds all running bd daemon processes (not just the current repo's daemon)\n2. Gracefully stops them all\n3. Reports how many were stopped\n\nThis is useful when:\n- Multiple daemons are running and causing race conditions\n- User wants a clean slate before running bd sync\n- Debugging daemon-related issues","status":"closed","priority":2,"issue_type":"feature","created_at":"2025-12-13T06:34:45.080633-08:00","updated_at":"2025-12-16T01:14:49.501989-08:00","closed_at":"2025-12-14T17:33:03.057089-08:00"} -{"id":"bd-xctp","title":"GH#519: bd sync fails when sync.branch is currently checked-out branch","status":"tombstone","priority":2,"issue_type":"bug","created_at":"2025-12-16T01:06:05.319281-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-kwro.6","title":"Mail Commands: bd mail send/inbox/read/ack","description":"Implement core mail commands in cmd/bd/mail.go\n\nCommands:\n- bd mail send \u003crecipient\u003e -s 'Subject' -m 'Body' [--urgent]\n - Creates issue with type=message, sender=identity, assignee=recipient\n - --urgent sets priority=0\n \n- bd mail inbox [--from \u003csender\u003e] [--priority \u003cn\u003e]\n - Lists open messages where assignee=my identity\n - Sorted by priority, then date\n \n- bd mail read \u003cid\u003e\n - Shows full message content (subject, body, sender, timestamp)\n - Does NOT close (separate from ack)\n \n- bd mail ack \u003cid\u003e\n - Marks message as read by closing it\n - Can ack multiple: bd mail ack \u003cid1\u003e \u003cid2\u003e ...\n\nRequires: Identity configuration (bd-kwro.7)","status":"tombstone","issue_type":"task","created_at":"2025-12-16T03:02:12.103755-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-6sm6","title":"Improve test coverage for internal/export (37.1% → 60%)","description":"The export package has only 37.1% test coverage. Export functionality needs good coverage to ensure data integrity.\n\nCurrent coverage: 37.1%\nTarget coverage: 60%","status":"open","priority":2,"issue_type":"task","created_at":"2025-12-13T20:43:06.802277-08:00","updated_at":"2025-12-13T21:01:19.08088-08:00"} -{"id":"bd-kwro.4","title":"Graph Link: duplicates for deduplication","description":"Implement duplicates link type for marking issues as duplicates.\n\nNew command:\n- bd duplicate \u003cid\u003e --of \u003ccanonical\u003e - marks id as duplicate of canonical\n- Auto-closes the duplicate issue\n\nQuery support:\n- bd show \u003cid\u003e shows 'Duplicate of: \u003ccanonical\u003e'\n- bd list --duplicates shows all duplicate pairs\n\nStorage:\n- duplicates column pointing to canonical issue ID\n\nEssential for large issue databases with many similar reports.","status":"tombstone","priority":1,"issue_type":"task","created_at":"2025-12-16T03:01:36.257223-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-fom","title":"Remove all deletions.jsonl code except migration","description":"There's deletions manifest code spread across the entire codebase that should have been removed after tombstone migration:\n\nFiles with deletions code (non-migration):\n- internal/deletions/ - entire package\n- cmd/bd/sync.go - 25+ references, auto-compact, sanitize\n- cmd/bd/delete.go - dual-writes to deletions.jsonl\n- internal/importer/importer.go - checks deletions manifest\n- internal/syncbranch/worktree.go - merges deletions.jsonl\n- cmd/bd/doctor/fix/sync.go - cleanupDeletionsManifest\n- cmd/bd/doctor/fix/deletions.go - HydrateDeletionsManifest\n- cmd/bd/integrity.go - checks deletions for data loss\n- cmd/bd/deleted.go - entire command\n- cmd/bd/compact.go - pruneDeletionsManifest\n- cmd/bd/doctor.go - checkDeletionsManifest\n- Plus many more\n\nAction: Aggressively remove all non-migration deletions code. Tombstones are the only deletion mechanism now.","status":"tombstone","priority":1,"issue_type":"task","created_at":"2025-12-16T13:29:04.960863-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.11","title":"Documentation for messaging and graph links","description":"Document all new features.\n\nFiles to update:\n- README.md - brief mention of messaging capability\n- AGENTS.md - update for AI agents using bd mail\n- docs/messaging.md (new) - full messaging reference\n- docs/graph-links.md (new) - graph link reference\n- CHANGELOG.md - v0.30.2 release notes\n\nTopics to cover:\n- Mail commands with examples\n- Graph link types and use cases\n- Identity configuration\n- Hooks setup for notifications\n- Migration notes","status":"tombstone","priority":2,"issue_type":"task","created_at":"2025-12-16T03:02:39.548518-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-s0qf","title":"GH#405: Fix prefix parsing with hyphens - multi-hyphen prefixes parsed incorrectly","description":"Fixed: ExtractIssuePrefix was falling back to first-hyphen for word-like suffixes, breaking multi-hyphen prefixes like 'hacker-news' and 'me-py-toolkit'.","status":"tombstone","priority":2,"issue_type":"bug","created_at":"2025-12-16T01:13:56.951359-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-ykd9","title":"Add bd doctor --fix flag to automatically repair issues","description":"Implement a --fix flag for bd doctor that can automatically repair detected issues.\n\nRequirements:\n- Add --fix flag to bd doctor command\n- Show all fixable issues and prompt for confirmation before applying fixes\n- Organize fix implementations under doctor/fix/\u003ctype_of_fix\u003e.go\n- Each fix type should have its own file (e.g., doctor/fix/hooks.go, doctor/fix/sync.go)\n- Display what will be fixed and ask user to confirm (Y/n) before proceeding\n- Support fixing issues like:\n - Missing or broken git hooks\n - Sync problems with remote\n - File permission issues\n - Any other auto-repairable issues doctor detects\n\nImplementation notes:\n- Maintain separation between detection (existing doctor code) and repair (new fix code)\n- Each fix should be idempotent and safe to run multiple times\n- Provide clear output about what was fixed\n- Log any fixes that fail with actionable error messages","status":"open","priority":2,"issue_type":"feature","created_at":"2025-11-14T18:17:48.411264-08:00","updated_at":"2025-11-14T18:17:58.88609-08:00"} -{"id":"bd-lfak","title":"bd preflight: PR readiness checks for contributors","description":"## Vision\n\nEncode project-specific institutional knowledge into executable checks. CONTRIBUTING.md is documentation that's read once and forgotten; `bd preflight` is documentation that runs at exactly the right moment.\n\n## Problem Statement\n\nContributors face a \"last mile\" problem - they do the work but stumble on project-specific gotchas at PR time:\n- Nix vendorHash gets stale when go.sum changes\n- Beads artifacts leak into PRs (see bd-umbf for namespace solution)\n- Version mismatches between version.go and default.nix\n- Tests/lint not run locally before pushing\n- Other project-specific checks that only surface when CI fails\n\nThese are too obscure to remember, exist in docs nobody reads end-to-end, and waste CI round-trips.\n\n## Why beads?\n\nBeads already has a foothold in the contributor workflow. It knows:\n- Git state (staged files, branch, dirty status)\n- Project structure\n- The specific issue being worked on\n- Project-specific configuration\n\n## Proposed Interface\n\n### Tier 1: Checklist Mode (v1)\n\n $ bd preflight\n PR Readiness Checklist:\n\n [ ] Tests pass: go test -short ./...\n [ ] Lint passes: golangci-lint run ./...\n [ ] No beads pollution: check .beads/issues.jsonl diff\n [ ] Nix hash current: go.sum unchanged or vendorHash updated\n [ ] Version sync: version.go matches default.nix\n\n Run 'bd preflight --check' to validate automatically.\n\n### Tier 2: Check Mode (v2)\n\n $ bd preflight --check\n āœ“ Tests pass\n āœ“ Lint passes\n ⚠ Beads pollution: 3 issues in diff - are these project issues or personal?\n āœ— Nix hash stale: go.sum changed, vendorHash needs update\n Fix: sha256-KRR6dXzsSw8OmEHGBEVDBOoIgfoZ2p0541T9ayjGHlI=\n āœ“ Version sync\n\n 1 error, 1 warning. Run 'bd preflight --fix' to auto-fix where possible.\n\n### Tier 3: Fix Mode (v3)\n\n $ bd preflight --fix\n āœ“ Updated vendorHash in default.nix\n ⚠ Cannot auto-fix beads pollution - manual review needed\n\n## Checks to Implement\n\n| Check | Description | Auto-fixable |\n|-------|-------------|--------------|\n| tests | Run go test -short ./... | No |\n| lint | Run golangci-lint | Partial (gofmt) |\n| beads-pollution | Detect personal issues in diff | No (see bd-umbf) |\n| nix-hash | Detect stale vendorHash | Yes (if nix available) |\n| version-sync | version.go matches default.nix | Yes |\n| no-debug | No TODO/FIXME/console.log | Warn only |\n| clean-stage | No unintended files staged | Warn only |\n\n## Future: Configuration\n\nMake checks configurable per-project via .beads/preflight.yaml:\n\n preflight:\n checks:\n - name: tests\n run: go test -short ./...\n required: true\n - name: no-secrets\n pattern: \"**/*.env\"\n staged: deny\n - name: custom-check\n run: ./scripts/validate.sh\n\nThis lets any project using beads define their own preflight checks.\n\n## Implementation Phases\n\n### Phase 1: Static Checklist\n- Implement bd preflight with hardcoded checklist for beads\n- No execution, just prints what to check\n- Update CONTRIBUTING.md to reference it\n\n### Phase 2: Automated Checks\n- Implement bd preflight --check\n- Run tests, lint, detect stale hashes\n- Clear pass/fail/warn output\n\n### Phase 3: Auto-fix\n- Implement bd preflight --fix\n- Fix vendorHash, version sync\n- Integrate with bd-umbf solution for pollution\n\n### Phase 4: Configuration\n- .beads/preflight.yaml support\n- Make it useful for other projects using beads\n- Plugin/hook system for custom checks\n\n## Dependencies\n\n- bd-umbf: Namespace isolation for beads pollution (blocking for full solution)\n\n## Success Metrics\n\n- Fewer CI failures on first PR push\n- Reduced \"fix nix hash\" commits\n- Contributors report preflight caught issues before CI","status":"open","priority":2,"issue_type":"epic","created_at":"2025-12-13T18:01:39.587078-08:00","updated_at":"2025-12-13T18:01:39.587078-08:00","dependencies":[{"issue_id":"bd-lfak","depends_on_id":"bd-umbf","type":"blocks","created_at":"2025-12-13T18:01:46.059901-08:00","created_by":"daemon"}]} -{"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-x36g","title":"Thread Test 2","description":"Testing direct mode","status":"tombstone","priority":2,"issue_type":"message","created_at":"2025-12-16T18:21:16.470631-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":"message"} -{"id":"bd-4nqq","title":"Remove dead test code in info_test.go","description":"Code health review found cmd/bd/info_test.go has two tests permanently skipped:\n\n- TestInfoCommand\n- TestInfoCommandNoDaemon\n\nBoth skip with: 'Manual test - bd info command is working, see manual testing'\n\nThese are essentially dead code. Either automate them or remove them entirely.","status":"open","priority":3,"issue_type":"task","created_at":"2025-12-16T18:17:27.554019-08:00","updated_at":"2025-12-16T18:17:27.554019-08:00","dependencies":[{"issue_id":"bd-4nqq","depends_on_id":"bd-tggf","type":"blocks","created_at":"2025-12-16T18:19:06.381694-08:00","created_by":"daemon"}]} -{"id":"bd-9g1z","title":"Fix or remove TestFindJSONLPathDefault (issue #356)","description":"Code health review found .test-skip permanently skips TestFindJSONLPathDefault.\n\nThe test references issue #356 about wrong JSONL filename expectations (issues.jsonl vs beads.jsonl).\n\nTest file: internal/beads/beads_test.go\n\nThe underlying migration from beads.jsonl to issues.jsonl may be complete, so either:\n1. Fix the test expectations\n2. Remove the test if no longer needed\n3. Document why it remains skipped","status":"open","priority":2,"issue_type":"bug","created_at":"2025-12-16T18:17:31.33975-08:00","updated_at":"2025-12-16T18:17:31.33975-08:00","dependencies":[{"issue_id":"bd-9g1z","depends_on_id":"bd-tggf","type":"blocks","created_at":"2025-12-16T18:19:06.169617-08:00","created_by":"daemon"}]} -{"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-16T01:00:40.256809-08:00","closed_at":"2025-10-28T10:49:45.021037-07:00"} -{"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"} -{"id":"bd-aydr.6","title":"Add unit tests for reset package","description":"Comprehensive unit tests for internal/reset package.\n\n## Test Cases\n\n### ValidateState tests\n- .beads/ exists → success\n- .beads/ missing → appropriate error\n- git dirty state detection\n\n### CountImpact tests \n- Empty .beads/ → zero counts\n- With issues → correct count (open vs closed)\n- With tombstones → correct count\n- Returns HasUncommitted correctly\n\n### Backup tests\n- Creates backup with correct timestamp format\n- Preserves all files and permissions\n- Returns correct path\n- Handles missing .beads/ gracefully\n- Errors on pre-existing backup dir\n\n### Git operation tests\n- CheckGitState detects dirty, detached, not-a-repo\n- GitRemoveBeads removes correct files\n- GitCommitReset creates commit with message\n- Operations skip gracefully when not in git repo\n\n### Reset tests (with mocks/temp dirs)\n- Soft reset removes files, calls init\n- Hard reset includes git operations\n- Dry run doesn't modify anything\n- SkipInit flag prevents re-initialization\n- Daemon killall is called\n- Backup is created when requested\n\n## Approach\n- Can start with interface definitions (TDD style)\n- Use testify for assertions\n- Create temp directories for isolation\n- Mock git operations where needed\n- Test completion depends on implementation tasks\n\n## File Location\n`internal/reset/reset_test.go`\n`internal/reset/backup_test.go`\n`internal/reset/git_test.go`","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-13T08:44:57.01739+11:00","updated_at":"2025-12-13T10:13:32.611698+11:00","closed_at":"2025-12-13T09:59:20.820314+11:00","dependencies":[{"issue_id":"bd-aydr.6","depends_on_id":"bd-aydr","type":"parent-child","created_at":"2025-12-13T08:44:57.017813+11:00","created_by":"daemon"}]} -{"id":"bd-ziy5","title":"GH#409: bd init uses issues.jsonl but docs say beads.jsonl","description":"bd init creates config referencing issues.jsonl but README/docs reference beads.jsonl as canonical. Standardize naming. See GitHub issue #409.","status":"tombstone","priority":2,"issue_type":"bug","created_at":"2025-12-16T01:03:58.109954-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-kwro.7","title":"Identity Configuration","description":"Implement identity system for sender field.\n\nConfiguration sources (in priority order):\n1. --identity flag on commands\n2. BEADS_IDENTITY environment variable\n3. .beads/config.json: {\"identity\": \"worker-name\"}\n4. Default: git user.name or hostname\n\nNew config file support:\n- .beads/config.json for per-repo settings\n- identity field for messaging\n\nHelper function:\n- GetIdentity() string - resolves identity from sources\n\nUpdate bd mail send to use GetIdentity() for sender field.","status":"tombstone","priority":1,"issue_type":"task","created_at":"2025-12-16T03:02:17.603608-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-7m16","title":"GH#519: bd sync fails when sync.branch is currently checked-out branch","description":"bd sync tries to create worktree for sync.branch even when already on that branch. Should commit directly instead. See GitHub issue #519.","status":"tombstone","priority":2,"issue_type":"bug","created_at":"2025-12-16T01:03:36.613211-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-n3v","title":"Error committing to sync branch: failed to create worktree","description":"\u003e bd sync --no-daemon\n→ Exporting pending changes to JSONL...\n→ Committing changes to sync branch 'beads-sync'...\nError committing to sync branch: failed to create worktree: failed to create worktree parent directory: mkdir /var/home/matt/dev/beads/fix-ci/.git: not a directory","notes":"**Problem Diagnosed**: The `bd sync` command was failing with \"mkdir /var/home/matt/dev/beads/fix-ci/.git: not a directory\" because it was being executed from the wrong directory.\n\n**Root Cause**: The command was run from `/var/home/matt/dev/beads` (where the `fix-ci` worktree exists) instead of the main repository directory `/var/home/matt/dev/beads/main`. Since `fix-ci` is a git worktree with a `.git` file (not directory), the worktree creation logic failed when trying to create `\u003ccurrent_dir\u003e/.git/beads-worktrees/\u003cbranch\u003e`.\n\n**Solution Verified**: Execute `bd sync` from the main repository directory:\n```bash\ncd main \u0026\u0026 bd sync --dry-run\n```\n","status":"open","priority":2,"issue_type":"task","created_at":"2025-12-05T15:25:24.514998248-07:00","updated_at":"2025-12-05T15:42:32.910166956-07:00"} -{"id":"bd-fu83","title":"Fix daemon/direct mode inconsistency in relate and duplicate commands","description":"The relate.go and duplicate.go commands have inconsistent daemon/direct mode handling:\n\nWhen daemonClient is connected, they resolve IDs via RPC but then perform updates directly via store.UpdateIssue(), bypassing the daemon.\n\nAffected locations:\n- relate.go:125-139 (runRelate update)\n- relate.go:235-246 (runUnrelate update) \n- duplicate.go:120 (runDuplicate update)\n- duplicate.go:207 (runSupersede update)\n\nShould either use RPC for updates when daemon is running, or document why direct access is intentional.","status":"open","priority":2,"issue_type":"bug","created_at":"2025-12-16T20:52:54.164189-08:00","updated_at":"2025-12-16T20:52:54.164189-08:00"} -{"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-16T01:00:45.930945-08:00","closed_at":"2025-10-28T20:03:26.675257-07:00","dependencies":[{"issue_id":"bd-cbed9619.3","depends_on_id":"bd-325da116","type":"parent-child","created_at":"2025-10-28T18:39:20.593102-07:00","created_by":"daemon"},{"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-16T01:00:45.105283-08:00","closed_at":"2025-10-28T19:08:17.715416-07:00","dependencies":[{"issue_id":"bd-cbed9619.4","depends_on_id":"bd-325da116","type":"parent-child","created_at":"2025-10-28T18:39:20.570276-07:00","created_by":"daemon"},{"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-m8ro","title":"Improve test coverage for internal/rpc (47.5% → 60%)","description":"The RPC package has only 47.5% test coverage. RPC is the communication layer for daemon operations.\n\nCurrent coverage: 47.5%\nTarget coverage: 60%","status":"open","priority":2,"issue_type":"task","created_at":"2025-12-13T20:43:09.515299-08:00","updated_at":"2025-12-13T21:01:17.17404-08:00"} -{"id":"bd-qioh","title":"Standardize error handling: replace direct fmt.Fprintf+os.Exit with FatalError","description":"Code health review found inconsistent error handling patterns:\n\nPattern A (preferred): FatalError() from errors.go:21-24\nPattern B (inconsistent): Direct fmt.Fprintf(os.Stderr) + os.Exit(1)\n\nAffected files:\n- delete.go:35-49 uses direct pattern\n- create.go:213 ignores error silently\n- Various other commands\n\nFix: Standardize on FatalError() wrapper for consistent error messages and testability.","status":"open","priority":2,"issue_type":"task","created_at":"2025-12-16T18:17:19.309394-08:00","updated_at":"2025-12-16T18:17:19.309394-08:00","dependencies":[{"issue_id":"bd-qioh","depends_on_id":"bd-tggf","type":"blocks","created_at":"2025-12-16T18:19:05.953559-08:00","created_by":"daemon"}]} -{"id":"bd-sh4c","title":"Improve test coverage for cmd/bd/setup (28.4% → 50%)","description":"The setup package has only 28.4% test coverage. Setup commands are critical for first-time user experience.\n\nCurrent coverage: 28.4%\nTarget coverage: 50%","status":"open","priority":2,"issue_type":"task","created_at":"2025-12-13T20:43:04.409346-08:00","updated_at":"2025-12-13T21:01:18.98833-08:00"} -{"id":"bd-a0cp","title":"Consider using types.Status in merge package for type safety","description":"The merge package uses string for status comparison (e.g., result.Status == closed, issue.Status == StatusTombstone). The types package defines Status as a type alias with validation. While the merge package needs its own Issue struct for JSONL flexibility, it could import and use types.Status for constants to get compile-time type checking. Current code: if left == closed || right == closed. Could be: if left == string(types.StatusClosed). This is low priority since string comparison works correctly. Files: internal/merge/merge.go:44, 488, 501-521","status":"open","priority":4,"issue_type":"task","created_at":"2025-12-05T16:37:10.690424-08:00","updated_at":"2025-12-05T16:37:10.690424-08:00"} -{"id":"bd-6rl","title":"Merge3Way public API does not expose TTL parameter","description":"The public Merge3Way() function in merge.go does not allow callers to configure the tombstone TTL. It hard-codes the default via merge3WayWithTTL(). While merge3WayWithTTL() exists, it is unexported (lowercase). This means the CLI and tests cannot configure TTL at merge time. Use cases: testing with different TTL values, per-repository TTL configuration, debugging with short TTL, supporting --ttl flag in bd merge command (mentioned in design doc bd-zvg). Recommendation: Export Merge3WayWithTTL (rename to uppercase). Files: internal/merge/merge.go:77, 292-298","status":"open","priority":3,"issue_type":"feature","created_at":"2025-12-05T16:36:15.756814-08:00","updated_at":"2025-12-05T16:36:15.756814-08:00"} -{"id":"bd-lsv4","title":"GH#444: Fix inconsistent status naming in_progress vs in-progress","description":"Documentation uses in-progress (hyphen) but code expects in_progress (underscore). Update all docs to use canonical in_progress. See GitHub issue #444.","status":"tombstone","priority":2,"issue_type":"bug","created_at":"2025-12-16T01:03:14.349425-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-74w1","title":"Consolidate duplicate path-finding utilities (findJSONLPath, findBeadsDir, findGitRoot)","description":"Code health review found these functions defined in multiple places:\n\n- findJSONLPath() in autoflush.go:45-73 and doctor/fix/migrate.go\n- findBeadsDir() in autoimport.go:197-239 (with git worktree handling)\n- findGitRoot() in autoimport.go:242-269 (Windows path conversion)\n\nThe beads package has public FindBeadsDir() and FindJSONLPath() APIs that should be used consistently.\n\nImpact: Bug fixes need to be applied in multiple places. Git worktree handling may not be replicated everywhere.\n\nFix: Consolidate all implementations to use the beads package APIs. Remove duplicates.","status":"open","priority":1,"issue_type":"task","created_at":"2025-12-16T18:17:16.694293-08:00","updated_at":"2025-12-16T18:17:16.694293-08:00","dependencies":[{"issue_id":"bd-74w1","depends_on_id":"bd-tggf","type":"blocks","created_at":"2025-12-16T18:19:05.526122-08:00","created_by":"daemon"}]} {"id":"bd-umbf","title":"Design contributor namespace isolation for beads pollution prevention","description":"## Problem\n\nWhen contributors work on beads-the-project using beads-the-tool, their personal work-tracking issues leak into PRs. The .beads/issues.jsonl is intentionally tracked (it's the project's issue database), but contributors' local issues pollute the diff.\n\nThis is a recursion problem unique to self-hosting projects.\n\n## Possible Solutions to Explore\n\n1. **Contributor namespaces** - Each contributor gets a private prefix (e.g., `bd-steve-xxxx`) that's gitignored or filtered\n2. **Separate database** - Contributors use BEADS_DIR pointing elsewhere for personal tracking\n3. **Issue ownership/visibility flags** - Mark issues as \"local-only\" vs \"project\"\n4. **Prefix-based filtering** - Configure which prefixes are committed vs ignored\n\n## Design Considerations\n\n- Should be zero-friction for contributors (no manual setup)\n- Must not break existing workflows\n- Needs to work with sync/collaboration features\n- Consider: what if a \"personal\" issue graduates to \"project\" issue?\n\n## Expansion Needed\n\nThis is a placeholder. Needs detailed design exploration before implementation.","status":"open","priority":2,"issue_type":"feature","created_at":"2025-12-13T18:00:29.638743-08:00","updated_at":"2025-12-13T18:00:41.345673-08:00"} +{"id":"bd-vpan","title":"Re: Thread Test 2","description":"Got your message. Testing reply feature.","status":"tombstone","priority":2,"issue_type":"message","created_at":"2025-12-16T18:21:29.144352-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":"message"} +{"id":"bd-vs9","title":"Fix unparam unused parameter in cmd/bd/doctor.go:541","description":"Linting issue: checkHooksQuick - path is unused (unparam) at cmd/bd/doctor.go:541:22. Error: func checkHooksQuick(path string) string {","status":"closed","priority":0,"issue_type":"bug","created_at":"2025-12-07T15:35:17.02177046-07:00","updated_at":"2025-12-17T23:13:40.535743-08:00","closed_at":"2025-12-17T16:46:11.028332-08:00"} +{"id":"bd-wc2","title":"Test body-file","description":"This is a test description from a file.\n\nIt has multiple lines.\n","status":"closed","priority":4,"issue_type":"task","created_at":"2025-12-17T17:27:20.508724-08:00","updated_at":"2025-12-17T17:28:33.83142-08:00","closed_at":"2025-12-17T17:28:33.83142-08:00"} +{"id":"bd-x36g","title":"Thread Test 2","description":"Testing direct mode","status":"tombstone","priority":2,"issue_type":"message","created_at":"2025-12-16T18:21:16.470631-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":"message"} +{"id":"bd-xctp","title":"GH#519: bd sync fails when sync.branch is currently checked-out branch","description":"","status":"tombstone","priority":2,"issue_type":"bug","created_at":"2025-12-16T01:06:05.319281-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-xj2e","title":"GH#522: Add --type flag to bd update command","description":"Add --type flag to bd update for changing issue type (task/epic/bug/feature). Storage layer already supports it. See GitHub issue #522.","status":"tombstone","priority":2,"issue_type":"task","created_at":"2025-12-16T01:03:12.506583-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-y2v","title":"Refactor duplicate JSONL-from-git parsing code","description":"Both readFirstIssueFromGit() in init.go and importFromGit() in autoimport.go have similar code patterns for:\n1. Running git show \u003cref\u003e:\u003cpath\u003e\n2. Scanning the output with bufio.Scanner\n3. Parsing JSON lines\n\nCould be refactored to share a helper like:\n- readJSONLFromGit(gitRef, path string) ([]byte, error)\n- Or a streaming version: streamJSONLFromGit(gitRef, path string) (io.Reader, error)\n\nFiles:\n- cmd/bd/autoimport.go:225-256 (importFromGit)\n- cmd/bd/init.go:1212-1243 (readFirstIssueFromGit)\n\nPriority is low since code duplication is minimal and both functions work correctly.","status":"open","priority":2,"issue_type":"task","created_at":"2025-12-05T14:51:18.41124-08:00","updated_at":"2025-12-05T14:51:18.41124-08:00"} {"id":"bd-yck","title":"Fix checkExistingBeadsData to be worktree-aware","description":"The checkExistingBeadsData function in cmd/bd/init.go checks for .beads in the current working directory, but for worktrees it should check the main repository root instead. This prevents proper worktree compatibility.","status":"open","priority":2,"issue_type":"bug","created_at":"2025-12-07T16:48:32.082776345-07:00","updated_at":"2025-12-07T16:48:32.082776345-07:00"} -{"id":"bd-gjla","title":"Test Thread","description":"Initial message for threading test","status":"tombstone","priority":2,"issue_type":"message","created_at":"2025-12-16T18:19:51.704324-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":"message"} -{"id":"bd-lw0x","title":"Fix bd sync race condition with daemon causing dirty working directory","description":"After bd sync completes with sync.branch mode, subsequent bd commands or daemon file watcher would see a hash mismatch and trigger auto-import, which then schedules re-export, dirtying the working directory.\n\n**Root cause:**\n1. bd sync exports JSONL with NEW content (hash H1)\n2. bd sync updates jsonl_content_hash = H1 in DB\n3. bd sync restores JSONL from HEAD (OLD content, hash H0)\n4. Now: file hash = H0, DB hash = H1 (MISMATCH)\n5. Daemon or next CLI command sees mismatch, imports from OLD JSONL\n6. Import triggers re-export → file is dirty\n\n**Fix:**\nAfter restoreBeadsDirFromBranch(), update jsonl_content_hash to match the restored file's hash. This ensures daemon and CLI see file hash = DB hash → no spurious import/export cycle.\n\nRelated: bd-c83r (multiple daemon prevention)","status":"closed","priority":2,"issue_type":"bug","created_at":"2025-12-13T06:42:17.130839-08:00","updated_at":"2025-12-13T06:43:33.329042-08:00","closed_at":"2025-12-13T06:43:33.329042-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-au0.7","title":"Audit and standardize JSON output across all commands","description":"Ensure consistent JSON format and error handling when --json flag is used.\n\n**Scope:**\n1. Verify all commands respect --json flag\n2. Standardize success response format\n3. Standardize error response format\n4. Document JSON schemas\n\n**Commands to audit:**\n- Core CRUD: create, update, delete, show, list, search āœ“\n- Queries: ready, blocked, stale, count, stats, status\n- Deps: dep add/remove/tree/cycles\n- Labels: label commands\n- Comments: comments add/list/delete\n- Epics: epic status/close-eligible\n- Export/import: already support --json āœ“\n\n**Testing:**\n- Success cases return valid JSON\n- Error cases return valid JSON (not plain text)\n- Consistent field naming (snake_case vs camelCase)\n- Array vs object wrapping consistency","status":"open","priority":1,"issue_type":"task","created_at":"2025-11-21T21:07:35.304424-05:00","updated_at":"2025-11-21T21:07:35.304424-05:00","dependencies":[{"issue_id":"bd-au0.7","depends_on_id":"bd-au0","type":"parent-child","created_at":"2025-11-21T21:07:35.305663-05:00","created_by":"daemon"}]} -{"id":"bd-0fvq","title":"bd doctor should recommend bd prime migration for existing repos","description":"bd doctor should detect old beads integration patterns and recommend migrating to bd prime approach.\n\n## Current behavior\n- bd doctor checks if Claude hooks are installed globally\n- Doesn't check project-level integration (AGENTS.md, CLAUDE.md)\n- Doesn't recommend migration for repos using old patterns\n\n## Desired behavior\nbd doctor should detect and suggest:\n\n1. **Old slash command pattern detected**\n - Check for /beads:* references in AGENTS.md, CLAUDE.md\n - Suggest: These slash commands are deprecated, use bd prime hooks instead\n \n2. **No agent documentation**\n - Check if AGENTS.md or CLAUDE.md exists\n - Suggest: Run 'bd onboard' or 'bd setup claude' to document workflow\n \n3. **Old MCP-only pattern**\n - Check for instructions to use MCP tools but no bd prime hooks\n - Suggest: Add bd prime hooks for better token efficiency\n\n4. **Migration path**\n - Show: 'Run bd setup claude to add SessionStart/PreCompact hooks'\n - Show: 'Update AGENTS.md to reference bd prime instead of slash commands'\n\n## Example output\n\n⚠ Warning: Old beads integration detected in CLAUDE.md\n Found: /beads:* slash command references (deprecated)\n Recommend: Migrate to bd prime hooks for better token efficiency\n Fix: Run 'bd setup claude' and update CLAUDE.md\n\nšŸ’” Tip: bd prime + hooks reduces token usage by 80-99% vs slash commands\n MCP mode: ~50 tokens vs ~10.5k for full MCP scan\n CLI mode: ~1-2k tokens with automatic context recovery\n\n## Benefits\n- Helps existing repos adopt new best practices\n- Clear migration path for users\n- Better token efficiency messaging","status":"open","priority":2,"issue_type":"feature","created_at":"2025-11-12T03:20:25.567748-08:00","updated_at":"2025-11-12T03:20:25.567748-08:00"} -{"id":"bd-svb5","title":"GH#505: Add bd reset/wipe command","description":"Add command to cleanly reset/wipe beads database. User reports painful manual process to start fresh. See GitHub issue #505.","status":"tombstone","priority":2,"issue_type":"feature","created_at":"2025-12-16T01:03:42.160966-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-tm2p","title":"Polecats get stuck on interactive shell prompts (cp/mv/rm -i)","description":"During swarm operations, polecats frequently get stuck waiting for interactive prompts from shell commands like:\n- cp prompting 'overwrite file? (y/n)'\n- mv prompting 'overwrite file? (y/n)' \n- rm prompting 'remove file?'\n\nThis happens because macOS aliases or shell configs may have -i flags set by default.\n\nRoot cause: Claude Code runs commands that trigger interactive confirmation prompts, but cannot respond to them, causing the agent to hang indefinitely.\n\nObserved in: Multiple polecats during GH issues swarm (Dec 2024)\n- Derrick, Roustabout, Prospector, Warboy all got stuck on y/n prompts\n\nSuggested fixes:\n1. AGENTS.md should instruct agents to always use -f flag with cp/mv/rm\n2. Polecat startup could set shell aliases to use non-interactive versions\n3. bd prime hook could include guidance about non-interactive commands\n4. Consider detecting stuck prompts and auto-recovering","status":"open","priority":1,"issue_type":"bug","created_at":"2025-12-14T16:51:24.572271-08:00","updated_at":"2025-12-14T16:51:24.572271-08:00"} -{"id":"bd-au0.5","title":"Add date and priority filters to bd search","description":"Add filter parity with bd list for consistent querying.\n\n**Missing filters to add:**\n- --priority, --priority-min, --priority-max\n- --created-after, --created-before\n- --updated-after, --updated-before\n- --closed-after, --closed-before\n- --empty-description, --no-assignee, --no-labels\n- --desc-contains, --notes-contains\n\n**Files to modify:**\n- cmd/bd/search.go\n- internal/rpc/protocol.go (SearchArgs)\n- internal/storage/storage.go (Search method)\n\n**Testing:**\n- All filter combinations\n- Date format parsing\n- Daemon and direct mode","status":"open","priority":1,"issue_type":"task","created_at":"2025-11-21T21:07:05.496726-05:00","updated_at":"2025-11-21T21:07:05.496726-05:00","dependencies":[{"issue_id":"bd-au0.5","depends_on_id":"bd-au0","type":"parent-child","created_at":"2025-11-21T21:07:05.497762-05:00","created_by":"daemon"}]} -{"id":"bd-eyto","title":"Time-dependent tests may be flaky near TTL boundary","description":"Several tombstone merge tests use time.Now() to create test data: time.Now().Add(-24 * time.Hour), time.Now().Add(-60 * 24 * time.Hour), etc. While these work reliably in practice (24h vs 30d TTL has large margin), they could theoretically be flaky if: 1) Tests run slowly, 2) System clock changes during test, 3) TTL constants change. Recommendation: Consider using a fixed reference time or time injection for deterministic tests. Lower priority since current margin is large. Files: internal/merge/merge_test.go:1337-1338, 1352-1353, 1548-1549, 1590-1591","status":"open","priority":3,"issue_type":"task","created_at":"2025-12-05T16:37:02.348143-08:00","updated_at":"2025-12-05T16:37:02.348143-08:00"} +{"id":"bd-ykd9","title":"Add bd doctor --fix flag to automatically repair issues","description":"Implement a --fix flag for bd doctor that can automatically repair detected issues.\n\nRequirements:\n- Add --fix flag to bd doctor command\n- Show all fixable issues and prompt for confirmation before applying fixes\n- Organize fix implementations under doctor/fix/\u003ctype_of_fix\u003e.go\n- Each fix type should have its own file (e.g., doctor/fix/hooks.go, doctor/fix/sync.go)\n- Display what will be fixed and ask user to confirm (Y/n) before proceeding\n- Support fixing issues like:\n - Missing or broken git hooks\n - Sync problems with remote\n - File permission issues\n - Any other auto-repairable issues doctor detects\n\nImplementation notes:\n- Maintain separation between detection (existing doctor code) and repair (new fix code)\n- Each fix should be idempotent and safe to run multiple times\n- Provide clear output about what was fixed\n- Log any fixes that fail with actionable error messages","status":"open","priority":2,"issue_type":"feature","created_at":"2025-11-14T18:17:48.411264-08:00","updated_at":"2025-11-14T18:17:58.88609-08:00"} +{"id":"bd-z86n","title":"Code Review: PR #551 - Persist close_reason to issues table","description":"Code review of PR #551 which fixes close_reason persistence bug.\n\n## Summary\nThe PR correctly fixes a bug where close_reason was only stored in the events table, not in the issues.close_reason column. This caused `bd show --json` to return empty close_reason.\n\n## What Was Fixed\n- āœ… CloseIssue now updates both close_reason and closed_at\n- āœ… ReOpenIssue clears both close_reason and closed_at\n- āœ… Comprehensive tests added for both storage and CLI layers\n- āœ… Clear documentation in queries.go about dual storage strategy\n\n## Quality Assessment\nāœ… Tests cover both storage layer and CLI JSON output\nāœ… Handles reopen case (clearing close_reason)\nāœ… Good comments explaining dual-storage design\nāœ… No known issues\n\n## Potential Followups\nSee linked issues for suggestions.","status":"open","priority":2,"issue_type":"task","created_at":"2025-12-14T14:25:06.887069-08:00","updated_at":"2025-12-14T14:25:06.887069-08:00"} +{"id":"bd-z8a6","title":"bd delete --from-file should add deleted issues to deletions manifest","description":"When using bd delete --from-file to bulk delete issues, the deleted issue IDs are not being added to the deletions.jsonl manifest.\n\nThis causes those issues to be resurrected during bd sync when git history scanning finds them in old commits.\n\nExpected: All deleted issues should be added to deletions.jsonl so they wont be reimported from git history.\n\nWorkaround: Manually add deletion records to deletions.jsonl.","status":"tombstone","priority":1,"issue_type":"bug","created_at":"2025-12-16T01:48:14.099855-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-ziy5","title":"GH#409: bd init uses issues.jsonl but docs say beads.jsonl","description":"bd init creates config referencing issues.jsonl but README/docs reference beads.jsonl as canonical. Standardize naming. See GitHub issue #409.","status":"tombstone","priority":2,"issue_type":"bug","created_at":"2025-12-16T01:03:58.109954-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-zwtq","title":"Run bd doctor at end of bd init to verify setup","description":"Run bd doctor diagnostics at end of bd init (after line 398 in init.go). If issues found, warn user immediately: '⚠ Setup incomplete. Run bd doctor --fix to complete setup.' Catches configuration problems before user encounters them in normal workflow.","status":"open","priority":1,"issue_type":"feature","created_at":"2025-11-21T23:16:09.596778-08:00","updated_at":"2025-11-21T23:16:27.781976-08:00","dependencies":[{"issue_id":"bd-zwtq","depends_on_id":"bd-tbz3","type":"parent-child","created_at":"2025-11-21T23:16:09.597617-08:00","created_by":"daemon"}]} diff --git a/@AGENTS.md b/@AGENTS.md new file mode 100644 index 00000000..df7a4af9 --- /dev/null +++ b/@AGENTS.md @@ -0,0 +1,40 @@ +# Agent Instructions + +This project uses **bd** (beads) for issue tracking. Run `bd onboard` to get started. + +## Quick Reference + +```bash +bd ready # Find available work +bd show # View issue details +bd update --status in_progress # Claim work +bd close # Complete work +bd sync # Sync with git +``` + +## Landing the Plane (Session Completion) + +**When ending a work session**, you MUST complete ALL steps below. Work is NOT complete until `git push` succeeds. + +**MANDATORY WORKFLOW:** + +1. **File issues for remaining work** - Create issues for anything that needs follow-up +2. **Run quality gates** (if code changed) - Tests, linters, builds +3. **Update issue status** - Close finished work, update in-progress items +4. **PUSH TO REMOTE** - This is MANDATORY: + ```bash + git pull --rebase + bd sync + git push + git status # MUST show "up to date with origin" + ``` +5. **Clean up** - Clear stashes, prune remote branches +6. **Verify** - All changes committed AND pushed +7. **Hand off** - Provide context for next session + +**CRITICAL RULES:** +- Work is NOT complete until `git push` succeeds +- NEVER stop before pushing - that leaves work stranded locally +- NEVER say "ready to push when you are" - YOU must push +- If push fails, resolve and retry until it succeeds + diff --git a/AGENTS.md b/AGENTS.md index c72219db..39dd81ce 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -23,7 +23,7 @@ This shows the last 3 versions with workflow-impacting changes, avoiding the nee - New commands and flags that improve agent workflows - Breaking changes that require workflow updates - Performance improvements and bug fixes -- Integration features (MCP, Agent Mail, git hooks) +- Integration features (MCP, git hooks) **Why this matters:** bd releases weekly with major versions. This command helps you quickly understand what changed without parsing the full CHANGELOG. @@ -286,52 +286,6 @@ bd create "Add feature" -t feature --json # What feature? Why needed? bd create "Refactor code" -t task --json # What code? Why refactor? ``` -### Optional: Agent Mail for Multi-Agent Coordination - -**āš ļø NOT CURRENTLY CONFIGURED** - The mcp-agent-mail server is not set up for this project. Do not attempt to use mcp-agent-mail tools. - -**For multi-agent workflows only** - if multiple AI agents work on the same repository simultaneously, consider using Agent Mail for real-time coordination: - -**With Agent Mail enabled:** -```bash -# Configure environment (one-time per session) -export BEADS_AGENT_MAIL_URL=http://127.0.0.1:8765 -export BEADS_AGENT_NAME=assistant-alpha -export BEADS_PROJECT_ID=my-project - -# Workflow (identical commands) -bd ready # Shows available work -bd update bd-42 --status in_progress # Reserves issue instantly (<100ms) -# ... work on issue ... -bd close bd-42 "Done" # Releases reservation automatically -``` - -**Without Agent Mail (git-only mode):** -```bash -# No environment variables needed -bd ready # Shows available work -bd update bd-42 --status in_progress # Updates via git sync (2-5s latency) -# ... work on issue ... -bd close bd-42 "Done" # Updates via git sync -``` - -**Key differences:** -- **Latency**: <100ms (Agent Mail) vs 2-5s (git-only) -- **Collision prevention**: Instant reservation (Agent Mail) vs eventual consistency (git) -- **Setup**: Requires server + env vars (Agent Mail) vs zero config (git-only) - -**When to use Agent Mail:** -- āœ… Multiple agents working concurrently -- āœ… Frequent status updates (high collision risk) -- āœ… Real-time coordination needed - -**When to skip:** -- āœ… Single agent workflows -- āœ… Infrequent updates (low collision risk) -- āœ… Simplicity preferred over latency - -See [docs/AGENT_MAIL_QUICKSTART.md](docs/AGENT_MAIL_QUICKSTART.md) for 5-minute setup, or [docs/AGENT_MAIL.md](docs/AGENT_MAIL.md) for complete documentation. Example code in [examples/python-agent/AGENT_MAIL_EXAMPLE.md](examples/python-agent/AGENT_MAIL_EXAMPLE.md). - ### Inter-Agent Messaging (bd mail) Beads includes a built-in messaging system for direct agent-to-agent communication. Messages are stored as beads issues, synced via git. diff --git a/cmd/bd/message.go b/cmd/bd/message.go deleted file mode 100644 index f3d3d4dc..00000000 --- a/cmd/bd/message.go +++ /dev/null @@ -1,520 +0,0 @@ -package main - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "net/http" - "os" - "path/filepath" - "strings" - "time" - - "github.com/spf13/cobra" -) - -var messageCmd = &cobra.Command{ - Use: "message", - Short: "Send and receive messages via Agent Mail", - Long: `Send and receive messages between agents using Agent Mail server. - -Requires Agent Mail server running and these environment variables: - BEADS_AGENT_MAIL_URL - Server URL (e.g., http://127.0.0.1:8765) - BEADS_AGENT_NAME - Your agent name (e.g., fred-beads-stevey-macbook) - BEADS_PROJECT_ID - Project identifier (defaults to repo path) - -Example: - bd message send dave-beads-stevey-macbook "Need review on bd-z0yn" - bd message inbox --unread-only - bd message read msg-abc123 - bd message ack msg-abc123`, -} - -var messageSendCmd = &cobra.Command{ - Use: "send ", - Short: "Send a message to another agent", - Long: `Send a message to another agent via Agent Mail. - -The message can be plain text or GitHub-flavored Markdown. - -Examples: - bd message send dave-beads-stevey-macbook "Working on bd-z0yn" - bd message send cino-beads-stevey-macbook "Please review PR #42" --subject "Review Request" - bd message send emma-beads-stevey-macbook "Found bug in auth" --thread-id bd-123`, - Args: cobra.ExactArgs(2), - RunE: runMessageSend, -} - -var messageInboxCmd = &cobra.Command{ - Use: "inbox", - Short: "List inbox messages", - Long: `List messages in your inbox. - -Examples: - bd message inbox - bd message inbox --unread-only --limit 10 - bd message inbox --urgent-only`, - RunE: runMessageInbox, -} - -var messageReadCmd = &cobra.Command{ - Use: "read ", - Short: "Read a specific message", - Long: `Read and display a specific message by ID. - -Marks the message as read automatically. - -Example: - bd message read msg-abc123`, - Args: cobra.ExactArgs(1), - RunE: runMessageRead, -} - -var messageAckCmd = &cobra.Command{ - Use: "ack ", - Short: "Acknowledge a message", - Long: `Acknowledge a message that requires acknowledgement. - -Also marks the message as read if not already. - -Example: - bd message ack msg-abc123`, - Args: cobra.ExactArgs(1), - RunE: runMessageAck, -} - -// Message send flags -var ( - messageSubject string - messageThreadID string - messageImportance string - messageAckRequired bool -) - -// Message inbox flags -var ( - messageLimit int - messageUnreadOnly bool - messageUrgentOnly bool -) - -func init() { - // Register message commands - rootCmd.AddCommand(messageCmd) - messageCmd.AddCommand(messageSendCmd) - messageCmd.AddCommand(messageInboxCmd) - messageCmd.AddCommand(messageReadCmd) - messageCmd.AddCommand(messageAckCmd) - - // Send command flags - messageSendCmd.Flags().StringVarP(&messageSubject, "subject", "s", "", "Message subject") - messageSendCmd.Flags().StringVar(&messageThreadID, "thread-id", "", "Thread ID to group related messages") - messageSendCmd.Flags().StringVar(&messageImportance, "importance", "normal", "Message importance (low, normal, high, urgent)") - messageSendCmd.Flags().BoolVar(&messageAckRequired, "ack-required", false, "Require acknowledgement from recipient") - - // Inbox command flags - messageInboxCmd.Flags().IntVar(&messageLimit, "limit", 20, "Maximum number of messages to show") - messageInboxCmd.Flags().BoolVar(&messageUnreadOnly, "unread-only", false, "Show only unread messages") - messageInboxCmd.Flags().BoolVar(&messageUrgentOnly, "urgent-only", false, "Show only urgent messages") -} - -// AgentMailConfig holds configuration for Agent Mail server -type AgentMailConfig struct { - URL string - AgentName string - ProjectID string -} - -const agentMailConfigHelp = `Agent Mail not configured. Configure with: - export BEADS_AGENT_MAIL_URL=http://127.0.0.1:8765 - export BEADS_AGENT_NAME=your-agent-name - export BEADS_PROJECT_ID=your-project` - -// Message represents an Agent Mail message -type Message struct { - ID int `json:"id"` - Subject string `json:"subject"` - Body string `json:"body,omitempty"` - FromAgent string `json:"from_agent"` - CreatedAt time.Time `json:"created_at"` - Importance string `json:"importance"` - AckRequired bool `json:"ack_required"` - ThreadID string `json:"thread_id,omitempty"` - Read bool `json:"read"` - Acknowledged bool `json:"acknowledged"` -} - -// getAgentMailConfig retrieves Agent Mail configuration from environment -func getAgentMailConfig() (*AgentMailConfig, error) { - url := os.Getenv("BEADS_AGENT_MAIL_URL") - if url == "" { - return nil, fmt.Errorf("BEADS_AGENT_MAIL_URL not set") - } - - agentName := os.Getenv("BEADS_AGENT_NAME") - if agentName == "" { - return nil, fmt.Errorf("BEADS_AGENT_NAME not set") - } - - projectID := os.Getenv("BEADS_PROJECT_ID") - if projectID == "" { - // Default to workspace root path (directory containing .beads/) - if dbPath != "" { - beadsDir := filepath.Dir(dbPath) - projectID = filepath.Dir(beadsDir) - } else { - // Fallback to current directory - cwd, err := os.Getwd() - if err == nil { - projectID = cwd - } - } - } - - return &AgentMailConfig{ - URL: url, - AgentName: agentName, - ProjectID: projectID, - }, nil -} - -// sendAgentMailRequest sends a JSON-RPC request to Agent Mail server -func sendAgentMailRequest(config *AgentMailConfig, method string, params interface{}) (json.RawMessage, error) { - request := map[string]interface{}{ - "jsonrpc": "2.0", - "id": 1, - "method": "tools/call", - "params": map[string]interface{}{ - "name": method, - "arguments": params, - }, - } - - reqBody, err := json.Marshal(request) - if err != nil { - return nil, fmt.Errorf("failed to marshal request: %w", err) - } - - url := strings.TrimRight(config.URL, "/") + "/mcp" - client := &http.Client{Timeout: 30 * time.Second} - resp, err := client.Post(url, "application/json", bytes.NewReader(reqBody)) - if err != nil { - return nil, fmt.Errorf("failed to connect to Agent Mail server: %w", err) - } - defer func() { _ = resp.Body.Close() }() - - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, fmt.Errorf("failed to read response: %w", err) - } - - if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("Agent Mail server returned error: %s (status %d)", string(body), resp.StatusCode) - } - - var response struct { - Result struct { - Content []struct { - Type string `json:"type"` - Text json.RawMessage `json:"text"` - } `json:"content"` - } `json:"result"` - Error *struct { - Code int `json:"code"` - Message string `json:"message"` - } `json:"error"` - } - - if err := json.Unmarshal(body, &response); err != nil { - return nil, fmt.Errorf("failed to parse response: %w", err) - } - - if response.Error != nil { - return nil, fmt.Errorf("Agent Mail error: %s (code %d)", response.Error.Message, response.Error.Code) - } - - if len(response.Result.Content) == 0 { - return nil, fmt.Errorf("no content in response") - } - - return response.Result.Content[0].Text, nil -} - -func runMessageSend(cmd *cobra.Command, args []string) error { - // Validate importance flag - validImportance := map[string]bool{ - "low": true, - "normal": true, - "high": true, - "urgent": true, - } - if !validImportance[messageImportance] { - return fmt.Errorf("invalid importance: %s (must be: low, normal, high, urgent)", messageImportance) - } - - config, err := getAgentMailConfig() - if err != nil { - return fmt.Errorf("%w\n\n%s", err, agentMailConfigHelp) - } - - toAgent := args[0] - message := args[1] - - // Prepare request parameters - params := map[string]interface{}{ - "project_key": config.ProjectID, - "sender_name": config.AgentName, - "to": []string{toAgent}, - "body_md": message, - } - - if messageSubject != "" { - params["subject"] = messageSubject - } else { - // Generate subject from first line of message - firstLine := strings.Split(message, "\n")[0] - if len(firstLine) > 50 { - firstLine = firstLine[:50] + "..." - } - params["subject"] = firstLine - } - - if messageThreadID != "" { - params["thread_id"] = messageThreadID - } - - if messageImportance != "normal" { - params["importance"] = messageImportance - } - - if messageAckRequired { - params["ack_required"] = true - } - - // Send message via Agent Mail - result, err := sendAgentMailRequest(config, "send_message", params) - if err != nil { - return err - } - - // Parse result - var sendResult struct { - Deliveries []struct { - Recipient string `json:"recipient"` - MessageID int `json:"message_id"` - } `json:"deliveries"` - Count int `json:"count"` - } - - if err := json.Unmarshal(result, &sendResult); err != nil { - return fmt.Errorf("failed to parse send result: %w", err) - } - - if jsonOutput { - encoder := json.NewEncoder(os.Stdout) - encoder.SetIndent("", " ") - return encoder.Encode(sendResult) - } - - fmt.Printf("Message sent to %s\n", toAgent) - if sendResult.Count > 0 && len(sendResult.Deliveries) > 0 { - fmt.Printf("Message ID: %d\n", sendResult.Deliveries[0].MessageID) - } - if messageThreadID != "" { - fmt.Printf("Thread: %s\n", messageThreadID) - } - - return nil -} - -func runMessageInbox(cmd *cobra.Command, args []string) error { - config, err := getAgentMailConfig() - if err != nil { - return fmt.Errorf("%w\n\n%s", err, agentMailConfigHelp) - } - - // Prepare request parameters - params := map[string]interface{}{ - "project_key": config.ProjectID, - "agent_name": config.AgentName, - "limit": messageLimit, - "include_bodies": false, - } - - if messageUnreadOnly { - params["unread_only"] = true - } - - if messageUrgentOnly { - params["urgent_only"] = true - } - - // Fetch inbox via Agent Mail - result, err := sendAgentMailRequest(config, "fetch_inbox", params) - if err != nil { - return err - } - - // Parse result - var messages []Message - - if err := json.Unmarshal(result, &messages); err != nil { - return fmt.Errorf("failed to parse inbox: %w", err) - } - - if jsonOutput { - encoder := json.NewEncoder(os.Stdout) - encoder.SetIndent("", " ") - return encoder.Encode(messages) - } - - if len(messages) == 0 { - fmt.Println("No messages in inbox") - return nil - } - - fmt.Printf("Inbox for %s (%d messages):\n\n", config.AgentName, len(messages)) - for _, msg := range messages { - // Format timestamp - age := time.Since(msg.CreatedAt) - var timeStr string - if age < time.Hour { - timeStr = fmt.Sprintf("%dm ago", int(age.Minutes())) - } else if age < 24*time.Hour { - timeStr = fmt.Sprintf("%dh ago", int(age.Hours())) - } else { - timeStr = fmt.Sprintf("%dd ago", int(age.Hours()/24)) - } - - // Status indicators - status := "" - if !msg.Read { - status += " [UNREAD]" - } - if msg.AckRequired && !msg.Acknowledged { - status += " [ACK REQUIRED]" - } - if msg.Importance == "high" || msg.Importance == "urgent" { - status += fmt.Sprintf(" [%s]", strings.ToUpper(msg.Importance)) - } - - fmt.Printf(" %d: %s%s\n", msg.ID, msg.Subject, status) - fmt.Printf(" From: %s (%s)\n", msg.FromAgent, timeStr) - if msg.ThreadID != "" { - fmt.Printf(" Thread: %s\n", msg.ThreadID) - } - fmt.Println() - } - - return nil -} - -func runMessageRead(cmd *cobra.Command, args []string) error { - config, err := getAgentMailConfig() - if err != nil { - return fmt.Errorf("%w\n\n%s", err, agentMailConfigHelp) - } - - messageID := args[0] - - // Fetch full message with body - fetchParams := map[string]interface{}{ - "project_key": config.ProjectID, - "agent_name": config.AgentName, - "message_id": messageID, - "include_bodies": true, - } - - result, err := sendAgentMailRequest(config, "fetch_inbox", fetchParams) - if err != nil { - return fmt.Errorf("failed to fetch message: %w", err) - } - - // Parse message - var messages []Message - - if err := json.Unmarshal(result, &messages); err != nil { - return fmt.Errorf("failed to parse message: %w", err) - } - - if len(messages) == 0 { - return fmt.Errorf("message not found: %s", messageID) - } - - msg := messages[0] - - // Mark as read if not already - if !msg.Read { - markParams := map[string]interface{}{ - "project_key": config.ProjectID, - "agent_name": config.AgentName, - "message_id": messageID, - } - _, _ = sendAgentMailRequest(config, "mark_message_read", markParams) - } - - if jsonOutput { - encoder := json.NewEncoder(os.Stdout) - encoder.SetIndent("", " ") - return encoder.Encode(msg) - } - - // Display message - fmt.Printf("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n") - fmt.Printf("From: %s\n", msg.FromAgent) - fmt.Printf("Subject: %s\n", msg.Subject) - fmt.Printf("Time: %s\n", msg.CreatedAt.Format("2006-01-02 15:04:05 MST")) - if msg.ThreadID != "" { - fmt.Printf("Thread: %s\n", msg.ThreadID) - } - if msg.Importance != "" && msg.Importance != "normal" { - fmt.Printf("Priority: %s\n", strings.ToUpper(msg.Importance)) - } - if msg.AckRequired { - status := "Required" - if msg.Acknowledged { - status = "Acknowledged" - } - fmt.Printf("Acknowledgement: %s\n", status) - } - fmt.Printf("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n") - fmt.Println(msg.Body) - fmt.Println() - - return nil -} - -func runMessageAck(cmd *cobra.Command, args []string) error { - config, err := getAgentMailConfig() - if err != nil { - return fmt.Errorf("%w\n\n%s", err, agentMailConfigHelp) - } - - messageID := args[0] - - // Acknowledge message - params := map[string]interface{}{ - "project_key": config.ProjectID, - "agent_name": config.AgentName, - "message_id": messageID, - } - - result, err := sendAgentMailRequest(config, "acknowledge_message", params) - if err != nil { - return err - } - - if jsonOutput { - encoder := json.NewEncoder(os.Stdout) - encoder.SetIndent("", " ") - var ackResult map[string]interface{} - if err := json.Unmarshal(result, &ackResult); err != nil { - return fmt.Errorf("failed to parse ack result: %w", err) - } - return encoder.Encode(ackResult) - } - - fmt.Printf("Message %s acknowledged\n", messageID) - return nil -} diff --git a/docs/AGENT_MAIL.md b/docs/AGENT_MAIL.md deleted file mode 100644 index e0070a9a..00000000 --- a/docs/AGENT_MAIL.md +++ /dev/null @@ -1,525 +0,0 @@ -# Agent Mail Integration Guide - -**Status:** Optional Enhancement -**Minimum bd Version:** 0.21.0 -**Related ADR:** [002-agent-mail-integration.md](adr/002-agent-mail-integration.md) - -## Overview - -MCP Agent Mail provides real-time coordination for multi-agent beads workflows, reducing latency from 2-5 seconds (git sync) to <100ms (HTTP API) and preventing work collisions through file reservations. - -**Key Benefits:** -- **20-50x latency reduction**: <100ms vs 2000-5000ms for status updates -- **Collision prevention**: Exclusive file reservations prevent duplicate work -- **Lightweight**: <50MB memory, simple HTTP API -- **100% optional**: Git-only mode works identically without it - -## Quick Start - -### Prerequisites -- Python 3.11+ -- bd 0.21.0+ -- Multi-agent workflow (2+ AI agents working on same repository) - -### Installation - -```bash -# Install Agent Mail server -git clone https://github.com/Dicklesworthstone/mcp_agent_mail.git -cd mcp_agent_mail -python3 -m venv .venv -source .venv/bin/activate # Windows: .venv\Scripts\activate -pip install -e . - -# Start server -python -m mcp_agent_mail.cli serve-http -# Server runs on http://127.0.0.1:8765 -``` - -### Configuration - -Enable Agent Mail by setting environment variables before running bd commands: - -```bash -# Agent 1 -export BEADS_AGENT_MAIL_URL=http://127.0.0.1:8765 -export BEADS_AGENT_NAME=assistant-alpha -export BEADS_PROJECT_ID=my-project - -# Agent 2 -export BEADS_AGENT_MAIL_URL=http://127.0.0.1:8765 -export BEADS_AGENT_NAME=assistant-beta -export BEADS_PROJECT_ID=my-project - -# Now run bd commands normally -bd ready -bd update bd-42 --status in_progress -``` - -**All configuration is via environment variables** - no changes to `.beads/` or git required. - -### Verification - -```bash -# Check if Agent Mail is active -bd info --json | grep agent_mail - -# View reservations in web UI -open http://127.0.0.1:8765/mail - -# Test collision prevention -# Terminal 1 (Agent A): -bd update bd-123 --status in_progress - -# Terminal 2 (Agent B): -bd update bd-123 --status in_progress -# Expected: Error - bd-123 reserved by assistant-alpha -``` - -## How It Works - -### Architecture - -``` -ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” -│ bd (Beads CLI) │ -│ │ -│ ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” │ -│ │ Git Sync │ │ Agent Mail │ │ -│ │ (required) │ │ (optional) │ │ -│ │ │ │ │ │ -│ │ - Export │ │ - Reservations │ │ -│ │ - Import │ │ - Notifications │ │ -│ │ - Commit │ │ - Status updates│ │ -│ │ - Push/Pull │ │ │ │ -│ ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ │ -│ │ │ │ -ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ - │ │ - ā–¼ ā–¼ - ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” - │ .beads/ │ │ Agent Mail │ - │ issues.jsonl │ │ Server │ - │ (git) │ │ (HTTP) │ - ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ -``` - -**Git remains the source of truth.** Agent Mail provides ephemeral coordination state only. - -### Coordination Flow - -**Without Agent Mail (git-only):** -``` -Agent A: bd update bd-123 --status in_progress - ↓ (30s debounce) - ↓ export to JSONL - ↓ git commit + push (1-2s) - ↓ -Agent B: git pull (1-2s) - ↓ import from JSONL - ↓ sees bd-123 is taken (TOO LATE - work already started!) - -Total latency: 2000-5000ms -Risk: Both agents work on same issue -``` - -**With Agent Mail:** -``` -Agent A: bd update bd-123 --status in_progress - ↓ - ā”œā”€ Agent Mail: POST /api/reservations (5ms) - │ └─ Reserve bd-123 for Agent A - ā”œā”€ Local: Update .beads/beads.db - └─ Background: Export to JSONL (30s debounce) - -Agent B: bd update bd-123 --status in_progress - ↓ - └─ Agent Mail: POST /api/reservations (5ms) - └─ HTTP 409 Conflict: "bd-123 reserved by Agent A" - └─ bd exits with clear error - -Total latency: <100ms -Risk: Zero - collision prevented at reservation layer -``` - -### Integration Points - -Agent Mail integrates at 4 key points in the bd workflow: - -1. **Issue Reservation** (`bd update --status in_progress`) - - Check if issue already reserved - - Create reservation if available - - Error 409 if conflict - -2. **Issue Release** (`bd close`) - - Release reservation automatically - - Notify other agents (optional) - -3. **Ready Work Query** (`bd ready`) - - Filter out reserved issues (future enhancement) - - Show only truly available work - -4. **Status Updates** (`bd update --priority`, etc.) - - No reservation required for non-status changes - - Graceful degradation if server unavailable - -## Configuration Reference - -### Environment Variables - -| Variable | Required | Default | Description | -|----------|----------|---------|-------------| -| `BEADS_AGENT_MAIL_URL` | Yes | None | Agent Mail server URL (e.g., `http://127.0.0.1:8765`) | -| `BEADS_AGENT_NAME` | Yes | None | Unique agent identifier (e.g., `assistant-alpha`) | -| `BEADS_PROJECT_ID` | Yes | None | Project namespace (e.g., `my-project`) | -| `BEADS_AGENT_MAIL_TOKEN` | No | None | Bearer token for authentication (future) | - -### Example Configurations - -**Local Development (Single Machine):** -```bash -# ~/.bashrc or ~/.zshrc -export BEADS_AGENT_MAIL_URL=http://127.0.0.1:8765 -export BEADS_AGENT_NAME=$(whoami)-$(hostname) -export BEADS_PROJECT_ID=$(basename $(pwd)) -``` - -**Multi-Machine Setup:** -```bash -# Machine 1 (runs server) -export BEADS_AGENT_MAIL_URL=http://192.168.1.100:8765 -export BEADS_AGENT_NAME=dev-machine-1 -export BEADS_PROJECT_ID=beads - -# Machine 2 (client only) -export BEADS_AGENT_MAIL_URL=http://192.168.1.100:8765 -export BEADS_AGENT_NAME=dev-machine-2 -export BEADS_PROJECT_ID=beads -``` - -**Docker Compose:** -```yaml -services: - agent-mail: - image: ghcr.io/dicklesworthstone/mcp_agent_mail:latest - ports: - - "8765:8765" - - agent-1: - image: my-beads-agent - environment: - BEADS_AGENT_MAIL_URL: http://agent-mail:8765 - BEADS_AGENT_NAME: worker-1 - BEADS_PROJECT_ID: my-project -``` - -## When to Use Agent Mail - -### āœ… Use Agent Mail When: - -1. **Multiple AI agents** working on the same repository simultaneously -2. **Frequent status updates** (multiple agents claiming/releasing work) -3. **Collision-sensitive workflows** (duplicate work is expensive) -4. **Real-time coordination needed** (latency matters) -5. **CI/CD integration** (agents triggered by webhooks/events) - -**Example Scenario:** -- Team has 3 AI agents (Claude, GPT-4, Gemini) -- Agents pull from work queue every 5 minutes -- Repository has 50+ open issues -- Duplicate work costs 30+ minutes to resolve - -### āŒ Skip Agent Mail When: - -1. **Single agent** workflows (no collision risk) -2. **Infrequent updates** (once per day/week) -3. **Git-only infrastructure** (no external services allowed) -4. **Offline workflows** (no network connectivity) -5. **Low issue volume** (<10 open issues) - -**Example Scenario:** -- Solo developer using beads for personal task tracking -- Updates happen once per session -- No concurrent work -- Simplicity over latency - -## Graceful Degradation - -**Agent Mail failure NEVER breaks beads.** If the server is unavailable: - -1. bd logs a warning: `Agent Mail unavailable, falling back to git-only mode` -2. All operations proceed normally -3. Git sync handles coordination (with higher latency) -4. No errors, no crashes - -**Test graceful degradation:** -```bash -# Stop Agent Mail server -pkill -f "mcp_agent_mail.cli" - -# bd commands work identically -bd ready # Success -bd update bd-42 --status in_progress # Success (git-only mode) -``` - -**Automatic fallback conditions:** -- Server not responding (connection refused) -- Server returns 500+ errors -- Network timeout (>5s) -- Invalid/missing environment variables - -## Multi-Machine Deployment - -### Centralized Server Pattern (Recommended) - -``` -ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” -│ Agent A │────▶│ Agent Mail │◀────│ Agent B │ -│ (Machine 1) │ │ Server │ │ (Machine 2) │ -ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ │ (Machine 3) │ ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ - ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ - │ - ā–¼ - ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” - │ PostgreSQL │ (optional) - │ Storage │ - ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ -``` - -**Steps:** -1. Deploy Agent Mail on dedicated server/container -2. Configure firewall to allow port 8765 from agent machines -3. Set `BEADS_AGENT_MAIL_URL` to server IP on all agents -4. (Optional) Configure PostgreSQL backend for persistence - -### Peer-to-Peer Pattern (Advanced) - -``` -ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” -│ Agent A │────▶│ Agent B │ -│ + Server │ │ + Server │ -ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ - │ │ - ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ - ā–¼ - ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” - │ Shared DB │ (e.g., Redis) - ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ -``` - -**Use case:** High-availability setups where server can't be single point of failure. - -## Troubleshooting - -### Issue: "Agent Mail unavailable" warnings - -**Symptoms:** -``` -WARN Agent Mail unavailable, falling back to git-only mode -``` - -**Solutions:** -1. Check server is running: `curl http://127.0.0.1:8765/health` -2. Verify `BEADS_AGENT_MAIL_URL` is set correctly -3. Check firewall allows connections to port 8765 -4. Review server logs: `tail -f ~/.mcp_agent_mail/logs/server.log` - -### Issue: "Reservation conflict" errors - -**Symptoms:** -``` -Error: bd-123 already reserved by assistant-alpha -``` - -**Solutions:** -1. **Expected behavior** - another agent claimed the issue first -2. Find different work: `bd ready` -3. If reservation is stale (agent crashed), release manually: - ```bash - curl -X DELETE http://127.0.0.1:8765/api/reservations/bd-123 - ``` -4. Configure reservation TTL to auto-expire stale locks (future) - -### Issue: Reservations persist after agent crashes - -**Symptoms:** -- Agent crashes mid-work -- Reservation not released -- Other agents can't claim the issue - -**Solutions:** -1. **Manual release** via web UI: http://127.0.0.1:8765/mail -2. **API release**: `curl -X DELETE http://127.0.0.1:8765/api/reservations/` -3. **Restart server** (clears all ephemeral state): `pkill -f mcp_agent_mail; python -m mcp_agent_mail.cli serve-http` -4. **Future:** Configure TTL to auto-expire (not yet implemented) - -### Issue: Two agents have different project IDs - -**Symptoms:** -- Agents don't see each other's reservations -- Collisions still happen - -**Solutions:** -1. Ensure all agents use **same** `BEADS_PROJECT_ID` -2. Check environment: `echo $BEADS_PROJECT_ID` -3. Set globally in shell profile: - ```bash - # ~/.bashrc - export BEADS_PROJECT_ID=my-project - ``` - -### Issue: Server uses too much memory - -**Symptoms:** -- Agent Mail process grows to 100+ MB -- Server becomes slow - -**Solutions:** -1. **Normal for in-memory mode** (<50MB baseline + reservation data) -2. Use PostgreSQL backend for large-scale deployments -3. Configure reservation expiry to prevent unbounded growth -4. Restart server periodically (ephemeral state is OK to lose) - -## Migration from Git-Only Mode - -**Good news:** Zero migration required! Agent Mail is purely additive. - -### Step 1: Test with Single Agent -```bash -# Start server -python -m mcp_agent_mail.cli serve-http - -# Configure one agent -export BEADS_AGENT_MAIL_URL=http://127.0.0.1:8765 -export BEADS_AGENT_NAME=test-agent -export BEADS_PROJECT_ID=test - -# Run normal workflow -bd ready -bd update bd-42 --status in_progress -bd close bd-42 "Done" - -# Verify in web UI -open http://127.0.0.1:8765/mail -``` - -### Step 2: Add Second Agent -```bash -# In separate terminal/machine -export BEADS_AGENT_MAIL_URL=http://127.0.0.1:8765 -export BEADS_AGENT_NAME=test-agent-2 -export BEADS_PROJECT_ID=test - -# Try claiming same issue (should fail) -bd update bd-42 --status in_progress -# Expected: Error - reservation conflict -``` - -### Step 3: Roll Out to Production -1. Deploy Agent Mail server to production environment -2. Add environment variables to agent deployment configs -3. Monitor logs for "Agent Mail unavailable" warnings -4. Gradually enable for all agents - -### Rollback Plan -Simply **unset environment variables** - agents immediately fall back to git-only mode: -```bash -unset BEADS_AGENT_MAIL_URL -unset BEADS_AGENT_NAME -unset BEADS_PROJECT_ID - -# bd works identically without Agent Mail -bd ready -``` - -## FAQ - -### Q: Do I need Agent Mail for single-agent workflows? -**A:** No. Agent Mail is only useful for multi-agent coordination. Single agents get no benefit from it. - -### Q: Can I use Agent Mail with the MCP server (beads-mcp)? -**A:** Yes! Set the environment variables before starting beads-mcp, and it will use Agent Mail for all operations. - -### Q: What happens if Agent Mail and git get out of sync? -**A:** Git is the source of truth. If Agent Mail has stale reservation data, worst case is a 409 error. Agent can manually release and retry. - -### Q: Does Agent Mail require changes to .beads/ or git? -**A:** No. Agent Mail is 100% external. No changes to database schema, JSONL format, or git workflow. - -### Q: Can I use Agent Mail for multiple projects on the same server? -**A:** Yes. Set different `BEADS_PROJECT_ID` for each project. Agent Mail provides namespace isolation. - -### Q: Is Agent Mail required for beads 1.0? -**A:** No. Agent Mail is an optional enhancement. Git-only mode is fully supported indefinitely. - -### Q: How does Agent Mail handle authentication? -**A:** Currently, no authentication (local network only). Bearer token support planned for future release. - -### Q: Can I self-host Agent Mail on corporate infrastructure? -**A:** Yes. Agent Mail is open source (MIT license) and can be deployed anywhere Python runs. - -### Q: What's the performance impact of Agent Mail? -**A:** ~5ms overhead per reservation API call. Negligible compared to 2000-5000ms git sync. - -### Q: Does Agent Mail work with protected branches? -**A:** Yes. Agent Mail operates independently of git workflow. Use with `bd config set sync.branch beads-metadata` as normal. - -## Advanced Topics - -### Custom Reservation TTL -```bash -# Future feature (not yet implemented) -export BEADS_RESERVATION_TTL=3600 # 1 hour in seconds -``` - -### PostgreSQL Backend -```bash -# For production deployments with persistence -export AGENT_MAIL_DB_URL=postgresql://user:pass@localhost/agentmail -python -m mcp_agent_mail.cli serve-http -``` - -### Monitoring & Observability -```bash -# Server exposes Prometheus metrics (future) -curl http://127.0.0.1:8765/metrics - -# Health check -curl http://127.0.0.1:8765/health -``` - -### Integration with CI/CD -```yaml -# GitHub Actions example -jobs: - agent-workflow: - runs-on: ubuntu-latest - services: - agent-mail: - image: ghcr.io/dicklesworthstone/mcp_agent_mail:latest - ports: - - 8765:8765 - env: - BEADS_AGENT_MAIL_URL: http://localhost:8765 - BEADS_AGENT_NAME: github-actions-${{ github.run_id }} - BEADS_PROJECT_ID: ${{ github.repository }} - steps: - - run: bd ready | head -1 | xargs -I {} bd update {} --status in_progress -``` - -## Resources - -- [ADR 002: Agent Mail Integration](adr/002-agent-mail-integration.md) -- [Agent Mail Repository](https://github.com/Dicklesworthstone/mcp_agent_mail) -- [Python Agent Example](../examples/python-agent/) - -## Contributing - -Found a bug or want to improve Agent Mail integration? See: -- [CONTRIBUTING.md](../CONTRIBUTING.md) for beads contribution guidelines -- [Agent Mail Issues](https://github.com/Dicklesworthstone/mcp_agent_mail/issues) for server-side issues - -## License - -Beads: Apache 2.0 -MCP Agent Mail: MIT diff --git a/docs/AGENT_MAIL_DEPLOYMENT.md b/docs/AGENT_MAIL_DEPLOYMENT.md deleted file mode 100644 index fdd80233..00000000 --- a/docs/AGENT_MAIL_DEPLOYMENT.md +++ /dev/null @@ -1,643 +0,0 @@ -# Agent Mail Deployment - Last Mile Steps - -Complete step-by-step guide to deploy Agent Mail across all 12 beads-enabled workspaces. - -## Prerequisites - -- āœ… MCP Agent Mail package installed at `~/src/mcp_agent_mail/` -- āœ… beads integration code completed (just finished) -- āœ… Helper scripts created in `scripts/` -- āœ… direnv installed (`brew install direnv`) -- āœ… direnv hook added to shell config - -## Architecture Overview - -**One Server, Three Channels:** -- **Agent Mail Server**: Single instance on `http://127.0.0.1:8765` -- **beads.dev**: 5 beads workers communicate here -- **vc.dev**: 5 vc workers communicate here -- **wyvern.dev**: 3 wyvern workers communicate here - -**Total: 13 workspaces** (12 repos + main beads for version bump) - -## Step 1: Version Bump (5 minutes) - -Since Agent Mail integration is brand new, bump beads version first. - -```bash -cd ~/src/fred/beads - -# Determine new version (check current first) -./bd --version -# Example output: bd version 0.16.0 - -# Bump to next version -./scripts/bump-version.sh 0.23.0 --commit - -# Push to trigger release -git push origin main -git push origin v0.23.0 -``` - -**Rationale:** All workspaces should use the new version with Agent Mail support. - -## Step 2: Start Agent Mail Server (2 minutes) - -```bash -cd ~/src/fred/beads - -# Start server in background -./scripts/start-agent-mail-server.sh - -# Expected output: -# āœ… Agent Mail server started successfully! -# PID: 12345 -# Health: http://127.0.0.1:8765/health -# Web UI: http://127.0.0.1:8765/mail - -# Verify server health -curl http://127.0.0.1:8765/health -# Expected: {"status": "healthy"} -``` - -**Troubleshooting:** -- If port 8765 in use: `lsof -i :8765` then `kill ` -- If server fails: Check `~/agent-mail.log` for errors -- If venv missing: See installation steps in `docs/AGENT_MAIL_QUICKSTART.md` - -## Step 3: Configure All Workspaces (10 minutes) - -Run setup script in each workspace to create `.envrc` files. - -### 3a. Configure 5 beads repos - -```bash -# Main beads repo -cd ~/src/beads -../fred/beads/scripts/setup-agent-mail-workspace.sh . -direnv allow - -# cino/beads fork -cd ~/src/cino/beads -../../fred/beads/scripts/setup-agent-mail-workspace.sh . -direnv allow - -# dave/beads fork -cd ~/src/dave/beads -../../fred/beads/scripts/setup-agent-mail-workspace.sh . -direnv allow - -# emma/beads fork -cd ~/src/emma/beads -../../fred/beads/scripts/setup-agent-mail-workspace.sh . -direnv allow - -# fred/beads fork (current repo) -cd ~/src/fred/beads -./scripts/setup-agent-mail-workspace.sh . -direnv allow -``` - -**Expected .envrc content:** -```bash -# Agent Mail Configuration -export BEADS_AGENT_MAIL_URL=http://127.0.0.1:8765 -export BEADS_AGENT_NAME=fred-beads-macbook # (varies by workspace/hostname) -export BEADS_PROJECT_ID=beads.dev -``` - -### 3b. Configure 5 vc repos - -```bash -# Main vc repo (if standalone exists) -cd ~/src/vc -../fred/beads/scripts/setup-agent-mail-workspace.sh . -direnv allow - -# cino/vc -cd ~/src/cino/vc -../../fred/beads/scripts/setup-agent-mail-workspace.sh . -direnv allow - -# dave/vc -cd ~/src/dave/vc -../../fred/beads/scripts/setup-agent-mail-workspace.sh . -direnv allow - -# fred/vc -cd ~/src/fred/vc -../../fred/beads/scripts/setup-agent-mail-workspace.sh . -direnv allow - -# (One more standalone vc if it exists - adjust path as needed) -``` - -**Expected PROJECT_ID:** `vc.dev` - -### 3c. Configure 3 wyvern repos - -```bash -# Main wyvern repo -cd ~/src/wyvern -../fred/beads/scripts/setup-agent-mail-workspace.sh . -direnv allow - -# cino/wyvern -cd ~/src/cino/wyvern -../../fred/beads/scripts/setup-agent-mail-workspace.sh . -direnv allow - -# fred/wyvern -cd ~/src/fred/wyvern -../../fred/beads/scripts/setup-agent-mail-workspace.sh . -direnv allow -``` - -**Expected PROJECT_ID:** `wyvern.dev` - -## Step 4: Upgrade beads Binary Everywhere (5 minutes) - -After version bump in Step 1, upgrade all workspaces to new version. - -```bash -# Wait for GitHub release to build (check https://github.com/steveyegge/beads/releases) -# Or build locally if impatient - -# Option 1: Install from release (when ready) -curl -sSL https://raw.githubusercontent.com/steveyegge/beads/main/install.sh | bash - -# Option 2: Build locally and copy to all repos -cd ~/src/fred/beads -go build -o bd ./cmd/bd - -# Copy to other repos (example for beads repos) -cp bd ~/src/beads/bd -cp bd ~/src/cino/beads/bd -cp bd ~/src/dave/beads/bd -cp bd ~/src/emma/beads/bd - -# Verify new version includes Agent Mail support -cd ~/src/fred/beads -./bd --version -# Expected: bd version 0.23.0 (or whatever you bumped to) - -./bd info --json | grep -i agent -# Expected: JSON output showing agent_mail config -``` - -## Step 5: Document Configuration (2 minutes) - -Add Agent Mail config to each workspace's AGENTS.md (if applicable). Agents need these instructions before testing. - -Example for vc repos: - -```bash -cd ~/src/fred/vc - -# Add to AGENTS.md (or create if missing) -cat >> AGENTS.md <<'EOF' - -## Agent Mail Configuration - -This workspace participates in multi-agent coordination via MCP Agent Mail. - -**Channel**: vc.dev (shared with all vc workers) -**Server**: http://127.0.0.1:8765 -**Agent Name**: fred-vc- - -**Configuration**: Loaded automatically via `.envrc` (direnv) - -**Tightly coupled workers**: -- ~/src/vc -- ~/src/cino/vc -- ~/src/dave/vc -- ~/src/fred/vc -- (one more standalone) - -All vc workers coordinate issue reservations in real-time (<100ms latency). - -**Cross-project coordination**: vc → beads bugs filed via git/PRs (not Agent Mail messaging) - -See `docs/AGENT_MAIL_MULTI_WORKSPACE_SETUP.md` for details. -EOF -``` - -Repeat for beads and wyvern workspaces with appropriate channel names. - -## Step 6: Test Same-Channel Coordination (5 minutes) - -Verify agents in same channel can see each other's reservations. - -### 6a. Test vc.dev channel (2 vc repos) - -**Terminal 1 - fred/vc:** -```bash -cd ~/src/fred/vc - -# Verify env vars -echo $BEADS_PROJECT_ID # Expected: vc.dev -echo $BEADS_AGENT_NAME # Expected: fred-vc- - -# Create test issue and reserve it -bd create "Test Agent Mail coordination" -p 2 -t task -# Example output: bd-test42 - -bd update bd-test42 --status in_progress -# Expected: āœ… Reserved bd-test42 for fred-vc-macbook -``` - -**Terminal 2 - cino/vc:** -```bash -cd ~/src/cino/vc - -# Verify env vars -echo $BEADS_PROJECT_ID # Expected: vc.dev -echo $BEADS_AGENT_NAME # Expected: cino-vc- - -# Try to claim same issue -bd update bd-test42 --status in_progress -# Expected: āŒ Error - bd-test42 already reserved by fred-vc-macbook -``` - -**Success!** Collision prevented across different repos in same channel. - -**Terminal 1 - Cleanup:** -```bash -cd ~/src/fred/vc -bd close bd-test42 "Test complete" -# Expected: āœ… Reservation released -``` - -### 6b. Test channel isolation - -Verify agents in different channels DON'T interfere. - -**Terminal 1 - fred/beads (beads.dev):** -```bash -cd ~/src/fred/beads -bd create "Test channel isolation" -p 2 -# Example: Created bd-test1 - -bd update bd-test1 --status in_progress -# Expected: Success -``` - -**Terminal 2 - fred/vc (vc.dev):** -```bash -cd ~/src/fred/vc -bd create "Test channel isolation" -p 2 -# Example: Created bd-test2 - -bd update bd-test2 --status in_progress -# Expected: Success (no conflict - different channel!) -``` - -**Both reservations succeed** because they're in different channels. - -## Step 7: Monitor All Channels (2 minutes) - -```bash -cd ~/src/fred/beads - -# Check overall status -./scripts/agent-mail-status.sh - -# Expected output: -# === Agent Mail Status === -# Server Process: āœ… Running (PID: 12345) -# Server Health: āœ… OK -# Active Projects: -# • beads.dev -# • vc.dev -# • wyvern.dev -# Active File Reservations: -# (list of current reservations across all channels) - -# Open Web UI for visual monitoring -open http://127.0.0.1:8765/mail -``` - -## Step 8: Make Server Auto-Start on Reboot (Optional, 5 minutes) - -Use macOS launchd for automatic server startup. - -```bash -# Create launchd plist -cat > ~/Library/LaunchAgents/com.user.mcp-agent-mail.plist <<'EOF' - - - - - Label - com.user.mcp-agent-mail - ProgramArguments - - /Users/stevey/src/mcp_agent_mail/.venv/bin/python - -m - mcp_agent_mail.cli - serve-http - --host - 127.0.0.1 - --port - 8765 - - WorkingDirectory - /Users/stevey/src/mcp_agent_mail - StandardOutPath - /Users/stevey/agent-mail.log - StandardErrorPath - /Users/stevey/agent-mail-error.log - RunAtLoad - - KeepAlive - - - -EOF - -# Load service -launchctl load ~/Library/LaunchAgents/com.user.mcp-agent-mail.plist - -# Verify loaded -launchctl list | grep mcp-agent-mail -# Expected: Shows PID and status - -# Test restart -sudo reboot # (or just log out/in) -# After reboot, verify server auto-started: -curl http://127.0.0.1:8765/health -``` - -## Step 9: Auto-Start on Reboot (Optional, 5 minutes) - -Use macOS launchd for automatic server startup. - -```bash -# Create launchd plist -cat > ~/Library/LaunchAgents/com.user.mcp-agent-mail.plist <<'EOF' - - - - - Label - com.user.mcp-agent-mail - ProgramArguments - - /Users/stevey/src/mcp_agent_mail/.venv/bin/python - -m - mcp_agent_mail.cli - serve-http - --host - 127.0.0.1 - --port - 8765 - - WorkingDirectory - /Users/stevey/src/mcp_agent_mail - StandardOutPath - /Users/stevey/agent-mail.log - StandardErrorPath - /Users/stevey/agent-mail-error.log - RunAtLoad - - KeepAlive - - - -EOF - -# Load service -launchctl load ~/Library/LaunchAgents/com.user.mcp-agent-mail.plist - -# Verify loaded -launchctl list | grep mcp-agent-mail -# Expected: Shows PID and status - -# Test restart -sudo reboot # (or just log out/in) -# After reboot, verify server auto-started: -curl http://127.0.0.1:8765/health -``` - -## Step 10: Verification Checklist - -Run through this checklist to confirm deployment success. - -### āœ… Server Health -- [ ] `curl http://127.0.0.1:8765/health` returns `{"status": "healthy"}` -- [ ] PID file exists: `cat ~/agent-mail.pid` -- [ ] Process running: `ps -p $(cat ~/agent-mail.pid)` -- [ ] Logs clean: `tail ~/agent-mail.log` (no errors) - -### āœ… Workspace Configuration -- [ ] All 13 workspaces have `.envrc` files -- [ ] All `.envrc` files have correct `BEADS_PROJECT_ID`: - - 5 beads repos → `beads.dev` - - 5 vc repos → `vc.dev` - - 3 wyvern repos → `wyvern.dev` -- [ ] All workspaces allowed: `direnv allow` in each -- [ ] Env vars load automatically when `cd`-ing into workspace - -### āœ… beads Binary -- [ ] All workspaces using new version with Agent Mail support -- [ ] `bd --version` shows 0.23.0+ everywhere -- [ ] `bd info --json | grep agent_mail` shows config - -### āœ… Multi-Agent Coordination -- [ ] Same channel: Reservation conflict works (tested in Step 6a) -- [ ] Different channels: No interference (tested in Step 6b) -- [ ] Web UI shows all 3 channels: http://127.0.0.1:8765/mail -- [ ] Status script works: `./scripts/agent-mail-status.sh` - -### āœ… Persistence -- [ ] Server survives reboot (if launchd configured in Step 8) -- [ ] Reservations cleared on server restart (expected behavior) -- [ ] Agents re-register automatically after server restart - -## Common Issues - -### Issue: direnv not loading .envrc - -**Symptoms:** -```bash -cd ~/src/fred/beads -echo $BEADS_PROJECT_ID -# (empty output) -``` - -**Fix:** -```bash -# Check direnv hook installed -grep direnv ~/.zshrc -# Should see: eval "$(direnv hook zsh)" - -# If missing, add it -echo 'eval "$(direnv hook zsh)"' >> ~/.zshrc -source ~/.zshrc - -# Allow .envrc -cd ~/src/fred/beads -direnv allow -``` - -### Issue: Agent names collide - -**Symptoms:** Two workspaces use same `BEADS_AGENT_NAME` - -**Fix:** Edit `.envrc` to make agent names unique: -```bash -# Bad (collision!) -export BEADS_AGENT_NAME=fred-beads-macbook # Same in fred/beads and fred/vc - -# Good (unique) -export BEADS_AGENT_NAME=fred-beads-macbook # In fred/beads -export BEADS_AGENT_NAME=fred-vc-macbook # In fred/vc -``` - -The `setup-agent-mail-workspace.sh` script already handles this by including workspace name. - -### Issue: Server not accessible - -**Symptoms:** -```bash -bd update bd-42 --status in_progress -# WARN Agent Mail unavailable, falling back to git-only mode -``` - -**Fix:** -```bash -# Check server health -curl http://127.0.0.1:8765/health -# If unreachable, restart server: -./scripts/stop-agent-mail-server.sh -./scripts/start-agent-mail-server.sh -``` - -### Issue: Old reservations stuck - -**Symptoms:** Agent crashed but reservation persists - -**Fix:** -```bash -# Option 1: Release via API -curl -X DELETE http://127.0.0.1:8765/api/reservations/bd-stuck - -# Option 2: Restart server (clears all) -./scripts/stop-agent-mail-server.sh -./scripts/start-agent-mail-server.sh -``` - -## Maintenance - -### Daily Operations - -**Start/stop server manually:** -```bash -cd ~/src/fred/beads -./scripts/start-agent-mail-server.sh -./scripts/stop-agent-mail-server.sh -``` - -**Check status:** -```bash -./scripts/agent-mail-status.sh -``` - -**View logs:** -```bash -tail -f ~/agent-mail.log -``` - -**Monitor Web UI:** -```bash -open http://127.0.0.1:8765/mail -``` - -### Upgrading beads - -When you bump beads version in the future: - -```bash -cd ~/src/fred/beads - -# Bump version -./scripts/bump-version.sh 0.24.0 --commit -git push origin main -git push origin v0.24.0 - -# Wait for release, then upgrade all workspaces -curl -sSL https://raw.githubusercontent.com/steveyegge/beads/main/install.sh | bash - -# Or rebuild locally and distribute -go build -o bd ./cmd/bd -# ... copy to other repos -``` - -### Adding New Workspaces - -To add a new workspace to Agent Mail coordination: - -```bash -# Example: Adding ~/src/gina/beads -cd ~/src/gina/beads - -# Run setup script -~/src/fred/beads/scripts/setup-agent-mail-workspace.sh . - -# Allow direnv -direnv allow - -# Verify configuration -echo $BEADS_PROJECT_ID -# Expected: beads.dev (or vc.dev/wyvern.dev) - -# Test reservation -bd ready | head -1 | xargs -I {} bd update {} --status in_progress -# Should work immediately -``` - -## Success Criteria - -You've successfully deployed Agent Mail when: - -1. āœ… **Server running**: `curl http://127.0.0.1:8765/health` returns healthy -2. āœ… **All workspaces configured**: 13 `.envrc` files created and allowed -3. āœ… **Three channels active**: beads.dev, vc.dev, wyvern.dev visible in Web UI -4. āœ… **Coordination works**: Reservation conflict test passes (Step 6a) -5. āœ… **Isolation works**: Different channels don't interfere (Step 6b) -6. āœ… **Monitoring works**: Status script shows all active projects -7. āœ… **Auto-start works**: Server survives reboot (if Step 8 completed) - -## Next Steps - -After successful deployment: - -1. **Start using bd normally** - Agent Mail coordination happens automatically -2. **Monitor reservations** via Web UI during concurrent work -3. **File issues** for any coordination bugs (rare with hash-based IDs) -4. **Consider MCP integration** for Claude Desktop (see `docs/AGENT_MAIL_QUICKSTART.md`) -5. **Wait for cross-project messaging** (planned in Agent Mail roadmap) - -## Reference - -- **Main Guide**: [AGENT_MAIL_MULTI_WORKSPACE_SETUP.md](AGENT_MAIL_MULTI_WORKSPACE_SETUP.md) -- **Quick Start**: [AGENT_MAIL_QUICKSTART.md](AGENT_MAIL_QUICKSTART.md) -- **Architecture**: [AGENT_MAIL.md](AGENT_MAIL.md) -- **ADR**: [adr/002-agent-mail-integration.md](adr/002-agent-mail-integration.md) - -## Timeline Estimate - -- **Step 1** (Version bump): 5 minutes -- **Step 2** (Start server): 2 minutes -- **Step 3** (Configure 13 workspaces): 10 minutes -- **Step 4** (Upgrade binaries): 5 minutes -- **Step 5** (Document): 2 minutes -- **Step 6** (Same-channel test): 5 minutes -- **Step 7** (Channel isolation test): 5 minutes -- **Step 8** (Monitor): 2 minutes -- **Step 9** (Auto-start, optional): 5 minutes -- **Step 10** (Verify checklist): 5 minutes - -**Total: ~46 minutes** (or ~41 minutes if skipping auto-start) - -You're ready to deploy! šŸš€ diff --git a/docs/AGENT_MAIL_MULTI_WORKSPACE_SETUP.md b/docs/AGENT_MAIL_MULTI_WORKSPACE_SETUP.md deleted file mode 100644 index 3b16788a..00000000 --- a/docs/AGENT_MAIL_MULTI_WORKSPACE_SETUP.md +++ /dev/null @@ -1,345 +0,0 @@ -# Multi-Workspace Agent Mail Setup - -Guide for running Agent Mail across multiple beads repositories. - -## Service Management - -### Start Agent Mail Server (One Instance for All Projects) - -```bash -# Start in background with nohup -cd ~/src/mcp_agent_mail -source .venv/bin/activate -nohup python -m mcp_agent_mail.cli serve-http --host 127.0.0.1 --port 8765 > ~/agent-mail.log 2>&1 & -echo $! > ~/agent-mail.pid -``` - -### Check Server Status - -```bash -# Health check -curl http://127.0.0.1:8765/health - -# View logs -tail -f ~/agent-mail.log - -# Check process -ps aux | grep mcp_agent_mail -# Or use saved PID -ps -p $(cat ~/agent-mail.pid) || echo "Server not running" -``` - -### Restart Server - -```bash -# Kill old server -kill $(cat ~/agent-mail.pid) 2>/dev/null || pkill -f mcp_agent_mail - -# Start fresh -cd ~/src/mcp_agent_mail -source .venv/bin/activate -nohup python -m mcp_agent_mail.cli serve-http --host 127.0.0.1 --port 8765 > ~/agent-mail.log 2>&1 & -echo $! > ~/agent-mail.pid -``` - -### Auto-Start on Reboot (macOS launchd) - -```bash -# Create ~/Library/LaunchAgents/com.user.mcp-agent-mail.plist -cat > ~/Library/LaunchAgents/com.user.mcp-agent-mail.plist <<'EOF' - - - - - Label - com.user.mcp-agent-mail - ProgramArguments - - /Users/stevey/src/mcp_agent_mail/.venv/bin/python - -m - mcp_agent_mail.cli - serve-http - --host - 127.0.0.1 - --port - 8765 - - WorkingDirectory - /Users/stevey/src/mcp_agent_mail - StandardOutPath - /Users/stevey/agent-mail.log - StandardErrorPath - /Users/stevey/agent-mail-error.log - RunAtLoad - - KeepAlive - - - -EOF - -# Load service -launchctl load ~/Library/LaunchAgents/com.user.mcp-agent-mail.plist - -# Check status -launchctl list | grep mcp-agent-mail -``` - -## Project Configuration Strategy - -**Three team channels** for tightly-coupled workers: - -```bash -beads.dev → All beads workers (5 repos) -vc.dev → All vc workers (5 repos) -wyvern.dev → All wyvern workers (3 repos) -``` - -**Why this design:** -- Each team's workers are tightly coupled and need frequent coordination -- Simple namespace strings (no filesystem paths required) -- Usenet-style naming for clarity -- Cross-project filing handled via git/PRs for now (until Agent Mail adds cross-project messaging) - -### Configuration by Workspace - -```bash -# All beads repos use same project -~/src/beads → BEADS_PROJECT_ID=beads.dev -~/src/cino/beads → BEADS_PROJECT_ID=beads.dev -~/src/dave/beads → BEADS_PROJECT_ID=beads.dev -~/src/emma/beads → BEADS_PROJECT_ID=beads.dev -~/src/fred/beads → BEADS_PROJECT_ID=beads.dev - -# All vc repos use same project -~/src/cino/vc → BEADS_PROJECT_ID=vc.dev -~/src/dave/vc → BEADS_PROJECT_ID=vc.dev -~/src/fred/vc → BEADS_PROJECT_ID=vc.dev -~/src/vc → BEADS_PROJECT_ID=vc.dev -(standalone at ~/src/vc if exists) - -# All wyvern repos use same project -~/src/cino/wyvern → BEADS_PROJECT_ID=wyvern.dev -~/src/fred/wyvern → BEADS_PROJECT_ID=wyvern.dev -~/src/wyvern → BEADS_PROJECT_ID=wyvern.dev -``` - -## Per-Directory Configuration - -Create `.envrc` or `.env` file in each workspace: - -### Example: ~/src/fred/vc/.envrc - -```bash -# Agent Mail Configuration for fred's vc -export BEADS_AGENT_MAIL_URL=http://127.0.0.1:8765 -export BEADS_AGENT_NAME=fred-vc-$(hostname) -export BEADS_PROJECT_ID=vc.dev - -# Load with direnv -# Install direnv: brew install direnv -# Then: direnv allow -``` - -### Example: ~/src/cino/beads/.envrc - -```bash -# Agent Mail Configuration for cino's beads fork -export BEADS_AGENT_MAIL_URL=http://127.0.0.1:8765 -export BEADS_AGENT_NAME=cino-beads-$(hostname) -export BEADS_PROJECT_ID=beads.dev -``` - -### Quick Setup Script - -```bash -#!/bin/bash -# setup-agent-mail.sh - Run in each workspace - -WORKSPACE=$(pwd) -WORKSPACE_NAME=$(basename $WORKSPACE) -PARENT=$(basename $(dirname $WORKSPACE)) - -# Determine project ID based on coupling -case "$WORKSPACE_NAME" in - vc|wyvern) - # Tightly coupled - use parent coordination project - PROJECT_ID="/Users/stevey/src/${PARENT}/coordination" - ;; - beads) - # Loosely coupled - use workspace as project - PROJECT_ID="$WORKSPACE" - ;; - *) - # Default - use workspace - PROJECT_ID="$WORKSPACE" - ;; -esac - -cat > .envrc < ~/agent-mail.log 2>&1 & -echo $! > ~/agent-mail.pid -``` - -## Best Practices - -1. **Use direnv for automatic env loading** - ```bash - brew install direnv - # Add to ~/.zshrc: eval "$(direnv hook zsh)" - # Then create .envrc in each workspace - ``` - -2. **Descriptive agent names** - ```bash - # Bad: export BEADS_AGENT_NAME=agent1 - # Good: export BEADS_AGENT_NAME=fred-vc-macbook - ``` - -3. **Monitor server logs** - ```bash - tail -f ~/agent-mail.log - ``` - -4. **Health check in scripts** - ```bash - if ! curl -sf http://127.0.0.1:8765/health > /dev/null; then - echo "WARNING: Agent Mail unavailable (falling back to git-only)" - fi - ``` - -5. **Document project relationships** - - Keep this file updated when adding workspaces - - Add comments in .envrc explaining project coupling - -## Summary - -- **One server** handles all projects (lightweight HTTP API) -- **Project ID = namespace** for agent isolation -- **Tight coupling** → shared project (vc + wyvern) -- **Loose coupling** → separate projects (beads forks) -- **Auto-restart** via launchd recommended -- **Per-directory .envrc** for automatic config loading diff --git a/docs/AGENT_MAIL_QUICKSTART.md b/docs/AGENT_MAIL_QUICKSTART.md deleted file mode 100644 index c932e63d..00000000 --- a/docs/AGENT_MAIL_QUICKSTART.md +++ /dev/null @@ -1,477 +0,0 @@ -# Agent Mail Quick Start Guide - -Get started with Agent Mail for multi-agent bd coordination in 5 minutes. - -## What is Agent Mail? - -Agent Mail is an **optional** coordination layer for bd that reduces latency from 2-5 seconds (git sync) to <100ms (HTTP API) and prevents work collisions through file reservations. - -**When to use it:** -- āœ… Multiple AI agents working concurrently -- āœ… Frequent status updates (high collision risk) -- āœ… Real-time coordination needed - -**When to skip it:** -- āŒ Single agent workflows -- āŒ Infrequent updates (low collision risk) -- āŒ Simplicity preferred over latency - -## 5-Minute Setup - -### Step 1: Install Agent Mail Server (30 seconds) - -```bash -git clone https://github.com/Dicklesworthstone/mcp_agent_mail.git ~/mcp_agent_mail -cd ~/mcp_agent_mail -python3 -m venv .venv -source .venv/bin/activate # Windows: .venv\Scripts\activate -pip install -e . -``` - -### Step 2: Start the Server (5 seconds) - -```bash -python -m mcp_agent_mail.cli serve-http -# āœ… Server running on http://127.0.0.1:8765 -``` - -Leave this terminal open. Open a new terminal for Step 3. - -### Step 3: Configure Your Agent (10 seconds) - -```bash -# Set environment variables -export BEADS_AGENT_MAIL_URL=http://127.0.0.1:8765 -export BEADS_AGENT_NAME=my-agent -export BEADS_PROJECT_ID=my-project -``` - -### Step 4: Use bd Normally (30 seconds) - -```bash -# Find ready work -bd ready - -# Claim an issue -bd update bd-42 --status in_progress -# āœ… Reserved bd-42 for my-agent in <100ms - -# Complete work -bd close bd-42 "Done" -# āœ… Reservation released automatically -``` - -**That's it!** bd now uses Agent Mail for coordination. - -### Step 5: Test Multi-Agent (1 minute) - -Open a second terminal: - -```bash -# Terminal 2 - Different agent -export BEADS_AGENT_MAIL_URL=http://127.0.0.1:8765 -export BEADS_AGENT_NAME=other-agent -export BEADS_PROJECT_ID=my-project - -# Try claiming same issue -bd update bd-42 --status in_progress -# āŒ Error: bd-42 already reserved by my-agent -``` - -**Success!** Agent Mail prevented collision. - -## Common Use Cases - -### Use Case 1: Claude Desktop + Command Line Agent - -**Terminal 1 - Agent Mail Server:** -```bash -cd ~/mcp_agent_mail -source .venv/bin/activate -python -m mcp_agent_mail.cli serve-http -``` - -**Terminal 2 - Command Line:** -```bash -export BEADS_AGENT_MAIL_URL=http://127.0.0.1:8765 -export BEADS_AGENT_NAME=cli-user -export BEADS_PROJECT_ID=my-project - -bd ready -bd update bd-100 --status in_progress -``` - -**Claude Desktop:** -``` -# In Claude's MCP settings, add env vars: -{ - "beads": { - "command": "beads-mcp", - "env": { - "BEADS_AGENT_MAIL_URL": "http://127.0.0.1:8765", - "BEADS_AGENT_NAME": "claude", - "BEADS_PROJECT_ID": "my-project" - } - } -} -``` - -Now Claude and your command line won't step on each other! - -### Use Case 2: Multiple Python Agents - -**Terminal 1 - Server:** -```bash -cd ~/mcp_agent_mail -source .venv/bin/activate -python -m mcp_agent_mail.cli serve-http -``` - -**Terminal 2 - Agent A:** -```bash -cd ~/myproject/examples/python-agent -./agent_with_mail.py \ - --agent-name alice \ - --project-id myproject \ - --agent-mail-url http://127.0.0.1:8765 -``` - -**Terminal 3 - Agent B:** -```bash -cd ~/myproject/examples/python-agent -./agent_with_mail.py \ - --agent-name bob \ - --project-id myproject \ - --agent-mail-url http://127.0.0.1:8765 -``` - -Watch them coordinate in real-time! - -### Use Case 3: Team Workflow - -**Shared Server (runs on dev machine):** -```bash -# Machine 192.168.1.100 -python -m mcp_agent_mail.cli serve-http --host 0.0.0.0 -``` - -**Team Member 1:** -```bash -export BEADS_AGENT_MAIL_URL=http://192.168.1.100:8765 -export BEADS_AGENT_NAME=alice -export BEADS_PROJECT_ID=team-project - -bd ready -``` - -**Team Member 2:** -```bash -export BEADS_AGENT_MAIL_URL=http://192.168.1.100:8765 -export BEADS_AGENT_NAME=bob -export BEADS_PROJECT_ID=team-project - -bd ready -``` - -Entire team shares one coordination server! - -### Use Case 4: CI/CD Pipeline - -**GitHub Actions Example:** - -```yaml -name: AI Agent Workflow -on: [push] - -jobs: - agent-work: - runs-on: ubuntu-latest - - services: - agent-mail: - image: ghcr.io/dicklesworthstone/mcp_agent_mail:latest - ports: - - 8765:8765 - - strategy: - matrix: - agent: [agent-1, agent-2, agent-3] - - steps: - - uses: actions/checkout@v4 - - - name: Run agent - env: - BEADS_AGENT_MAIL_URL: http://localhost:8765 - BEADS_AGENT_NAME: ${{ matrix.agent }} - BEADS_PROJECT_ID: ${{ github.repository }} - run: | - bd ready | head -1 | xargs -I {} bd update {} --status in_progress - # ... do work ... - bd close {} "Completed by CI" -``` - -Three agents run in parallel without collisions! - -## Verification Checklist - -After setup, verify everything works: - -**āœ… Server is running:** -```bash -curl http://127.0.0.1:8765/health -# Expected: {"status": "healthy"} -``` - -**āœ… Environment variables are set:** -```bash -echo $BEADS_AGENT_MAIL_URL -# Expected: http://127.0.0.1:8765 - -echo $BEADS_AGENT_NAME -# Expected: my-agent - -echo $BEADS_PROJECT_ID -# Expected: my-project -``` - -**āœ… bd sees Agent Mail:** -```bash -bd info --json | grep agent_mail -# Expected: JSON with agent_mail config -``` - -**āœ… Reservations work:** -```bash -# Terminal 1 -bd update bd-test --status in_progress -# Expected: Success - -# Terminal 2 (different agent) -export BEADS_AGENT_NAME=other-agent -bd update bd-test --status in_progress -# Expected: Error - reservation conflict -``` - -**āœ… Graceful degradation works:** -```bash -# Stop server (Ctrl+C in server terminal) - -# Try bd command -bd ready -# Expected: Warning about Agent Mail unavailable, but command succeeds -``` - -## Troubleshooting - -### Problem: "Agent Mail unavailable" warnings - -**Symptoms:** -``` -WARN Agent Mail unavailable, falling back to git-only mode -``` - -**Quick Fix:** -1. Check server is running: `curl http://127.0.0.1:8765/health` -2. Verify URL is correct: `echo $BEADS_AGENT_MAIL_URL` -3. Restart server if needed - -### Problem: Agents don't see each other's reservations - -**Cause:** Different `BEADS_PROJECT_ID` values - -**Quick Fix:** -```bash -# All agents MUST use same project ID! -export BEADS_PROJECT_ID=same-project-name -``` - -### Problem: Reservation stuck after agent crashes - -**Quick Fix:** -```bash -# Option 1: Release via API -curl -X DELETE http://127.0.0.1:8765/api/reservations/bd-stuck - -# Option 2: Restart server (clears all reservations) -pkill -f mcp_agent_mail -python -m mcp_agent_mail.cli serve-http -``` - -### Problem: Port 8765 already in use - -**Quick Fix:** -```bash -# Find what's using port -lsof -i :8765 # macOS/Linux -netstat -ano | findstr :8765 # Windows - -# Kill old server -pkill -f mcp_agent_mail - -# Or use different port -python -m mcp_agent_mail.cli serve-http --port 8766 -export BEADS_AGENT_MAIL_URL=http://127.0.0.1:8766 -``` - -## Monitoring - -### Web UI - -View all reservations in real-time: -```bash -open http://127.0.0.1:8765/mail -``` - -### API - -Check reservations programmatically: -```bash -# List all reservations -curl http://127.0.0.1:8765/api/reservations | jq - -# Check specific reservation -curl http://127.0.0.1:8765/api/reservations/bd-42 | jq - -# Release reservation manually -curl -X DELETE http://127.0.0.1:8765/api/reservations/bd-42 -``` - -### Logs - -Agent Mail logs to stdout. Redirect to file if needed: -```bash -python -m mcp_agent_mail.cli serve-http > agent-mail.log 2>&1 & -tail -f agent-mail.log -``` - -## Best Practices - -### 1. Use Descriptive Agent Names - -**Bad:** -```bash -export BEADS_AGENT_NAME=agent1 -export BEADS_AGENT_NAME=agent2 -``` - -**Good:** -```bash -export BEADS_AGENT_NAME=claude-frontend -export BEADS_AGENT_NAME=gpt4-backend -export BEADS_AGENT_NAME=alice-laptop -``` - -Makes debugging much easier! - -### 2. Set Environment Variables Globally - -**Option 1: Shell Profile** -```bash -# Add to ~/.bashrc or ~/.zshrc -export BEADS_AGENT_MAIL_URL=http://127.0.0.1:8765 -export BEADS_AGENT_NAME=$(whoami)-$(hostname) -export BEADS_PROJECT_ID=$(basename $(pwd)) -``` - -**Option 2: Project Config** -```bash -# .env file in project root -BEADS_AGENT_MAIL_URL=http://127.0.0.1:8765 -BEADS_AGENT_NAME=my-agent -BEADS_PROJECT_ID=my-project - -# Load in scripts -source .env -``` - -### 3. Use Same Project ID Across Team - -Create a shared config: -```bash -# team-config.sh -export BEADS_PROJECT_ID=our-team-project -export BEADS_AGENT_MAIL_URL=http://agent-mail.internal:8765 - -# Each team member sources it -source team-config.sh -export BEADS_AGENT_NAME=alice # Only this differs per person -``` - -### 4. Monitor Reservations in Long-Running Agents - -```python -# Check reservation health periodically -import requests - -def check_reservations(): - resp = requests.get(f"{agent_mail_url}/api/reservations") - my_reservations = [r for r in resp.json() if r["agent_id"] == agent_name] - - for res in my_reservations: - # Release if work completed - if is_done(res["resource_id"]): - requests.delete(f"{agent_mail_url}/api/reservations/{res['resource_id']}") -``` - -### 5. Handle Graceful Degradation - -Always assume Agent Mail might be unavailable: -```python -try: - bd_update(issue_id, status="in_progress") -except ReservationConflict: - # Expected - try different issue - pass -except Exception: - # Agent Mail down - falls back to git - # Continue normally - pass -``` - -## Next Steps - -1. **Read the full guide**: [AGENT_MAIL.md](AGENT_MAIL.md) -2. **Try the Python example**: [examples/python-agent/AGENT_MAIL_EXAMPLE.md](../examples/python-agent/AGENT_MAIL_EXAMPLE.md) -3. **Review the ADR**: [adr/002-agent-mail-integration.md](adr/002-agent-mail-integration.md) - -## Getting Help - -**Documentation:** -- [AGENT_MAIL.md](AGENT_MAIL.md) - Complete integration guide -- [TROUBLESHOOTING.md](TROUBLESHOOTING.md) - General bd troubleshooting -- [FAQ.md](FAQ.md) - Frequently asked questions - -**Issues:** -- [bd issues](https://github.com/steveyegge/beads/issues) - Integration bugs -- [Agent Mail issues](https://github.com/Dicklesworthstone/mcp_agent_mail/issues) - Server bugs - -**Community:** -- [Discussions](https://github.com/steveyegge/beads/discussions) - Ask questions -- [Examples](../examples/) - Learn from working code - -## TL;DR - Copy-Paste Setup - -```bash -# 1. Install Agent Mail -git clone https://github.com/Dicklesworthstone/mcp_agent_mail.git ~/mcp_agent_mail -cd ~/mcp_agent_mail -python3 -m venv .venv -source .venv/bin/activate -pip install -e . - -# 2. Start server (leave running) -python -m mcp_agent_mail.cli serve-http & - -# 3. Configure agent (in new terminal) -export BEADS_AGENT_MAIL_URL=http://127.0.0.1:8765 -export BEADS_AGENT_NAME=my-agent -export BEADS_PROJECT_ID=my-project - -# 4. Use bd normally - coordination happens automatically! -bd ready -bd update bd-42 --status in_progress -bd close bd-42 "Done" -``` - -**Done!** You're now using Agent Mail for sub-100ms coordination. diff --git a/docs/CLI_REFERENCE.md b/docs/CLI_REFERENCE.md index d0df387c..13188a71 100644 --- a/docs/CLI_REFERENCE.md +++ b/docs/CLI_REFERENCE.md @@ -25,8 +25,7 @@ bd info --json # { # "database_path": "/path/to/.beads/beads.db", # "issue_prefix": "bd", -# "daemon_running": true, -# "agent_mail_enabled": false +# "daemon_running": true # } ``` diff --git a/docs/QUICKSTART.md b/docs/QUICKSTART.md index 2089c4a0..762ba76d 100644 --- a/docs/QUICKSTART.md +++ b/docs/QUICKSTART.md @@ -197,40 +197,6 @@ bd cleanup --force **Note:** Compaction is permanent graceful decay. Original content is discarded but viewable via `bd restore ` from git history. -## Advanced: Agent Mail (Optional) - -For **multi-agent workflows** (2+ AI agents working concurrently), Agent Mail provides real-time coordination: - -**Benefits:** -- 20-50x latency reduction (<100ms vs 2-5s git sync) -- Collision prevention via file reservations -- Agents can't accidentally claim same issue - -**Quick setup:** -```bash -# Install and start server (one-time) -git clone https://github.com/Dicklesworthstone/mcp_agent_mail.git -cd mcp_agent_mail && python3 -m venv .venv && source .venv/bin/activate -pip install -e . -python -m mcp_agent_mail.cli serve-http - -# Configure each agent (environment variables) -export BEADS_AGENT_MAIL_URL=http://127.0.0.1:8765 -export BEADS_AGENT_NAME=assistant-alpha # Unique per agent -export BEADS_PROJECT_ID=my-project - -# Use bd normally - Agent Mail auto-activates -bd ready -bd update bd-42 --status in_progress # Reserves instantly -``` - -**When to use:** -- āœ… Multiple agents working simultaneously -- āœ… High collision risk (frequent status updates) -- āŒ Single agent workflows (unnecessary overhead) - -See [AGENT_MAIL.md](AGENT_MAIL.md) for complete guide. - ## Background Daemon bd runs a background daemon for auto-sync and performance. You rarely need to manage it directly: diff --git a/docs/adr/002-agent-mail-integration.md b/docs/adr/002-agent-mail-integration.md deleted file mode 100644 index 5f668122..00000000 --- a/docs/adr/002-agent-mail-integration.md +++ /dev/null @@ -1,228 +0,0 @@ -# ADR 002: MCP Agent Mail Integration for Multi-Agent Coordination - -**Status:** Proposed -**Date:** 2025-11-08 -**Epic:** [bd-spmx](../../.beads/beads.db) (Investigation & Proof of Concept) -**Related Issues:** bd-6hji, bd-htfk, bd-muls - -## Context - -Beads is designed for AI-supervised coding workflows where multiple AI agents coordinate work on shared codebases. As multi-agent systems become more common, we face challenges: - -### Problem Statement - -1. **Git Sync Latency**: Current git-based synchronization has 2-5 second round-trip latency for status updates -2. **No Collision Prevention**: Two agents can claim the same issue simultaneously, causing wasted work and merge conflicts -3. **Git Repository Pollution**: Frequent agent status updates create noisy git history with dozens of micro-commits -4. **Lack of Real-Time Awareness**: Agents don't know what other agents are working on until after git sync completes - -### Current Workflow - -``` -Agent A: bd update bd-123 --status in_progress - ↓ (30s debounce) - ↓ export to JSONL - ↓ git commit + push (1-2s) - ↓ -Agent B: git pull (1-2s) - ↓ import from JSONL - ↓ sees bd-123 is taken (too late!) -``` - -Total latency: **2000-5000ms** - -## Decision - -**Adopt MCP Agent Mail as an *optional* coordination layer** for real-time multi-agent communication, while maintaining full backward compatibility with git-only workflows. - -## Alternatives Considered - -### 1. Custom RPC Server -**Pros:** -- Full control over implementation -- Optimized for beads-specific needs - -**Cons:** -- High development cost (3-4 weeks) -- Maintenance burden -- Reinventing the wheel - -**Verdict:** āŒ Too much effort for marginal benefit - -### 2. Redis/Memcached -**Pros:** -- Battle-tested infrastructure -- Low latency - -**Cons:** -- Heavy dependency (requires separate service) -- Overkill for lightweight coordination -- No built-in authentication/multi-tenancy - -**Verdict:** āŒ Too heavy for beads' lightweight ethos - -### 3. Git-Only (Status Quo) -**Pros:** -- Zero dependencies -- Works everywhere git works - -**Cons:** -- 2-5s latency for status updates -- No collision prevention -- Noisy git history - -**Verdict:** āœ… Remains the default, Agent Mail is optional enhancement - -### 4. MCP Agent Mail (Chosen) -**Pros:** -- Lightweight HTTP server (<50MB memory) -- <100ms latency for status updates (20-50x faster than git) -- Built-in file reservation system (prevents collisions) -- Project/agent isolation (multi-tenancy support) -- Optional: graceful degradation to git-only mode -- Active maintenance by @Dicklesworthstone - -**Cons:** -- External dependency (requires running server) -- Adds complexity for single-agent workflows (mitigated by optional nature) - -**Verdict:** āœ… Best balance of benefits vs. cost - -## Integration Principles - -### 1. **Optional & Non-Intrusive** -- Agent Mail is 100% optional -- Beads works identically without it (git-only mode) -- No breaking changes to existing workflows - -### 2. **Graceful Degradation** -- If server unavailable, fall back to git-only sync -- No errors, no crashes, just log a warning - -### 3. **Lightweight HTTP Client** -- Use standard library HTTP client (no SDK bloat) -- Minimal code footprint in beads (<500 LOC) - -### 4. **Configuration via Environment** -```bash -# Enable Agent Mail (optional) -export BEADS_AGENT_MAIL_URL=http://127.0.0.1:8765 -export BEADS_AGENT_MAIL_TOKEN= -export BEADS_AGENT_NAME=assistant-alpha - -# Disabled by default (git-only mode) -bd ready # Works without Agent Mail -``` - -## Proof of Concept Results - -### File Reservation Testing (bd-6hji) āœ… -- **Test:** Two agents (BrownBear, ChartreuseHill) race to claim bd-123 -- **Result:** First agent gets reservation, second gets clear conflict error -- **Verdict:** Collision prevention works as expected - -### Latency Benchmarking (bd-htfk) āœ… -- **Git Sync:** 2000-5000ms (commit + push + pull + import) -- **Agent Mail:** <100ms (HTTP send + fetch round-trip) -- **Improvement:** 20-50x latency reduction -- **Verdict:** Real-time coordination achievable - -### Installation (bd-muls) āœ… -- **Server:** Runs on port 8765, <50MB memory -- **Web UI:** Accessible for human supervision -- **Verdict:** Easy to deploy and monitor - -## Architecture - -``` -ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” -│ bd (Beads CLI) │ -│ │ -│ ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” │ -│ │ Git Sync │ │ Agent Mail │ │ -│ │ (required) │ │ (optional) │ │ -│ │ │ │ │ │ -│ │ - Export │ │ - Reservations │ │ -│ │ - Import │ │ - Notifications │ │ -│ │ - Commit │ │ - Status updates│ │ -│ │ - Push/Pull │ │ │ │ -│ ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ │ -│ │ │ │ -ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ - │ │ - ā–¼ ā–¼ - ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” - │ .beads/ │ │ Agent Mail │ - │ issues.jsonl │ │ Server │ - │ (git) │ │ (HTTP) │ - ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ -``` - -### Coordination Flow (with Agent Mail) - -``` -Agent A: bd update bd-123 --status in_progress - ↓ - ā”œā”€ Agent Mail: POST /api/reservations (5ms) - │ └─ Reserve bd-123 for Agent A - ā”œā”€ Local: Update .beads/beads.db - └─ Background: Export to JSONL (30s debounce) - -Agent B: bd update bd-123 --status in_progress - ↓ - └─ Agent Mail: POST /api/reservations (5ms) - └─ HTTP 409 Conflict: "bd-123 reserved by Agent A" - └─ bd exits with clear error - -Total latency: <100ms (vs 2000-5000ms with git-only) -``` - -## Implementation Plan - -### Phase 1: Core Integration (bd-wfmw) -- [ ] HTTP client wrapper for Agent Mail API -- [ ] Reservation check before status updates -- [ ] Graceful fallback when server unavailable -- [ ] Environment-based configuration - -### Phase 2: Enhanced Features -- [ ] Notification system (agent X finished bd-Y) -- [ ] Automatic reservation expiry (TTL) -- [ ] Multi-project support -- [ ] Web dashboard for human supervision - -### Phase 3: Documentation -- [ ] Quick start guide -- [ ] Multi-agent workflow examples -- [ ] Troubleshooting guide - -## Risks & Mitigations - -### Risk 1: Server Dependency -**Mitigation:** Graceful degradation to git-only mode. Beads never *requires* Agent Mail. - -### Risk 2: Configuration Complexity -**Mitigation:** Zero config required for single-agent workflows. Environment variables for multi-agent setups. - -### Risk 3: Upstream Changes -**Mitigation:** Use HTTP API directly (not SDK). Minimal surface area for breaking changes. - -### Risk 4: Data Durability -**Mitigation:** Git remains the source of truth. Agent Mail is ephemeral coordination state only. - -## Success Metrics - -- āœ… Latency reduction: 20-50x (verified) -- āœ… Collision prevention: 100% effective (verified) -- šŸ”² Git operation reduction: ≄70% (pending bd-nemp) -- šŸ”² Zero functional regression in git-only mode - -## References - -- [MCP Agent Mail Repository](https://github.com/Dicklesworthstone/mcp_agent_mail) - -## Decision Outcome - -**Proceed with Agent Mail integration** using the optional, non-intrusive approach outlined above. The proof of concept validated the core benefits (latency, collision prevention) while the lightweight HTTP integration minimizes risk and complexity. - -Git-only mode remains the default and fully supported workflow for single-agent scenarios. diff --git a/examples/README.md b/examples/README.md index 2bb01f91..cd8cbc16 100644 --- a/examples/README.md +++ b/examples/README.md @@ -6,7 +6,6 @@ This directory contains examples of how to integrate bd with AI agents and workf ### Agent Integration - **[python-agent/](python-agent/)** - Simple Python agent that discovers ready work and completes tasks - - **[AGENT_MAIL_EXAMPLE.md](python-agent/AGENT_MAIL_EXAMPLE.md)** - Multi-agent coordination with Agent Mail - **[bash-agent/](bash-agent/)** - Bash script showing the full agent workflow - **[startup-hooks/](startup-hooks/)** - Session startup scripts for automatic bd upgrade detection - **[claude-desktop-mcp/](claude-desktop-mcp/)** - MCP server for Claude Desktop integration diff --git a/examples/bash-agent/README.md b/examples/bash-agent/README.md index 63c43399..395cdf0b 100644 --- a/examples/bash-agent/README.md +++ b/examples/bash-agent/README.md @@ -10,7 +10,6 @@ A bash script demonstrating how an AI agent can use bd to manage tasks autonomou - Random issue creation to simulate real agent behavior - Dependency linking with `discovered-from` - Statistics display -- **Optional Agent Mail integration** for multi-agent coordination ## Prerequisites @@ -21,8 +20,6 @@ A bash script demonstrating how an AI agent can use bd to manage tasks autonomou ## Usage -### Basic (Single Agent) - ```bash # Make executable chmod +x agent.sh @@ -34,29 +31,6 @@ chmod +x agent.sh ./agent.sh 20 ``` -### Multi-Agent Mode (with Agent Mail) - -```bash -# Terminal 1: Start Agent Mail server -cd ~/src/mcp_agent_mail -source .venv/bin/activate -python -m mcp_agent_mail.cli serve-http - -# Terminal 2: Run first agent -export BEADS_AGENT_MAIL_URL=http://127.0.0.1:8765 -export BEADS_AGENT_NAME=bash-agent-1 -export BEADS_PROJECT_ID=my-project -./agent.sh 10 - -# Terminal 3: Run second agent (simultaneously) -export BEADS_AGENT_MAIL_URL=http://127.0.0.1:8765 -export BEADS_AGENT_NAME=bash-agent-2 -export BEADS_PROJECT_ID=my-project -./agent.sh 10 -``` - -Agents will coordinate via Agent Mail to prevent claiming the same issues. - ## What It Does The agent runs in a loop: diff --git a/examples/bash-agent/agent.sh b/examples/bash-agent/agent.sh index be548d09..85b64e64 100755 --- a/examples/bash-agent/agent.sh +++ b/examples/bash-agent/agent.sh @@ -4,15 +4,10 @@ # # This demonstrates the full lifecycle of an agent managing tasks: # - Find ready work -# - Claim and execute (with optional Agent Mail reservation) +# - Claim and execute # - Discover new issues # - Link discoveries # - Complete and move on -# -# Optional Agent Mail integration: -# export BEADS_AGENT_MAIL_URL=http://127.0.0.1:8765 -# export BEADS_AGENT_NAME=bash-agent-1 -# export BEADS_PROJECT_ID=my-project set -euo pipefail @@ -39,87 +34,7 @@ log_error() { echo -e "${RED}āœ—${NC} $1" } -# Agent Mail integration (optional, graceful degradation) -AGENT_MAIL_ENABLED=false -AGENT_MAIL_URL="${BEADS_AGENT_MAIL_URL:-}" AGENT_NAME="${BEADS_AGENT_NAME:-bash-agent-$$}" -PROJECT_ID="${BEADS_PROJECT_ID:-default}" - -# Check Agent Mail health -check_agent_mail() { - if [[ -z "$AGENT_MAIL_URL" ]]; then - return 1 - fi - - if ! command -v curl &> /dev/null; then - log_warning "curl not available, Agent Mail disabled" - return 1 - fi - - if curl -s -f "$AGENT_MAIL_URL/health" > /dev/null 2>&1; then - return 0 - fi - return 1 -} - -# Reserve an issue (prevents other agents from claiming) -reserve_issue() { - local issue_id="$1" - - if ! $AGENT_MAIL_ENABLED; then - return 0 # Skip if disabled - fi - - local response=$(curl -s -X POST "$AGENT_MAIL_URL/api/reserve" \ - -H "Content-Type: application/json" \ - -d "{\"file_path\": \"$issue_id\", \"agent_name\": \"$AGENT_NAME\", \"project_id\": \"$PROJECT_ID\", \"ttl_seconds\": 300}" \ - 2>/dev/null || echo "") - - if [[ -z "$response" ]] || echo "$response" | grep -q "error"; then - log_warning "Failed to reserve $issue_id (may be claimed by another agent)" - return 1 - fi - - return 0 -} - -# Release an issue reservation -release_issue() { - local issue_id="$1" - - if ! $AGENT_MAIL_ENABLED; then - return 0 # Skip if disabled - fi - - curl -s -X POST "$AGENT_MAIL_URL/api/release" \ - -H "Content-Type: application/json" \ - -d "{\"file_path\": \"$issue_id\", \"agent_name\": \"$AGENT_NAME\", \"project_id\": \"$PROJECT_ID\"}" \ - > /dev/null 2>&1 || true -} - -# Send notification to other agents -notify() { - local event_type="$1" - local issue_id="$2" - local message="$3" - - if ! $AGENT_MAIL_ENABLED; then - return 0 # Skip if disabled - fi - - curl -s -X POST "$AGENT_MAIL_URL/api/notify" \ - -H "Content-Type: application/json" \ - -d "{\"event_type\": \"$event_type\", \"from_agent\": \"$AGENT_NAME\", \"project_id\": \"$PROJECT_ID\", \"payload\": {\"issue_id\": \"$issue_id\", \"message\": \"$message\"}}" \ - > /dev/null 2>&1 || true -} - -# Initialize Agent Mail -if check_agent_mail; then - AGENT_MAIL_ENABLED=true - log_success "Agent Mail enabled (agent: $AGENT_NAME)" -else - log_info "Agent Mail disabled (Beads-only mode)" -fi # Check if bd is installed if ! command -v bd &> /dev/null; then @@ -150,19 +65,10 @@ get_field() { # Claim a task claim_task() { local issue_id="$1" - - # Try to reserve first (Agent Mail) - if ! reserve_issue "$issue_id"; then - log_error "Could not reserve $issue_id - skipping" - return 1 - fi - + log_info "Claiming task: $issue_id" bd update "$issue_id" --status in_progress --json > /dev/null - - # Notify other agents - notify "status_changed" "$issue_id" "Claimed by $AGENT_NAME" - + log_success "Task claimed" return 0 } @@ -212,11 +118,7 @@ complete_task() { log_info "Completing task: $issue_id" bd close "$issue_id" --reason "$reason" --json > /dev/null - - # Notify completion and release reservation - notify "issue_completed" "$issue_id" "$reason" - release_issue "$issue_id" - + log_success "Task completed: $issue_id" } @@ -259,9 +161,9 @@ run_agent() { issue_id=$(get_field "$ready_work" "id") - # Claim it (may fail if another agent reserved it) + # Claim it if ! claim_task "$issue_id"; then - log_warning "Skipping already-claimed task, trying next iteration" + log_warning "Failed to claim task, trying next iteration" continue fi diff --git a/examples/go-agent/README.md b/examples/go-agent/README.md deleted file mode 100644 index bc21c6b8..00000000 --- a/examples/go-agent/README.md +++ /dev/null @@ -1,97 +0,0 @@ -# Go Agent Example - -Example Go agent that uses bd with optional Agent Mail coordination for multi-agent workflows. - -## Features - -- Uses native Go Agent Mail client (`pkg/agentmail`) -- Graceful degradation when Agent Mail unavailable -- Handles reservation conflicts -- Discovers and links new work -- Environment-based configuration - -## Usage - -### Git-only mode (no Agent Mail) - -```bash -cd examples/go-agent -go run main.go --agent-name agent-alpha --max-iterations 5 -``` - -### With Agent Mail coordination - -```bash -# Start Agent Mail server (in separate terminal) -cd integrations/agent-mail -python server.py - -# Run agent -cd examples/go-agent -go run main.go \ - --agent-name agent-alpha \ - --project-id my-project \ - --agent-mail-url http://127.0.0.1:8765 \ - --max-iterations 10 -``` - -### Environment Variables - -```bash -export BEADS_AGENT_NAME=agent-alpha -export BEADS_PROJECT_ID=my-project -export BEADS_AGENT_MAIL_URL=http://127.0.0.1:8765 - -go run main.go -``` - -## Multi-Agent Demo - -Run multiple agents concurrently with Agent Mail: - -```bash -# Terminal 1: Start Agent Mail server -cd integrations/agent-mail -python server.py - -# Terminal 2: Agent Alpha -cd examples/go-agent -go run main.go --agent-name agent-alpha --agent-mail-url http://127.0.0.1:8765 - -# Terminal 3: Agent Beta -go run main.go --agent-name agent-beta --agent-mail-url http://127.0.0.1:8765 -``` - -## How It Works - -1. **Initialization**: Creates Agent Mail client with health check -2. **Find work**: Queries `bd ready` for unblocked issues -3. **Claim issue**: Reserves via Agent Mail (if enabled) and updates status to `in_progress` -4. **Work simulation**: Processes the issue (sleeps 1s in this example) -5. **Discover work**: 33% chance to create linked issue via `discovered-from` dependency -6. **Complete**: Closes issue and releases Agent Mail reservation - -## Collision Handling - -When Agent Mail is enabled: -- Issues are reserved before claiming (prevents race conditions) -- Conflicts return immediately (<100ms latency) -- Agents gracefully skip reserved issues - -Without Agent Mail: -- Relies on git-based eventual consistency -- Higher latency (2-5s for sync) -- Collision detection via git merge conflicts - -## Comparison with Python Agent - -The Go implementation mirrors the Python agent (`examples/python-agent/agent_with_mail.py`): -- āœ… Same API surface (ReserveIssue, ReleaseIssue, Notify, CheckInbox) -- āœ… Same graceful degradation behavior -- āœ… Same environment variable configuration -- āœ… Native Go types and idioms (no shell exec for Agent Mail) - -Key differences: -- Go uses `pkg/agentmail.Client` instead of `lib/beads_mail_adapter.py` -- Go struct methods vs Python class methods -- Type safety with Go structs diff --git a/examples/go-agent/go.mod b/examples/go-agent/go.mod deleted file mode 100644 index fa0e2d70..00000000 --- a/examples/go-agent/go.mod +++ /dev/null @@ -1,7 +0,0 @@ -module github.com/steveyegge/beads/examples/go-agent - -go 1.24.0 - -replace github.com/steveyegge/beads => ../.. - -require github.com/steveyegge/beads v0.0.0-00010101000000-000000000000 diff --git a/examples/go-agent/main.go b/examples/go-agent/main.go deleted file mode 100644 index e0a94bb9..00000000 --- a/examples/go-agent/main.go +++ /dev/null @@ -1,238 +0,0 @@ -package main - -import ( - "encoding/json" - "flag" - "fmt" - "math/rand" - "os" - "os/exec" - "strings" - "time" - - "github.com/steveyegge/beads/pkg/agentmail" -) - -type Issue struct { - ID string `json:"id"` - Title string `json:"title"` - Description string `json:"description"` - Status string `json:"status"` - Priority int `json:"priority"` - IssueType string `json:"issue_type"` -} - -type BeadsAgent struct { - agentName string - projectID string - agentMailURL string - mailClient *agentmail.Client - maxIterations int -} - -func NewBeadsAgent(agentName, projectID, agentMailURL string, maxIterations int) *BeadsAgent { - agent := &BeadsAgent{ - agentName: agentName, - projectID: projectID, - agentMailURL: agentMailURL, - maxIterations: maxIterations, - } - - if agentMailURL != "" { - _ = os.Setenv("BEADS_AGENT_MAIL_URL", agentMailURL) - _ = os.Setenv("BEADS_AGENT_NAME", agentName) - _ = os.Setenv("BEADS_PROJECT_ID", projectID) - agent.mailClient = agentmail.NewClient( - agentmail.WithURL(agentMailURL), - agentmail.WithAgentName(agentName), - ) - if agent.mailClient.Enabled { - fmt.Printf("✨ Agent Mail enabled: %s @ %s\n", agentName, agentMailURL) - } else { - fmt.Printf("šŸ“ Git-only mode: %s (Agent Mail unavailable)\n", agentName) - } - } else { - fmt.Printf("šŸ“ Git-only mode: %s\n", agentName) - } - - return agent -} - -func (a *BeadsAgent) runBD(args ...string) ([]byte, error) { - args = append(args, "--json") - cmd := exec.Command("bd", args...) - output, err := cmd.CombinedOutput() - if err != nil { - if strings.Contains(string(output), "already reserved") || strings.Contains(string(output), "reservation conflict") { - return output, fmt.Errorf("reservation_conflict") - } - return output, err - } - return output, nil -} - -func (a *BeadsAgent) getReadyWork() ([]Issue, error) { - output, err := a.runBD("ready") - if err != nil { - return nil, err - } - - var issues []Issue - if err := json.Unmarshal(output, &issues); err != nil { - return nil, fmt.Errorf("failed to parse ready work: %w", err) - } - - return issues, nil -} - -func (a *BeadsAgent) claimIssue(issueID string) bool { - fmt.Printf("šŸ“‹ Claiming issue: %s\n", issueID) - - if a.mailClient != nil && a.mailClient.Enabled { - if !a.mailClient.ReserveIssue(issueID, 3600) { - fmt.Printf(" āš ļø Issue %s already claimed by another agent\n", issueID) - return false - } - } - - _, err := a.runBD("update", issueID, "--status", "in_progress") - if err != nil { - if err.Error() == "reservation_conflict" { - fmt.Printf(" āš ļø Issue %s already claimed by another agent\n", issueID) - return false - } - fmt.Printf(" āŒ Failed to claim %s: %v\n", issueID, err) - return false - } - - fmt.Printf(" āœ… Successfully claimed %s\n", issueID) - return true -} - -func (a *BeadsAgent) completeIssue(issueID, reason string) bool { - fmt.Printf("āœ… Completing issue: %s\n", issueID) - - _, err := a.runBD("close", issueID, "--reason", reason) - if err != nil { - fmt.Printf(" āŒ Failed to complete %s: %v\n", issueID, err) - return false - } - - if a.mailClient != nil && a.mailClient.Enabled { - a.mailClient.ReleaseIssue(issueID) - a.mailClient.Notify("issue_completed", map[string]interface{}{ - "issue_id": issueID, - "agent": a.agentName, - }) - } - - fmt.Printf(" āœ… Issue %s completed\n", issueID) - return true -} - -func (a *BeadsAgent) createDiscoveredIssue(title, parentID string, priority int, issueType string) string { - fmt.Printf("šŸ’” Creating discovered issue: %s\n", title) - - output, err := a.runBD("create", title, - "-t", issueType, - "-p", fmt.Sprintf("%d", priority), - "--deps", fmt.Sprintf("discovered-from:%s", parentID), - ) - if err != nil { - fmt.Printf(" āŒ Failed to create issue: %v\n", err) - return "" - } - - var result struct { - ID string `json:"id"` - } - if err := json.Unmarshal(output, &result); err != nil { - fmt.Printf(" āŒ Failed to parse created issue: %v\n", err) - return "" - } - - fmt.Printf(" āœ… Created %s\n", result.ID) - return result.ID -} - -func (a *BeadsAgent) simulateWork(issue Issue) { - fmt.Printf("šŸ¤– Working on: %s (%s)\n", issue.Title, issue.ID) - fmt.Printf(" Priority: %d, Type: %s\n", issue.Priority, issue.IssueType) - time.Sleep(1 * time.Second) -} - -func (a *BeadsAgent) run() { - fmt.Printf("\nšŸš€ Agent '%s' starting...\n", a.agentName) - fmt.Printf(" Project: %s\n", a.projectID) - if a.agentMailURL != "" { - fmt.Printf(" Agent Mail: Enabled\n\n") - } else { - fmt.Printf(" Agent Mail: Disabled (git-only mode)\n\n") - } - - for iteration := 1; iteration <= a.maxIterations; iteration++ { - fmt.Println(strings.Repeat("=", 60)) - fmt.Printf("Iteration %d/%d\n", iteration, a.maxIterations) - fmt.Println(strings.Repeat("=", 60)) - - readyIssues, err := a.getReadyWork() - if err != nil { - fmt.Printf("āŒ Failed to get ready work: %v\n", err) - continue - } - - if len(readyIssues) == 0 { - fmt.Println("šŸ“­ No ready work available. Stopping.") - break - } - - claimed := false - for _, issue := range readyIssues { - if a.claimIssue(issue.ID) { - claimed = true - - a.simulateWork(issue) - - // 33% chance to discover new work - if rand.Float32() < 0.33 { - discoveredTitle := fmt.Sprintf("Follow-up work for %s", issue.Title) - newID := a.createDiscoveredIssue(discoveredTitle, issue.ID, issue.Priority, "task") - if newID != "" { - fmt.Printf("šŸ”— Linked %s ← discovered-from ← %s\n", newID, issue.ID) - } - } - - a.completeIssue(issue.ID, "Implemented successfully") - break - } - } - - if !claimed { - fmt.Println("āš ļø All ready issues are reserved by other agents. Waiting...") - time.Sleep(2 * time.Second) - } - - fmt.Println() - } - - fmt.Printf("šŸ Agent '%s' finished\n", a.agentName) -} - -func main() { - agentName := flag.String("agent-name", getEnv("BEADS_AGENT_NAME", fmt.Sprintf("agent-%d", os.Getpid())), "Unique agent identifier") - projectID := flag.String("project-id", getEnv("BEADS_PROJECT_ID", "default"), "Project namespace for Agent Mail") - agentMailURL := flag.String("agent-mail-url", os.Getenv("BEADS_AGENT_MAIL_URL"), "Agent Mail server URL") - maxIterations := flag.Int("max-iterations", 10, "Maximum number of issues to process") - - flag.Parse() - - agent := NewBeadsAgent(*agentName, *projectID, *agentMailURL, *maxIterations) - agent.run() -} - -func getEnv(key, defaultValue string) string { - if val := os.Getenv(key); val != "" { - return val - } - return defaultValue -} diff --git a/examples/python-agent/AGENT_MAIL_EXAMPLE.md b/examples/python-agent/AGENT_MAIL_EXAMPLE.md deleted file mode 100644 index 3d47009f..00000000 --- a/examples/python-agent/AGENT_MAIL_EXAMPLE.md +++ /dev/null @@ -1,418 +0,0 @@ -# Agent Mail Integration Example - -This example demonstrates using bd with **Agent Mail** for multi-agent coordination. It shows how to handle reservation conflicts, graceful degradation, and best practices for real-time collaboration. - -## Quick Start - -### Prerequisites - -1. **Install bd** (0.21.0+): - ```bash - go install github.com/steveyegge/beads/cmd/bd@latest - ``` - -2. **Install Agent Mail server**: - ```bash - git clone https://github.com/Dicklesworthstone/mcp_agent_mail.git - cd mcp_agent_mail - python3 -m venv .venv - source .venv/bin/activate - pip install -e . - ``` - -3. **Initialize beads database**: - ```bash - bd init --prefix bd - ``` - -4. **Create some test issues**: - ```bash - bd create "Implement login feature" -t feature -p 1 - bd create "Add database migrations" -t task -p 1 - bd create "Fix bug in auth flow" -t bug -p 0 - bd create "Write integration tests" -t task -p 2 - ``` - -## Usage Scenarios - -### Scenario 1: Single Agent (Git-Only Mode) - -No Agent Mail server required. The agent works in traditional git-sync mode: - -```bash -# Run agent without Agent Mail -./agent_with_mail.py --agent-name alice --project-id myproject -``` - -**What happens:** -- Agent finds ready work using `bd ready` -- Claims issues by updating status to `in_progress` -- Completes work and closes issues -- All coordination happens via git (2-5 second latency) - -### Scenario 2: Multi-Agent with Agent Mail - -Start the Agent Mail server and run multiple agents: - -**Terminal 1 - Start Agent Mail server:** -```bash -cd ~/mcp_agent_mail -source .venv/bin/activate -python -m mcp_agent_mail.cli serve-http -# Server runs on http://127.0.0.1:8765 -``` - -**Terminal 2 - First agent:** -```bash -./agent_with_mail.py \ - --agent-name alice \ - --project-id myproject \ - --agent-mail-url http://127.0.0.1:8765 \ - --max-iterations 5 -``` - -**Terminal 3 - Second agent:** -```bash -./agent_with_mail.py \ - --agent-name bob \ - --project-id myproject \ - --agent-mail-url http://127.0.0.1:8765 \ - --max-iterations 5 -``` - -**Terminal 4 - Monitor (optional):** -```bash -# Watch reservations in real-time -open http://127.0.0.1:8765/mail -``` - -**What happens:** -- Both agents query for ready work -- First agent to claim an issue gets exclusive reservation -- Second agent gets reservation conflict and tries different work -- Coordination happens in <100ms via Agent Mail -- No duplicate work, no git collisions - -### Scenario 3: Environment Variables - -Set Agent Mail configuration globally: - -```bash -# In your shell profile (~/.bashrc, ~/.zshrc) -export BEADS_AGENT_MAIL_URL=http://127.0.0.1:8765 -export BEADS_AGENT_NAME=my-agent -export BEADS_PROJECT_ID=my-project - -# Now all bd commands use Agent Mail automatically -./agent_with_mail.py --max-iterations 3 -``` - -### Scenario 4: Graceful Degradation - -Start an agent with Agent Mail enabled, then stop the server mid-run: - -**Terminal 1:** -```bash -# Start server -cd ~/mcp_agent_mail -source .venv/bin/activate -python -m mcp_agent_mail.cli serve-http -``` - -**Terminal 2:** -```bash -# Start agent -./agent_with_mail.py \ - --agent-name charlie \ - --agent-mail-url http://127.0.0.1:8765 \ - --max-iterations 10 -``` - -**Terminal 1 (after a few iterations):** -```bash -# Stop server (Ctrl+C) -^C -``` - -**What happens:** -- Agent starts in Agent Mail mode (<100ms latency) -- After server stops, agent automatically falls back to git-only mode -- No errors, no crashes - work continues normally -- Only difference is increased latency (2-5 seconds) - -## Example Output - -### With Agent Mail (Successful Reservation) - -``` -✨ Agent Mail enabled: alice @ http://127.0.0.1:8765 - -šŸš€ Agent 'alice' starting... - Project: myproject - Agent Mail: Enabled - -============================================================ -Iteration 1/5 -============================================================ - -šŸ“‹ Claiming issue: bd-42 - āœ… Successfully claimed bd-42 -šŸ¤– Working on: Implement login feature (bd-42) - Priority: 1, Type: feature -šŸ’” Creating discovered issue: Follow-up work for Implement login feature - āœ… Created bd-43 -šŸ”— Linked bd-43 ← discovered-from ← bd-42 -āœ… Completing issue: bd-42 - āœ… Issue bd-42 completed -``` - -### With Agent Mail (Reservation Conflict) - -``` -✨ Agent Mail enabled: bob @ http://127.0.0.1:8765 - -šŸš€ Agent 'bob' starting... - Project: myproject - Agent Mail: Enabled - -============================================================ -Iteration 1/5 -============================================================ - -šŸ“‹ Claiming issue: bd-42 -āš ļø Reservation conflict: Error: bd-42 already reserved by alice - āš ļø Issue bd-42 already claimed by another agent -šŸ“‹ Claiming issue: bd-44 - āœ… Successfully claimed bd-44 -šŸ¤– Working on: Write integration tests (bd-44) - Priority: 2, Type: task -``` - -### Git-Only Mode (No Agent Mail) - -``` -šŸ“ Git-only mode: charlie - -šŸš€ Agent 'charlie' starting... - Project: myproject - Agent Mail: Disabled (git-only mode) - -============================================================ -Iteration 1/5 -============================================================ - -šŸ“‹ Claiming issue: bd-42 - āœ… Successfully claimed bd-42 -šŸ¤– Working on: Implement login feature (bd-42) - Priority: 1, Type: feature -``` - -## Code Walkthrough - -### Key Methods - -**`__init__`**: Configure Agent Mail environment variables -```python -if self.agent_mail_url: - os.environ["BEADS_AGENT_MAIL_URL"] = self.agent_mail_url - os.environ["BEADS_AGENT_NAME"] = self.agent_name - os.environ["BEADS_PROJECT_ID"] = self.project_id -``` - -**`run_bd`**: Execute bd commands with error handling -```python -result = subprocess.run(["bd"] + list(args) + ["--json"], ...) -if "already reserved" in result.stderr: - return {"error": "reservation_conflict"} -``` - -**`claim_issue`**: Try to claim an issue, handle conflicts -```python -result = self.run_bd("update", issue_id, "--status", "in_progress") -if result["error"] == "reservation_conflict": - return False # Try different issue -``` - -**`complete_issue`**: Close issue and release reservation -```python -self.run_bd("close", issue_id, "--reason", reason) -# Agent Mail automatically releases reservation -``` - -### Error Handling - -The agent handles three types of failures: - -1. **Reservation conflicts** - Expected in multi-agent workflows: - ```python - if "reservation_conflict" in result: - print("āš ļø Issue already claimed by another agent") - return False # Try different work - ``` - -2. **Agent Mail unavailable** - Graceful degradation: - ```python - # bd automatically falls back to git-only mode - # No special handling needed! - ``` - -3. **Command failures** - General errors: - ```python - if returncode != 0: - print(f"āŒ Command failed: {stderr}") - return {"error": "command_failed"} - ``` - -## Integration Tips - -### Real LLM Agents - -To integrate with Claude, GPT-4, or other LLMs: - -1. **Replace `simulate_work()` with LLM calls**: - ```python - def simulate_work(self, issue: Dict[str, Any]) -> None: - # Call LLM with issue context - prompt = f"Implement: {issue['title']}\nDescription: {issue['description']}" - response = llm_client.generate(prompt) - - # Parse response for new issues/bugs - if "TODO" in response or "BUG" in response: - self.create_discovered_issue( - "Found during work", - issue["id"] - ) - ``` - -2. **Use issue IDs for conversation context**: - ```python - # Track conversation history per issue - conversation_history[issue["id"]].append({ - "role": "user", - "content": issue["description"] - }) - ``` - -3. **Export state after each iteration**: - ```python - # Ensure git state is synced - subprocess.run(["bd", "sync"]) - ``` - -### CI/CD Integration - -Run agents in GitHub Actions with Agent Mail: - -```yaml -jobs: - agent-workflow: - runs-on: ubuntu-latest - services: - agent-mail: - image: ghcr.io/dicklesworthstone/mcp_agent_mail:latest - ports: - - 8765:8765 - - strategy: - matrix: - agent: [alice, bob, charlie] - - steps: - - uses: actions/checkout@v4 - - - name: Run agent - env: - BEADS_AGENT_MAIL_URL: http://localhost:8765 - BEADS_AGENT_NAME: ${{ matrix.agent }} - BEADS_PROJECT_ID: ${{ github.repository }} - run: | - ./examples/python-agent/agent_with_mail.py --max-iterations 3 -``` - -### Monitoring & Debugging - -**View reservations in real-time:** -```bash -# Web UI -open http://127.0.0.1:8765/mail - -# API -curl http://127.0.0.1:8765/api/reservations | jq -``` - -**Check Agent Mail connectivity:** -```bash -# Health check -curl http://127.0.0.1:8765/health - -# Test reservation -curl -X POST http://127.0.0.1:8765/api/reservations \ - -H "Content-Type: application/json" \ - -d '{"resource_id": "bd-test", "agent_id": "test-agent", "project_id": "test"}' -``` - -**Debug agent behavior:** -```bash -# Increase verbosity -./agent_with_mail.py --agent-name debug-agent --max-iterations 1 - -# Check bd Agent Mail status -bd info --json | grep -A5 agent_mail -``` - -## Common Issues - -### "Agent Mail unavailable" warnings - -**Cause:** Server not running or wrong URL - -**Solution:** -```bash -# Verify server is running -curl http://127.0.0.1:8765/health - -# Check environment variables -echo $BEADS_AGENT_MAIL_URL -echo $BEADS_AGENT_NAME -echo $BEADS_PROJECT_ID -``` - -### Reservations not released after crash - -**Cause:** Agent crashed before calling `bd close` - -**Solution:** -```bash -# Manual release via API -curl -X DELETE http://127.0.0.1:8765/api/reservations/bd-42 - -# Or restart server (clears all ephemeral state) -pkill -f mcp_agent_mail -python -m mcp_agent_mail.cli serve-http -``` - -### Agents don't see each other's reservations - -**Cause:** Different `BEADS_PROJECT_ID` values - -**Solution:** -```bash -# Ensure all agents use SAME project ID -export BEADS_PROJECT_ID=my-project # All agents must use this! - -# Verify -./agent_with_mail.py --agent-name alice & -./agent_with_mail.py --agent-name bob & -# Both should coordinate on same namespace -``` - -## See Also - -- [../../docs/AGENT_MAIL.md](../../docs/AGENT_MAIL.md) - Complete Agent Mail integration guide -- [../../docs/adr/002-agent-mail-integration.md](../../docs/adr/002-agent-mail-integration.md) - Architecture decision record -- [agent.py](agent.py) - Original agent example (git-only mode) -- [Agent Mail Repository](https://github.com/Dicklesworthstone/mcp_agent_mail) - -## License - -Apache 2.0 (same as beads) diff --git a/examples/python-agent/README.md b/examples/python-agent/README.md index dd64dd12..692966d8 100644 --- a/examples/python-agent/README.md +++ b/examples/python-agent/README.md @@ -84,6 +84,5 @@ tree = agent.run_bd("dep", "tree", "bd-1") ## See Also -- [AGENT_MAIL_EXAMPLE.md](AGENT_MAIL_EXAMPLE.md) - Multi-agent coordination with Agent Mail - [../bash-agent/](../bash-agent/) - Bash version of this example - [../claude-desktop-mcp/](../claude-desktop-mcp/) - MCP server for Claude Desktop diff --git a/examples/python-agent/agent.py b/examples/python-agent/agent.py index 9f8ee81e..1d148873 100755 --- a/examples/python-agent/agent.py +++ b/examples/python-agent/agent.py @@ -8,34 +8,19 @@ This demonstrates how an agent can: 3. Discover new issues during work 4. Link discoveries back to parent tasks 5. Complete work and move on -6. Coordinate with other agents via Agent Mail (optional) """ import json import subprocess import sys -import os -from pathlib import Path from typing import Optional -# Add lib directory to path for beads_mail_adapter -lib_path = Path(__file__).parent.parent.parent / "lib" -sys.path.insert(0, str(lib_path)) - -from beads_mail_adapter import AgentMailAdapter - class BeadsAgent: """Simple agent that manages tasks using bd.""" def __init__(self): self.current_task = None - self.mail = AgentMailAdapter() - - if self.mail.enabled: - print(f"šŸ“¬ Agent Mail enabled (agent: {self.mail.agent_name})") - else: - print("šŸ“­ Agent Mail disabled (Beads-only mode)") def run_bd(self, *args) -> dict: """Run bd command and parse JSON output.""" @@ -47,20 +32,7 @@ class BeadsAgent: return {} def find_ready_work(self) -> Optional[dict]: - """Find the highest priority ready work. - - Integration Point 1: Check inbox before finding work. - """ - # Check inbox for notifications from other agents - messages = self.mail.check_inbox() - if messages: - print(f"šŸ“Ø Received {len(messages)} messages:") - for msg in messages: - event_type = msg.get("event_type", "unknown") - payload = msg.get("payload", {}) - from_agent = msg.get("from_agent", "unknown") - print(f" • {event_type} from {from_agent}: {payload}") - + """Find the highest priority ready work.""" ready = self.run_bd("ready", "--limit", "1") if isinstance(ready, list) and len(ready) > 0: @@ -68,26 +40,9 @@ class BeadsAgent: return None def claim_task(self, issue_id: str) -> dict: - """Claim a task by setting status to in_progress. - - Integration Point 2: Reserve issue before claiming. - Integration Point 3: Notify other agents of status change. - """ - # Reserve the issue to prevent conflicts with other agents - if not self.mail.reserve_issue(issue_id): - print(f"āš ļø Failed to reserve {issue_id} - already claimed by another agent") - return {} - + """Claim a task by setting status to in_progress.""" print(f"šŸ“‹ Claiming task: {issue_id}") result = self.run_bd("update", issue_id, "--status", "in_progress") - - # Notify other agents of status change - self.mail.notify("status_changed", { - "issue_id": issue_id, - "status": "in_progress", - "agent": self.mail.agent_name - }) - return result def create_issue(self, title: str, description: str = "", @@ -108,23 +63,9 @@ class BeadsAgent: ) def complete_task(self, issue_id: str, reason: str = "Completed"): - """Mark task as complete. - - Integration Point 4: Release reservation and notify completion. - """ + """Mark task as complete.""" print(f"āœ… Completing task: {issue_id} - {reason}") result = self.run_bd("close", issue_id, "--reason", reason) - - # Notify other agents of completion - self.mail.notify("issue_completed", { - "issue_id": issue_id, - "reason": reason, - "agent": self.mail.agent_name - }) - - # Release the reservation - self.mail.release_issue(issue_id) - return result def simulate_work(self, issue: dict) -> bool: diff --git a/examples/python-agent/agent_with_mail.py b/examples/python-agent/agent_with_mail.py deleted file mode 100755 index f192a0a2..00000000 --- a/examples/python-agent/agent_with_mail.py +++ /dev/null @@ -1,288 +0,0 @@ -#!/usr/bin/env python3 -""" -Beads Agent with Agent Mail Integration Example - -Demonstrates how to use bd with optional Agent Mail coordination for multi-agent workflows. -Shows collision handling, graceful degradation, and best practices. -""" - -import json -import os -import subprocess -import sys -import time -from typing import Optional, Dict, Any, List - - -class BeadsAgent: - """A simple agent that uses bd with optional Agent Mail coordination.""" - - def __init__(self, agent_name: str, project_id: str, agent_mail_url: Optional[str] = None): - """ - Initialize the agent. - - Args: - agent_name: Unique identifier for this agent (e.g., "assistant-alpha") - project_id: Project namespace for Agent Mail - agent_mail_url: Agent Mail server URL (optional, e.g., "http://127.0.0.1:8765") - """ - self.agent_name = agent_name - self.project_id = project_id - self.agent_mail_url = agent_mail_url - - # Configure environment for Agent Mail if URL provided - if self.agent_mail_url: - os.environ["BEADS_AGENT_MAIL_URL"] = self.agent_mail_url - os.environ["BEADS_AGENT_NAME"] = self.agent_name - os.environ["BEADS_PROJECT_ID"] = self.project_id - print(f"✨ Agent Mail enabled: {agent_name} @ {agent_mail_url}") - else: - print(f"šŸ“ Git-only mode: {agent_name}") - - def run_bd(self, *args) -> Dict[str, Any]: - """ - Run a bd command and return parsed JSON output. - - Args: - *args: Command arguments (e.g., "ready", "--json") - - Returns: - Parsed JSON output from bd - """ - cmd = ["bd"] + list(args) - if "--json" not in args: - cmd.append("--json") - - try: - result = subprocess.run( - cmd, - capture_output=True, - text=True, - check=False # Don't raise on non-zero exit - ) - - # Handle reservation conflicts gracefully - if result.returncode != 0: - # Check if it's a reservation conflict - if "already reserved" in result.stderr or "reservation conflict" in result.stderr: - print(f"āš ļø Reservation conflict: {result.stderr.strip()}") - return {"error": "reservation_conflict", "stderr": result.stderr} - else: - print(f"āŒ Command failed: {' '.join(cmd)}") - print(f" Error: {result.stderr}") - return {"error": "command_failed", "stderr": result.stderr} - - # Parse JSON output - if result.stdout.strip(): - return json.loads(result.stdout) - else: - return {} - - except json.JSONDecodeError as e: - print(f"āŒ Failed to parse JSON from bd: {e}") - print(f" Output: {result.stdout}") - return {"error": "json_parse_failed"} - except Exception as e: - print(f"āŒ Failed to run bd: {e}") - return {"error": str(e)} - - def get_ready_work(self) -> List[Dict[str, Any]]: - """Get list of unblocked issues ready to work on.""" - result = self.run_bd("ready", "--json") - - if "error" in result: - return [] - - # bd ready returns array of issues - if isinstance(result, list): - return result - else: - return [] - - def claim_issue(self, issue_id: str) -> bool: - """ - Claim an issue by setting status to in_progress. - - Returns: - True if successful, False if reservation conflict or error - """ - print(f"šŸ“‹ Claiming issue: {issue_id}") - result = self.run_bd("update", issue_id, "--status", "in_progress") - - if "error" in result: - if result["error"] == "reservation_conflict": - print(f" āš ļø Issue {issue_id} already claimed by another agent") - return False - else: - print(f" āŒ Failed to claim {issue_id}") - return False - - print(f" āœ… Successfully claimed {issue_id}") - return True - - def complete_issue(self, issue_id: str, reason: str = "Completed") -> bool: - """ - Complete an issue and release reservation. - - Returns: - True if successful, False otherwise - """ - print(f"āœ… Completing issue: {issue_id}") - result = self.run_bd("close", issue_id, "--reason", reason) - - if "error" in result: - print(f" āŒ Failed to complete {issue_id}") - return False - - print(f" āœ… Issue {issue_id} completed") - return True - - def create_discovered_issue( - self, - title: str, - parent_id: str, - priority: int = 2, - issue_type: str = "task" - ) -> Optional[str]: - """ - Create an issue discovered during work on another issue. - - Args: - title: Issue title - parent_id: ID of the issue this was discovered from - priority: Priority level (0-4) - issue_type: Issue type (bug, feature, task, etc.) - - Returns: - New issue ID if successful, None otherwise - """ - print(f"šŸ’” Creating discovered issue: {title}") - result = self.run_bd( - "create", - title, - "-t", issue_type, - "-p", str(priority), - "--deps", f"discovered-from:{parent_id}" - ) - - if "error" in result or "id" not in result: - print(f" āŒ Failed to create issue") - return None - - new_id = result["id"] - print(f" āœ… Created {new_id}") - return new_id - - def simulate_work(self, issue: Dict[str, Any]) -> None: - """Simulate working on an issue.""" - print(f"šŸ¤– Working on: {issue['title']} ({issue['id']})") - print(f" Priority: {issue['priority']}, Type: {issue['issue_type']}") - time.sleep(1) # Simulate work - - def run(self, max_iterations: int = 10) -> None: - """ - Main agent loop: find work, claim it, complete it. - - Args: - max_iterations: Maximum number of issues to process - """ - print(f"\nšŸš€ Agent '{self.agent_name}' starting...") - print(f" Project: {self.project_id}") - print(f" Agent Mail: {'Enabled' if self.agent_mail_url else 'Disabled (git-only mode)'}\n") - - for iteration in range(1, max_iterations + 1): - print("=" * 60) - print(f"Iteration {iteration}/{max_iterations}") - print("=" * 60) - - # Get ready work - ready_issues = self.get_ready_work() - - if not ready_issues: - print("šŸ“­ No ready work available. Stopping.") - break - - # Sort by priority (lower number = higher priority) - ready_issues.sort(key=lambda x: x.get("priority", 99)) - - # Try to claim the highest priority issue - claimed = False - for issue in ready_issues: - if self.claim_issue(issue["id"]): - claimed = True - - # Simulate work - self.simulate_work(issue) - - # Randomly discover new work (33% chance) - import random - if random.random() < 0.33: - discovered_title = f"Follow-up work for {issue['title']}" - new_id = self.create_discovered_issue( - discovered_title, - issue["id"], - priority=issue.get("priority", 2) - ) - if new_id: - print(f"šŸ”— Linked {new_id} ← discovered-from ← {issue['id']}") - - # Complete the issue - self.complete_issue(issue["id"], "Implemented successfully") - break - - if not claimed: - print("āš ļø All ready issues are reserved by other agents. Waiting...") - time.sleep(2) # Wait before retrying - - print() - - print(f"šŸ Agent '{self.agent_name}' finished after {iteration} iterations.") - - -def main(): - """Main entry point.""" - # Parse command line arguments - import argparse - parser = argparse.ArgumentParser( - description="Beads agent with optional Agent Mail coordination" - ) - parser.add_argument( - "--agent-name", - default=os.getenv("BEADS_AGENT_NAME", f"agent-{os.getpid()}"), - help="Unique agent identifier (default: agent-)" - ) - parser.add_argument( - "--project-id", - default=os.getenv("BEADS_PROJECT_ID", "default"), - help="Project namespace for Agent Mail" - ) - parser.add_argument( - "--agent-mail-url", - default=os.getenv("BEADS_AGENT_MAIL_URL"), - help="Agent Mail server URL (optional, e.g., http://127.0.0.1:8765)" - ) - parser.add_argument( - "--max-iterations", - type=int, - default=10, - help="Maximum number of issues to process (default: 10)" - ) - - args = parser.parse_args() - - # Create and run agent - agent = BeadsAgent( - agent_name=args.agent_name, - project_id=args.project_id, - agent_mail_url=args.agent_mail_url - ) - - try: - agent.run(max_iterations=args.max_iterations) - except KeyboardInterrupt: - print("\n\nāš ļø Agent interrupted by user. Exiting...") - sys.exit(0) - - -if __name__ == "__main__": - main() diff --git a/integrations/beads-mcp/src/beads_mcp/mail.py b/integrations/beads-mcp/src/beads_mcp/mail.py deleted file mode 100644 index 41e2f2d7..00000000 --- a/integrations/beads-mcp/src/beads_mcp/mail.py +++ /dev/null @@ -1,613 +0,0 @@ -"""Agent Mail integration for beads MCP server. - -Provides simple messaging functions that wrap Agent Mail HTTP API. -Requires BEADS_AGENT_MAIL_URL and BEADS_AGENT_NAME environment variables. -""" - -import logging -import os -from typing import Any, Optional -from urllib.parse import urljoin - -import requests - -logger = logging.getLogger(__name__) - -# Timeout for Agent Mail HTTP requests (seconds) -AGENT_MAIL_TIMEOUT = 5.0 -AGENT_MAIL_RETRIES = 2 - - -class MailError(Exception): - """Base exception for Agent Mail errors.""" - - def __init__(self, code: str, message: str, data: Optional[dict[str, Any]] = None): - self.code = code - self.message = message - self.data = data or {} - super().__init__(message) - - -def _get_config() -> tuple[str, str, Optional[str]]: - """Get Agent Mail configuration from environment. - - Returns: - (base_url, agent_name, token) - - Raises: - MailError: If required configuration is missing - """ - base_url = os.environ.get("BEADS_AGENT_MAIL_URL") - if not base_url: - raise MailError( - "NOT_CONFIGURED", - "Agent Mail not configured. Set BEADS_AGENT_MAIL_URL environment variable.\n" - "Example: export BEADS_AGENT_MAIL_URL=http://127.0.0.1:8765\n" - "See docs/AGENT_MAIL_QUICKSTART.md for setup instructions.", - ) - - agent_name = os.environ.get("BEADS_AGENT_NAME") - if not agent_name: - # Try to derive from user/repo - import getpass - - try: - user = getpass.getuser() - cwd = os.getcwd() - repo_name = os.path.basename(cwd) - agent_name = f"{user}-{repo_name}" - logger.warning( - f"BEADS_AGENT_NAME not set, using derived name: {agent_name}" - ) - except Exception: - raise MailError( - "NOT_CONFIGURED", - "Agent Mail not configured. Set BEADS_AGENT_NAME environment variable.\n" - "Example: export BEADS_AGENT_NAME=my-agent", - ) - - token = os.environ.get("BEADS_AGENT_MAIL_TOKEN") - - return base_url, agent_name, token - - -def _get_project_key() -> str: - """Get project key from environment or derive from Git/workspace. - - Returns: - Project key (absolute path to workspace root) - """ - # Check explicit project ID first - project_id = os.environ.get("BEADS_PROJECT_ID") - if project_id: - return project_id - - # Try to get from bd workspace detection - # Import here to avoid circular dependency - from .tools import _find_beads_db_in_tree - - workspace = _find_beads_db_in_tree() - if workspace: - return os.path.abspath(workspace) - - # Fallback to current directory - return os.path.abspath(os.getcwd()) - - -def _call_agent_mail( - method: str, - endpoint: str, - json_data: Optional[dict[str, Any]] = None, - params: Optional[dict[str, Any]] = None, -) -> Any: - """Make HTTP request to Agent Mail server with retries. - - Args: - method: HTTP method (GET, POST, DELETE, etc.) - endpoint: API endpoint path (e.g., "/api/messages") - json_data: Request body as JSON - params: URL query parameters - - Returns: - Response JSON - - Raises: - MailError: On request failure or server error - """ - base_url, _, token = _get_config() - url = urljoin(base_url, endpoint) - - headers = {} - if token: - headers["Authorization"] = f"Bearer {token}" - - # Use idempotency key for write operations to avoid duplicates on retry - if method in {"POST", "PUT"} and json_data: - import uuid - - headers["Idempotency-Key"] = str(uuid.uuid4()) - - last_error = None - for attempt in range(AGENT_MAIL_RETRIES + 1): - try: - response = requests.request( - method, - url, - json=json_data, - params=params, - headers=headers, - timeout=AGENT_MAIL_TIMEOUT, - ) - - # Success - if response.status_code < 400: - return response.json() if response.content else {} - - # Client error - don't retry - if 400 <= response.status_code < 500: - error_data = {} - try: - error_data = response.json() - except Exception: - error_data = {"detail": response.text} - - if response.status_code == 404: - raise MailError( - "NOT_FOUND", - f"Resource not found: {endpoint}", - error_data, - ) - elif response.status_code == 409: - raise MailError( - "CONFLICT", - error_data.get("detail", "Conflict"), - error_data, - ) - else: - raise MailError( - "INVALID_ARGUMENT", - error_data.get("detail", f"HTTP {response.status_code}"), - error_data, - ) - - # Server error - retry - last_error = MailError( - "UNAVAILABLE", - f"Agent Mail server error: HTTP {response.status_code}", - {"status": response.status_code, "attempt": attempt + 1}, - ) - - except requests.exceptions.Timeout: - last_error = MailError( - "TIMEOUT", - f"Agent Mail request timeout after {AGENT_MAIL_TIMEOUT}s", - {"attempt": attempt + 1}, - ) - except requests.exceptions.ConnectionError as e: - last_error = MailError( - "UNAVAILABLE", - f"Cannot connect to Agent Mail server at {base_url}", - {"error": str(e), "attempt": attempt + 1}, - ) - except MailError: - raise # Re-raise our own errors - except Exception as e: - last_error = MailError( - "INTERNAL_ERROR", - f"Unexpected error calling Agent Mail: {e}", - {"error": str(e), "attempt": attempt + 1}, - ) - - # Exponential backoff between retries - if attempt < AGENT_MAIL_RETRIES: - import time - - time.sleep(0.5 * (2**attempt)) - - # All retries exhausted - if last_error: - raise last_error - raise MailError("INTERNAL_ERROR", "Request failed with no error details") - - -def mail_send( - to: list[str], - subject: str, - body: str, - urgent: bool = False, - cc: Optional[list[str]] = None, - project_key: Optional[str] = None, - sender_name: Optional[str] = None, -) -> dict[str, Any]: - """Send a message to other agents. - - Args: - to: List of recipient agent names - subject: Message subject - body: Message body (Markdown) - urgent: Mark as urgent (default: False) - cc: Optional CC recipients - project_key: Override project identifier (default: auto-detect) - sender_name: Override sender name (default: BEADS_AGENT_NAME) - - Returns: - { - "message_id": int, - "thread_id": str, - "sent_to": int # number of recipients - } - - Raises: - MailError: On configuration or delivery error - """ - _, auto_agent_name, _ = _get_config() - auto_project_key = _get_project_key() - - sender = sender_name or auto_agent_name - project = project_key or auto_project_key - - importance = "urgent" if urgent else "normal" - - # Call Agent Mail send_message tool via HTTP - # Note: Agent Mail MCP tools use POST to /mcp/call endpoint - result = _call_agent_mail( - "POST", - "/mcp/call", - json_data={ - "method": "tools/call", - "params": { - "name": "send_message", - "arguments": { - "project_key": project, - "sender_name": sender, - "to": to, - "subject": subject, - "body_md": body, - "cc": cc or [], - "importance": importance, - }, - }, - }, - ) - - # Extract message details from result - # Agent Mail returns: {"deliveries": [...], "count": N} - deliveries = result.get("deliveries", []) - if not deliveries: - raise MailError("INTERNAL_ERROR", "No deliveries returned from Agent Mail") - - # Get message ID from first delivery - first_delivery = deliveries[0] - payload = first_delivery.get("payload", {}) - message_id = payload.get("id") - thread_id = payload.get("thread_id") - - return { - "message_id": message_id, - "thread_id": thread_id, - "sent_to": len(deliveries), - } - - -def mail_inbox( - limit: int = 20, - urgent_only: bool = False, - unread_only: bool = False, - cursor: Optional[str] = None, - agent_name: Optional[str] = None, - project_key: Optional[str] = None, -) -> dict[str, Any]: - """Get messages from inbox. - - Args: - limit: Maximum messages to return (default: 20) - urgent_only: Only return urgent messages - unread_only: Only return unread messages - cursor: Pagination cursor (for next page) - agent_name: Override agent name (default: BEADS_AGENT_NAME) - project_key: Override project (default: auto-detect) - - Returns: - { - "messages": [ - { - "id": int, - "thread_id": str, - "from": str, - "subject": str, - "created_ts": str, # ISO-8601 - "unread": bool, - "ack_required": bool, - "urgent": bool, - "preview": str # first 100 chars - }, - ... - ], - "next_cursor": str | None - } - """ - _, auto_agent_name, _ = _get_config() - auto_project_key = _get_project_key() - - agent = agent_name or auto_agent_name - project = project_key or auto_project_key - - # Call fetch_inbox via MCP - result = _call_agent_mail( - "POST", - "/mcp/call", - json_data={ - "method": "tools/call", - "params": { - "name": "fetch_inbox", - "arguments": { - "project_key": project, - "agent_name": agent, - "limit": limit, - "urgent_only": urgent_only, - "include_bodies": False, # Get preview only - }, - }, - }, - ) - - # Agent Mail returns list of messages directly - messages: list[dict[str, Any]] = result if isinstance(result, list) else [] - - # Transform to our format and filter unread if requested - formatted_messages = [] - for msg in messages: - # Skip read messages if unread_only - if unread_only and msg.get("read_ts"): - continue - - formatted_messages.append( - { - "id": msg.get("id"), - "thread_id": msg.get("thread_id"), - "from": msg.get("from"), - "subject": msg.get("subject"), - "created_ts": msg.get("created_ts"), - "unread": not bool(msg.get("read_ts")), - "ack_required": msg.get("ack_required", False), - "urgent": msg.get("importance") in {"high", "urgent"}, - "preview": msg.get("body_md", "")[:100] if msg.get("body_md") else "", - } - ) - - # Simple cursor pagination (use last message ID) - next_cursor = None - if formatted_messages and len(formatted_messages) >= limit: - next_cursor = str(formatted_messages[-1]["id"]) - - return {"messages": formatted_messages, "next_cursor": next_cursor} - - -def mail_read( - message_id: int, - mark_read: bool = True, - agent_name: Optional[str] = None, - project_key: Optional[str] = None, -) -> dict[str, Any]: - """Read full message with body. - - Args: - message_id: Message ID to read - mark_read: Mark message as read (default: True) - agent_name: Override agent name (default: BEADS_AGENT_NAME) - project_key: Override project (default: auto-detect) - - Returns: - { - "id": int, - "thread_id": str, - "from": str, - "to": list[str], - "subject": str, - "body": str, # Full Markdown body - "created_ts": str, - "ack_required": bool, - "ack_status": bool, - "read_ts": str | None, - "urgent": bool - } - """ - _, auto_agent_name, _ = _get_config() - auto_project_key = _get_project_key() - - agent = agent_name or auto_agent_name - project = project_key or auto_project_key - - # Get message via resource - result = _call_agent_mail( - "GET", f"/mcp/resources/resource://message/{message_id}" - ) - - # Mark as read if requested - if mark_read: - try: - _call_agent_mail( - "POST", - "/mcp/call", - json_data={ - "method": "tools/call", - "params": { - "name": "mark_message_read", - "arguments": { - "project_key": project, - "agent_name": agent, - "message_id": message_id, - }, - }, - }, - ) - except MailError as e: - # Don't fail read if mark fails - logger.warning(f"Failed to mark message {message_id} as read: {e}") - - # Extract message from result - # Resource returns: {"contents": [{...}]} - contents = result.get("contents", []) - if not contents: - raise MailError("NOT_FOUND", f"Message {message_id} not found") - - msg = contents[0] - - return { - "id": msg.get("id"), - "thread_id": msg.get("thread_id"), - "from": msg.get("from"), - "to": msg.get("to", []), - "subject": msg.get("subject"), - "body": msg.get("body_md", ""), - "created_ts": msg.get("created_ts"), - "ack_required": msg.get("ack_required", False), - "ack_status": bool(msg.get("ack_ts")), - "read_ts": msg.get("read_ts"), - "urgent": msg.get("importance") in {"high", "urgent"}, - } - - -def mail_reply( - message_id: int, - body: str, - subject: Optional[str] = None, - agent_name: Optional[str] = None, - project_key: Optional[str] = None, -) -> dict[str, Any]: - """Reply to a message (preserves thread). - - Args: - message_id: Message ID to reply to - body: Reply body (Markdown) - subject: Override subject (default: "Re: ") - agent_name: Override sender name (default: BEADS_AGENT_NAME) - project_key: Override project (default: auto-detect) - - Returns: - { - "message_id": int, - "thread_id": str - } - """ - _, auto_agent_name, _ = _get_config() - auto_project_key = _get_project_key() - - sender = agent_name or auto_agent_name - project = project_key or auto_project_key - - # Call reply_message via MCP - args = { - "project_key": project, - "message_id": message_id, - "sender_name": sender, - "body_md": body, - } - - if subject: - args["subject_prefix"] = subject - - result = _call_agent_mail( - "POST", - "/mcp/call", - json_data={ - "method": "tools/call", - "params": {"name": "reply_message", "arguments": args}, - }, - ) - - # Extract reply details - reply = result.get("reply", {}) - return { - "message_id": reply.get("id"), - "thread_id": reply.get("thread_id"), - } - - -def mail_ack( - message_id: int, - agent_name: Optional[str] = None, - project_key: Optional[str] = None, -) -> dict[str, bool]: - """Acknowledge a message (for ack_required messages). - - Args: - message_id: Message ID to acknowledge - agent_name: Override agent name (default: BEADS_AGENT_NAME) - project_key: Override project (default: auto-detect) - - Returns: - {"acknowledged": True} - """ - _, auto_agent_name, _ = _get_config() - auto_project_key = _get_project_key() - - agent = agent_name or auto_agent_name - project = project_key or auto_project_key - - # Call acknowledge_message via MCP - _call_agent_mail( - "POST", - "/mcp/call", - json_data={ - "method": "tools/call", - "params": { - "name": "acknowledge_message", - "arguments": { - "project_key": project, - "agent_name": agent, - "message_id": message_id, - }, - }, - }, - ) - - return {"acknowledged": True} - - -def mail_delete( - message_id: int, - agent_name: Optional[str] = None, - project_key: Optional[str] = None, -) -> dict[str, bool]: - """Delete (archive) a message from inbox. - - Note: Agent Mail may archive rather than truly delete. - - Args: - message_id: Message ID to delete - agent_name: Override agent name (default: BEADS_AGENT_NAME) - project_key: Override project (default: auto-detect) - - Returns: - {"deleted": True} or {"archived": True} - """ - _, auto_agent_name, _ = _get_config() - auto_project_key = _get_project_key() - - agent = agent_name or auto_agent_name - project = project_key or auto_project_key - - # Agent Mail doesn't have explicit delete in MCP API - # Best we can do is mark as read and acknowledged - # (This prevents it from showing in urgent/unread views) - try: - _call_agent_mail( - "POST", - "/mcp/call", - json_data={ - "method": "tools/call", - "params": { - "name": "mark_message_read", - "arguments": { - "project_key": project, - "agent_name": agent, - "message_id": message_id, - }, - }, - }, - ) - return {"archived": True} - except MailError: - # Soft failure - message may not exist or already read - return {"archived": True} diff --git a/integrations/beads-mcp/src/beads_mcp/mail_tools.py b/integrations/beads-mcp/src/beads_mcp/mail_tools.py deleted file mode 100644 index a8af868b..00000000 --- a/integrations/beads-mcp/src/beads_mcp/mail_tools.py +++ /dev/null @@ -1,211 +0,0 @@ -"""MCP tools for Agent Mail messaging.""" - -import logging -from typing import Annotated, Any - -from .mail import ( - MailError, - mail_ack, - mail_delete, - mail_inbox, - mail_read, - mail_reply, - mail_send, -) -from .models import ( - MailAckParams, - MailDeleteParams, - MailInboxParams, - MailReadParams, - MailReplyParams, - MailSendParams, -) - -logger = logging.getLogger(__name__) - - -def beads_mail_send(params: MailSendParams) -> dict[str, Any]: - """Send a message to other agents via Agent Mail. - - Requires BEADS_AGENT_MAIL_URL and BEADS_AGENT_NAME environment variables. - Auto-detects project from workspace root. - - Example: - mail_send(to=["alice"], subject="Review PR", body="Can you review PR #42?") - - Args: - params: Message parameters (to, subject, body, etc.) - - Returns: - {message_id: int, thread_id: str, sent_to: int} - - Raises: - MailError: On configuration or delivery error - """ - try: - return mail_send( - to=params.to, - subject=params.subject, - body=params.body, - urgent=params.urgent, - cc=params.cc, - project_key=params.project_key, - sender_name=params.sender_name, - ) - except MailError as e: - logger.error(f"mail_send failed: {e.message}") - return {"error": e.code, "message": e.message, "data": e.data} - - -def beads_mail_inbox( - params: Annotated[MailInboxParams, "Parameters"] = MailInboxParams(), -) -> dict[str, Any]: - """Get messages from Agent Mail inbox. - - Requires BEADS_AGENT_MAIL_URL and BEADS_AGENT_NAME environment variables. - - Example: - mail_inbox(limit=10, unread_only=True) - - Args: - params: Inbox filter parameters - - Returns: - { - messages: [{id, thread_id, from, subject, created_ts, unread, ack_required, urgent, preview}, ...], - next_cursor: str | None - } - - Raises: - MailError: On configuration or fetch error - """ - try: - return mail_inbox( - limit=params.limit, - urgent_only=params.urgent_only, - unread_only=params.unread_only, - cursor=params.cursor, - agent_name=params.agent_name, - project_key=params.project_key, - ) - except MailError as e: - logger.error(f"mail_inbox failed: {e.message}") - return {"error": e.code, "message": e.message, "data": e.data} - - -def beads_mail_read(params: MailReadParams) -> dict[str, Any]: - """Read full message with body from Agent Mail. - - By default, marks the message as read. Set mark_read=False to preview without marking. - - Example: - mail_read(message_id=123) - - Args: - params: Read parameters (message_id, mark_read) - - Returns: - { - id, thread_id, from, to, subject, body, - created_ts, ack_required, ack_status, read_ts, urgent - } - - Raises: - MailError: On configuration or read error - """ - try: - return mail_read( - message_id=params.message_id, - mark_read=params.mark_read, - agent_name=params.agent_name, - project_key=params.project_key, - ) - except MailError as e: - logger.error(f"mail_read failed: {e.message}") - return {"error": e.code, "message": e.message, "data": e.data} - - -def beads_mail_reply(params: MailReplyParams) -> dict[str, Any]: - """Reply to a message (preserves thread). - - Automatically inherits thread_id from the original message. - - Example: - mail_reply(message_id=123, body="Thanks, will review today!") - - Args: - params: Reply parameters (message_id, body, subject) - - Returns: - {message_id: int, thread_id: str} - - Raises: - MailError: On configuration or reply error - """ - try: - return mail_reply( - message_id=params.message_id, - body=params.body, - subject=params.subject, - agent_name=params.agent_name, - project_key=params.project_key, - ) - except MailError as e: - logger.error(f"mail_reply failed: {e.message}") - return {"error": e.code, "message": e.message, "data": e.data} - - -def beads_mail_ack(params: MailAckParams) -> dict[str, Any]: - """Acknowledge a message (for ack_required messages). - - Safe to call even if message doesn't require acknowledgement. - - Example: - mail_ack(message_id=123) - - Args: - params: Acknowledgement parameters (message_id) - - Returns: - {acknowledged: True} - - Raises: - MailError: On configuration or ack error - """ - try: - return mail_ack( - message_id=params.message_id, - agent_name=params.agent_name, - project_key=params.project_key, - ) - except MailError as e: - logger.error(f"mail_ack failed: {e.message}") - return {"error": e.code, "acknowledged": False, "message": e.message} - - -def beads_mail_delete(params: MailDeleteParams) -> dict[str, Any]: - """Delete (archive) a message from Agent Mail inbox. - - Note: Agent Mail archives messages rather than permanently deleting them. - - Example: - mail_delete(message_id=123) - - Args: - params: Delete parameters (message_id) - - Returns: - {deleted: True} or {archived: True} - - Raises: - MailError: On configuration or delete error - """ - try: - return mail_delete( - message_id=params.message_id, - agent_name=params.agent_name, - project_key=params.project_key, - ) - except MailError as e: - logger.error(f"mail_delete failed: {e.message}") - return {"error": e.code, "deleted": False, "message": e.message} diff --git a/integrations/beads-mcp/src/beads_mcp/models.py b/integrations/beads-mcp/src/beads_mcp/models.py index af0b26b1..de9108c2 100644 --- a/integrations/beads-mcp/src/beads_mcp/models.py +++ b/integrations/beads-mcp/src/beads_mcp/models.py @@ -217,63 +217,3 @@ class InitResult(BaseModel): database: str prefix: str message: str - - -# Agent Mail Models - -class MailSendParams(BaseModel): - """Parameters for sending mail.""" - - to: list[str] - subject: str - body: str - urgent: bool = False - cc: list[str] | None = None - project_key: str | None = None - sender_name: str | None = None - - -class MailInboxParams(BaseModel): - """Parameters for checking inbox.""" - - limit: int = 20 - urgent_only: bool = False - unread_only: bool = False - cursor: str | None = None - agent_name: str | None = None - project_key: str | None = None - - -class MailReadParams(BaseModel): - """Parameters for reading mail.""" - - message_id: int - mark_read: bool = True - agent_name: str | None = None - project_key: str | None = None - - -class MailReplyParams(BaseModel): - """Parameters for replying to mail.""" - - message_id: int - body: str - subject: str | None = None - agent_name: str | None = None - project_key: str | None = None - - -class MailAckParams(BaseModel): - """Parameters for acknowledging mail.""" - - message_id: int - agent_name: str | None = None - project_key: str | None = None - - -class MailDeleteParams(BaseModel): - """Parameters for deleting mail.""" - - message_id: int - agent_name: str | None = None - project_key: str | None = None diff --git a/integrations/beads-mcp/tests/test_mail.py b/integrations/beads-mcp/tests/test_mail.py deleted file mode 100644 index b598b6ce..00000000 --- a/integrations/beads-mcp/tests/test_mail.py +++ /dev/null @@ -1,426 +0,0 @@ -"""Tests for Agent Mail messaging integration.""" - -import os -from unittest.mock import Mock, patch - -import pytest - -from beads_mcp.mail import ( - MailError, - mail_ack, - mail_delete, - mail_inbox, - mail_read, - mail_reply, - mail_send, -) -from beads_mcp.models import ( - MailAckParams, - MailDeleteParams, - MailInboxParams, - MailReadParams, - MailReplyParams, - MailSendParams, -) - - -@pytest.fixture -def mock_agent_mail_env(tmp_path): - """Set up Agent Mail environment variables.""" - old_env = os.environ.copy() - - os.environ["BEADS_AGENT_MAIL_URL"] = "http://127.0.0.1:8765" - os.environ["BEADS_AGENT_NAME"] = "test-agent" - os.environ["BEADS_PROJECT_ID"] = str(tmp_path) - - yield - - # Restore environment - os.environ.clear() - os.environ.update(old_env) - - -@pytest.fixture -def mock_requests(): - """Mock requests library for HTTP calls.""" - with patch("beads_mcp.mail.requests.request") as mock_req: - yield mock_req - - -class TestMailConfiguration: - """Test configuration and error handling.""" - - def test_missing_url_raises_error(self): - """Test that missing BEADS_AGENT_MAIL_URL raises NOT_CONFIGURED.""" - old_url = os.environ.pop("BEADS_AGENT_MAIL_URL", None) - - try: - with pytest.raises(MailError) as exc_info: - mail_send(to=["alice"], subject="Test", body="Test") - - assert exc_info.value.code == "NOT_CONFIGURED" - assert "BEADS_AGENT_MAIL_URL" in exc_info.value.message - finally: - if old_url: - os.environ["BEADS_AGENT_MAIL_URL"] = old_url - - def test_missing_agent_name_derives_default(self, mock_agent_mail_env, mock_requests, tmp_path): - """Test that missing BEADS_AGENT_NAME derives from user/repo.""" - del os.environ["BEADS_AGENT_NAME"] - os.environ["BEADS_PROJECT_ID"] = str(tmp_path) - - mock_requests.return_value.status_code = 200 - mock_requests.return_value.json.return_value = { - "deliveries": [{ - "payload": { - "id": 123, - "thread_id": "thread-1", - } - }] - } - mock_requests.return_value.content = b'{"deliveries": []}' - - # Should not raise - derives agent name - result = mail_send(to=["alice"], subject="Test", body="Test") - assert result["message_id"] == 123 - - -class TestMailSend: - """Test mail_send function.""" - - def test_send_basic_message(self, mock_agent_mail_env, mock_requests): - """Test sending a basic message.""" - mock_requests.return_value.status_code = 200 - mock_requests.return_value.json.return_value = { - "deliveries": [{ - "payload": { - "id": 123, - "thread_id": "thread-abc", - } - }] - } - mock_requests.return_value.content = b'{"deliveries": []}' - - result = mail_send( - to=["alice", "bob"], - subject="Test Message", - body="Hello world!", - ) - - assert result["message_id"] == 123 - assert result["thread_id"] == "thread-abc" - assert result["sent_to"] == 1 - - # Verify HTTP request - mock_requests.assert_called_once() - args, call_kwargs = mock_requests.call_args - assert args[0] == "POST" - assert call_kwargs["json"]["params"]["name"] == "send_message" - assert call_kwargs["json"]["params"]["arguments"]["to"] == ["alice", "bob"] - assert call_kwargs["json"]["params"]["arguments"]["subject"] == "Test Message" - - def test_send_urgent_message(self, mock_agent_mail_env, mock_requests): - """Test sending urgent message.""" - mock_requests.return_value.status_code = 200 - mock_requests.return_value.json.return_value = { - "deliveries": [{ - "payload": {"id": 456, "thread_id": "thread-xyz"} - }] - } - mock_requests.return_value.content = b'{"deliveries": []}' - - result = mail_send( - to=["alice"], - subject="URGENT", - body="Need review now!", - urgent=True, - ) - - call_kwargs = mock_requests.call_args.kwargs - assert call_kwargs["json"]["params"]["arguments"]["importance"] == "urgent" - - def test_send_with_cc(self, mock_agent_mail_env, mock_requests): - """Test sending message with CC recipients.""" - mock_requests.return_value.status_code = 200 - mock_requests.return_value.json.return_value = { - "deliveries": [{ - "payload": {"id": 789, "thread_id": "thread-123"} - }] - } - mock_requests.return_value.content = b'{"deliveries": []}' - - result = mail_send( - to=["alice"], - subject="FYI", - body="For your info", - cc=["bob", "charlie"], - ) - - call_kwargs = mock_requests.call_args.kwargs - assert call_kwargs["json"]["params"]["arguments"]["cc"] == ["bob", "charlie"] - - def test_send_connection_error(self, mock_agent_mail_env, mock_requests): - """Test handling connection errors.""" - import requests.exceptions - mock_requests.side_effect = requests.exceptions.ConnectionError("Connection refused") - - with pytest.raises(MailError) as exc_info: - mail_send(to=["alice"], subject="Test", body="Test") - - assert exc_info.value.code == "UNAVAILABLE" - assert "Cannot connect" in exc_info.value.message - - -class TestMailInbox: - """Test mail_inbox function.""" - - def test_fetch_inbox_default(self, mock_agent_mail_env, mock_requests): - """Test fetching inbox with default parameters.""" - mock_requests.return_value.status_code = 200 - mock_requests.return_value.json.return_value = [ - { - "id": 1, - "thread_id": "thread-1", - "from": "alice", - "subject": "Hello", - "created_ts": "2025-01-01T00:00:00Z", - "read_ts": None, - "ack_required": False, - "importance": "normal", - "body_md": "This is a test message", - }, - { - "id": 2, - "thread_id": "thread-2", - "from": "bob", - "subject": "Urgent!", - "created_ts": "2025-01-02T00:00:00Z", - "read_ts": "2025-01-02T01:00:00Z", - "ack_required": True, - "importance": "urgent", - "body_md": "Please review ASAP", - }, - ] - mock_requests.return_value.content = b'[]' - - result = mail_inbox() - - assert len(result["messages"]) == 2 - assert result["messages"][0]["id"] == 1 - assert result["messages"][0]["unread"] is True - assert result["messages"][0]["urgent"] is False - assert result["messages"][1]["id"] == 2 - assert result["messages"][1]["unread"] is False - assert result["messages"][1]["urgent"] is True - - def test_fetch_inbox_unread_only(self, mock_agent_mail_env, mock_requests): - """Test fetching only unread messages.""" - mock_requests.return_value.status_code = 200 - mock_requests.return_value.json.return_value = [ - {"id": 1, "thread_id": "t1", "from": "alice", "subject": "Test", "created_ts": "2025-01-01T00:00:00Z", "read_ts": None, "importance": "normal"}, - {"id": 2, "thread_id": "t2", "from": "bob", "subject": "Test2", "created_ts": "2025-01-01T00:00:00Z", "read_ts": "2025-01-01T01:00:00Z", "importance": "normal"}, - ] - mock_requests.return_value.content = b'[]' - - result = mail_inbox(unread_only=True) - - # Should filter out message 2 (read) - assert len(result["messages"]) == 1 - assert result["messages"][0]["id"] == 1 - - def test_fetch_inbox_pagination(self, mock_agent_mail_env, mock_requests): - """Test inbox pagination with next_cursor.""" - mock_requests.return_value.status_code = 200 - # Simulate full page (limit reached) - mock_requests.return_value.json.return_value = [ - {"id": i, "thread_id": f"t{i}", "from": "alice", "subject": f"Msg {i}", "created_ts": "2025-01-01T00:00:00Z", "importance": "normal"} - for i in range(20) - ] - mock_requests.return_value.content = b'[]' - - result = mail_inbox(limit=20) - - # Should return next_cursor when limit reached - assert result["next_cursor"] == "19" # Last message ID - - -class TestMailRead: - """Test mail_read function.""" - - def test_read_message_marks_read(self, mock_agent_mail_env, mock_requests): - """Test reading message marks it as read by default.""" - # Mock resource fetch - mock_requests.return_value.status_code = 200 - mock_requests.return_value.json.return_value = { - "contents": [{ - "id": 123, - "thread_id": "thread-1", - "from": "alice", - "to": ["test-agent"], - "subject": "Test", - "body_md": "Hello world!", - "created_ts": "2025-01-01T00:00:00Z", - "ack_required": False, - "importance": "normal", - "read_ts": None, - }] - } - mock_requests.return_value.content = b'{}' - - result = mail_read(message_id=123) - - assert result["id"] == 123 - assert result["body"] == "Hello world!" - assert result["urgent"] is False - - # Should have called both GET resource and POST mark_read - assert mock_requests.call_count == 2 - - def test_read_message_no_mark(self, mock_agent_mail_env, mock_requests): - """Test reading without marking as read.""" - mock_requests.return_value.status_code = 200 - mock_requests.return_value.json.return_value = { - "contents": [{ - "id": 123, - "thread_id": "thread-1", - "from": "alice", - "to": ["test-agent"], - "subject": "Test", - "body_md": "Preview", - "created_ts": "2025-01-01T00:00:00Z", - "importance": "normal", - }] - } - mock_requests.return_value.content = b'{}' - - result = mail_read(message_id=123, mark_read=False) - - # Should only call GET resource, not mark_read - assert mock_requests.call_count == 1 - - -class TestMailReply: - """Test mail_reply function.""" - - def test_reply_to_message(self, mock_agent_mail_env, mock_requests): - """Test replying to a message.""" - mock_requests.return_value.status_code = 200 - mock_requests.return_value.json.return_value = { - "reply": { - "id": 456, - "thread_id": "thread-1", - } - } - mock_requests.return_value.content = b'{}' - - result = mail_reply( - message_id=123, - body="Thanks for the message!", - ) - - assert result["message_id"] == 456 - assert result["thread_id"] == "thread-1" - - call_kwargs = mock_requests.call_args.kwargs - assert call_kwargs["json"]["params"]["name"] == "reply_message" - assert call_kwargs["json"]["params"]["arguments"]["message_id"] == 123 - - -class TestMailAck: - """Test mail_ack function.""" - - def test_acknowledge_message(self, mock_agent_mail_env, mock_requests): - """Test acknowledging a message.""" - mock_requests.return_value.status_code = 200 - mock_requests.return_value.json.return_value = {} - mock_requests.return_value.content = b'{}' - - result = mail_ack(message_id=123) - - assert result["acknowledged"] is True - - call_kwargs = mock_requests.call_args.kwargs - assert call_kwargs["json"]["params"]["name"] == "acknowledge_message" - - -class TestMailDelete: - """Test mail_delete function.""" - - def test_delete_message(self, mock_agent_mail_env, mock_requests): - """Test deleting/archiving a message.""" - mock_requests.return_value.status_code = 200 - mock_requests.return_value.json.return_value = {} - mock_requests.return_value.content = b'{}' - - result = mail_delete(message_id=123) - - assert result["archived"] is True - - -class TestMailRetries: - """Test retry logic and error handling.""" - - def test_retries_on_server_error(self, mock_agent_mail_env, mock_requests): - """Test that 500 errors trigger retries.""" - mock_requests.return_value.status_code = 500 - mock_requests.return_value.content = b'Internal Server Error' - - with pytest.raises(MailError) as exc_info: - mail_send(to=["alice"], subject="Test", body="Test") - - assert exc_info.value.code == "UNAVAILABLE" - # Should retry 3 times total (initial + 2 retries) - assert mock_requests.call_count == 3 - - def test_no_retry_on_client_error(self, mock_agent_mail_env, mock_requests): - """Test that 404 errors don't trigger retries.""" - mock_requests.return_value.status_code = 404 - mock_requests.return_value.json.return_value = {"detail": "Not found"} - mock_requests.return_value.content = b'{"detail": "Not found"}' - - with pytest.raises(MailError) as exc_info: - mail_read(message_id=999) - - assert exc_info.value.code == "NOT_FOUND" - # Should not retry on 404 - assert mock_requests.call_count == 1 - - -class TestMailToolWrappers: - """Test MCP tool wrappers.""" - - def test_mail_send_params(self, mock_agent_mail_env, mock_requests): - """Test MailSendParams validation.""" - from beads_mcp.mail_tools import beads_mail_send - - mock_requests.return_value.status_code = 200 - mock_requests.return_value.json.return_value = { - "deliveries": [{ - "payload": {"id": 123, "thread_id": "t1"} - }] - } - mock_requests.return_value.content = b'{}' - - params = MailSendParams( - to=["alice"], - subject="Test", - body="Hello", - urgent=True, - ) - - result = beads_mail_send(params) - assert result["message_id"] == 123 - - def test_mail_inbox_default_params(self, mock_agent_mail_env, mock_requests): - """Test MailInboxParams with defaults.""" - from beads_mcp.mail_tools import beads_mail_inbox - - mock_requests.return_value.status_code = 200 - mock_requests.return_value.json.return_value = [] - mock_requests.return_value.content = b'[]' - - params = MailInboxParams() # All defaults - result = beads_mail_inbox(params) - - assert result["messages"] == [] - assert result["next_cursor"] is None diff --git a/lib/README.md b/lib/README.md deleted file mode 100644 index e454ff0d..00000000 --- a/lib/README.md +++ /dev/null @@ -1,173 +0,0 @@ -# Beads Agent Mail Adapter - -Lightweight Python library for integrating [MCP Agent Mail](https://github.com/Dicklesworthstone/mcp_agent_mail) with Beads issue tracking. - -## Features - -- **Collision Prevention**: Reserve issues to prevent duplicate work across agents -- **Real-Time Coordination**: <100ms latency vs 2-5s with git-only sync -- **Graceful Degradation**: Automatically falls back to git-only mode when server unavailable -- **Zero Configuration**: Works without Agent Mail (optional enhancement) - -## Installation - -No installation required - just copy `beads_mail_adapter.py` to your project: - -```bash -cp lib/beads_mail_adapter.py /path/to/your/agent/ -``` - -## Quick Start - -```python -from beads_mail_adapter import AgentMailAdapter - -# Initialize adapter (automatically detects server availability) -adapter = AgentMailAdapter() - -if adapter.enabled: - print("āœ… Agent Mail coordination enabled") -else: - print("āš ļø Agent Mail unavailable, using git-only mode") - -# Reserve issue before claiming -if adapter.reserve_issue("bd-123"): - # Claim issue in Beads - subprocess.run(["bd", "update", "bd-123", "--status", "in_progress"]) - - # Do work... - - # Notify other agents - adapter.notify("status_changed", {"issue_id": "bd-123", "status": "completed"}) - - # Release reservation - adapter.release_issue("bd-123") -else: - print("āŒ Issue bd-123 already reserved by another agent") -``` - -## Configuration - -Configure via environment variables: - -```bash -# Agent Mail server URL (default: http://127.0.0.1:8765) -export AGENT_MAIL_URL=http://localhost:8765 - -# Authentication token (optional) -export AGENT_MAIL_TOKEN=your-bearer-token - -# Agent identifier (default: hostname) -export BEADS_AGENT_NAME=assistant-alpha - -# Request timeout in seconds (default: 5) -export AGENT_MAIL_TIMEOUT=5 -``` - -Or pass directly to constructor: - -```python -adapter = AgentMailAdapter( - url="http://localhost:8765", - token="your-token", - agent_name="assistant-alpha", - timeout=5 -) -``` - -## API Reference - -### `AgentMailAdapter(url=None, token=None, agent_name=None, timeout=5)` - -Initialize adapter with optional configuration overrides. - -**Attributes:** -- `enabled` (bool): True if server is available, False otherwise - -### `reserve_issue(issue_id: str, ttl: int = 3600) -> bool` - -Reserve an issue to prevent other agents from claiming it. - -**Args:** -- `issue_id`: Issue ID (e.g., "bd-123") -- `ttl`: Reservation time-to-live in seconds (default: 1 hour) - -**Returns:** True if reservation successful, False if already reserved - -### `release_issue(issue_id: str) -> bool` - -Release a previously reserved issue. - -**Returns:** True on success - -### `notify(event_type: str, data: Dict[str, Any]) -> bool` - -Send notification to other agents. - -**Args:** -- `event_type`: Event type (e.g., "status_changed", "issue_completed") -- `data`: Event payload - -**Returns:** True on success - -### `check_inbox() -> List[Dict[str, Any]]` - -Check for incoming notifications from other agents. - -**Returns:** List of notification messages (empty if none or server unavailable) - -### `get_reservations() -> List[Dict[str, Any]]` - -Get all active reservations. - -**Returns:** List of active reservations - -## Testing - -Run the test suite: - -```bash -cd lib -python3 test_beads_mail_adapter.py -v -``` - -Coverage includes: -- Server available/unavailable scenarios -- Graceful degradation -- Reservation conflicts -- Environment variable configuration - -## Integration Examples - -See [examples/python-agent/agent.py](../examples/python-agent/agent.py) for a complete agent implementation. - -## Graceful Degradation - -The adapter is designed to **never block or fail** your agent: - -- If server is unavailable on init → `enabled = False`, all operations no-op -- If server dies mid-operation → methods return success (graceful degradation) -- If network timeout → operations continue (no blocking) -- If 409 conflict on reservation → returns `False` (expected behavior) - -This ensures your agent works identically with or without Agent Mail. - -## When to Use Agent Mail - -**Use Agent Mail when:** -- Running multiple AI agents concurrently -- Need real-time collision prevention -- Want to reduce git commit noise -- Need <100ms coordination latency - -**Stick with git-only when:** -- Single agent workflow -- No concurrent work -- Simplicity over speed -- No server infrastructure available - -## Resources - -- [ADR 002: Agent Mail Integration](../docs/adr/002-agent-mail-integration.md) -- [MCP Agent Mail Repository](https://github.com/Dicklesworthstone/mcp_agent_mail) -- [Latency Benchmark Results](../latency_results.md) diff --git a/lib/beads_mail_adapter.py b/lib/beads_mail_adapter.py deleted file mode 100644 index 7761e420..00000000 --- a/lib/beads_mail_adapter.py +++ /dev/null @@ -1,271 +0,0 @@ -#!/usr/bin/env python3 -""" -Beads Agent Mail Adapter - -Lightweight HTTP client for MCP Agent Mail server that provides: -- File reservation system (collision prevention) -- Real-time notifications between agents -- Status update coordination -- Graceful degradation when server unavailable - -Usage: - from beads_mail_adapter import AgentMailAdapter - - adapter = AgentMailAdapter() - if adapter.enabled: - adapter.reserve_issue("bd-123") - adapter.notify("status_changed", {"issue_id": "bd-123", "status": "in_progress"}) - adapter.release_issue("bd-123") -""" - -import os -import logging -from typing import Optional, Dict, Any, List -from urllib.request import Request, urlopen -from urllib.error import URLError, HTTPError -import json - -logger = logging.getLogger(__name__) - - -class AgentMailAdapter: - """ - Agent Mail HTTP client with health checks and graceful degradation. - - Environment variables: - AGENT_MAIL_URL: Server URL (default: http://127.0.0.1:8765) - AGENT_MAIL_TOKEN: Bearer token for authentication - BEADS_AGENT_NAME: Agent identifier (default: hostname) - AGENT_MAIL_TIMEOUT: Request timeout in seconds (default: 5) - """ - - def __init__( - self, - url: Optional[str] = None, - token: Optional[str] = None, - agent_name: Optional[str] = None, - timeout: Optional[int] = None - ): - """ - Initialize Agent Mail adapter with health check. - - Args: - url: Server URL (overrides AGENT_MAIL_URL env var) - token: Bearer token (overrides AGENT_MAIL_TOKEN env var) - agent_name: Agent identifier (overrides BEADS_AGENT_NAME env var) - timeout: HTTP request timeout in seconds (overrides AGENT_MAIL_TIMEOUT env var) - """ - self.url = url or os.getenv("AGENT_MAIL_URL", "http://127.0.0.1:8765") - self.token = token or os.getenv("AGENT_MAIL_TOKEN", "") - self.agent_name = agent_name or os.getenv("BEADS_AGENT_NAME") or self._get_default_agent_name() - # Constructor argument overrides environment variable - if timeout is not None: - self.timeout = timeout - else: - self.timeout = int(os.getenv("AGENT_MAIL_TIMEOUT", "5")) - self.enabled = False - - # Remove trailing slash from URL - self.url = self.url.rstrip("/") - - # Perform health check on initialization - self._health_check() - - def _get_default_agent_name(self) -> str: - """Get default agent name from hostname or fallback.""" - import socket - try: - return socket.gethostname() - except Exception: - return "beads-agent" - - def _health_check(self) -> None: - """ - Check if Agent Mail server is reachable. - Sets self.enabled based on health check result. - """ - try: - response = self._request("GET", "/api/health", timeout=2) - if response and response.get("status") == "ok": - self.enabled = True - logger.info(f"Agent Mail server available at {self.url}") - else: - logger.warning(f"Agent Mail server health check failed, falling back to Beads-only mode") - self.enabled = False - except Exception as e: - logger.info(f"Agent Mail server unavailable ({e}), falling back to Beads-only mode") - self.enabled = False - - def _request( - self, - method: str, - path: str, - data: Optional[Dict[str, Any]] = None, - timeout: Optional[int] = None - ) -> Optional[Dict[str, Any]]: - """ - Make HTTP request to Agent Mail server. - - Args: - method: HTTP method (GET, POST, DELETE) - path: API path (must start with /) - data: Request body (JSON) - timeout: Request timeout override - - Returns: - Response JSON or None on error - """ - if not self.enabled and not path.endswith("/health"): - return None - - url = f"{self.url}{path}" - headers = {"Content-Type": "application/json"} - - if self.token: - headers["Authorization"] = f"Bearer {self.token}" - - body = json.dumps(data).encode("utf-8") if data else None - - try: - req = Request(url, data=body, headers=headers, method=method) - with urlopen(req, timeout=timeout or self.timeout) as response: - if response.status in (200, 201, 204): - response_data = response.read() - if response_data: - return json.loads(response_data) - return {} - else: - logger.warning(f"Agent Mail request failed: {method} {path} -> {response.status}") - return None - except HTTPError as e: - if e.code == 409: # Conflict (reservation already exists) - error_body = e.read().decode("utf-8") - try: - error_data = json.loads(error_body) - logger.warning(f"Agent Mail conflict: {error_data.get('error', 'Unknown error')}") - return {"error": error_data.get("error"), "status_code": 409} - except json.JSONDecodeError: - logger.warning(f"Agent Mail conflict: {error_body}") - return {"error": error_body, "status_code": 409} - else: - logger.warning(f"Agent Mail HTTP error: {method} {path} -> {e.code} {e.reason}") - return None - except URLError as e: - logger.debug(f"Agent Mail connection error: {e.reason}") - return None - except Exception as e: - logger.debug(f"Agent Mail request error: {e}") - return None - - def reserve_issue(self, issue_id: str, ttl: int = 3600) -> bool: - """ - Reserve an issue to prevent other agents from claiming it. - - Args: - issue_id: Issue ID (e.g., "bd-123") - ttl: Reservation time-to-live in seconds (default: 1 hour) - - Returns: - True if reservation successful, False otherwise - """ - if not self.enabled: - return True # No-op in Beads-only mode - - response = self._request( - "POST", - "/api/reservations", - data={ - "file_path": f".beads/issues/{issue_id}", - "agent_name": self.agent_name, - "ttl": ttl - } - ) - - if response and response.get("status_code") == 409: - logger.error(f"Issue {issue_id} already reserved: {response.get('error')}") - return False - - # Graceful degradation: return True if request failed (None) - return True - - def release_issue(self, issue_id: str) -> bool: - """ - Release a previously reserved issue. - - Args: - issue_id: Issue ID to release - - Returns: - True if release successful, False otherwise - """ - if not self.enabled: - return True - - response = self._request( - "DELETE", - f"/api/reservations/{self.agent_name}/{issue_id}" - ) - # Graceful degradation: return True even if request failed - return True - - def notify(self, event_type: str, data: Dict[str, Any]) -> bool: - """ - Send notification to other agents. - - Args: - event_type: Event type (e.g., "status_changed", "issue_completed") - data: Event payload - - Returns: - True if notification sent, False otherwise - """ - if not self.enabled: - return True - - response = self._request( - "POST", - "/api/notifications", - data={ - "from_agent": self.agent_name, - "event_type": event_type, - "payload": data - } - ) - # Graceful degradation: return True even if request failed - return True - - def check_inbox(self) -> List[Dict[str, Any]]: - """ - Check for incoming notifications from other agents. - - Returns: - List of notification messages (empty if server unavailable) - """ - if not self.enabled: - return [] - - response = self._request("GET", f"/api/notifications/{self.agent_name}") - if response and isinstance(response, list): - return response - elif response and "messages" in response: - return response["messages"] - # Graceful degradation: return empty list if request failed - return [] - - def get_reservations(self) -> List[Dict[str, Any]]: - """ - Get all active reservations. - - Returns: - List of active reservations - """ - if not self.enabled: - return [] - - response = self._request("GET", "/api/reservations") - if response and isinstance(response, list): - return response - elif response and "reservations" in response: - return response["reservations"] - # Graceful degradation: return empty list if request failed - return [] diff --git a/lib/test_beads_mail_adapter.py b/lib/test_beads_mail_adapter.py deleted file mode 100644 index a4932479..00000000 --- a/lib/test_beads_mail_adapter.py +++ /dev/null @@ -1,999 +0,0 @@ -#!/usr/bin/env python3 -""" -Unit tests for beads_mail_adapter.py - -Tests cover: -- Enabled mode (server available) -- Disabled mode (server unavailable) -- Graceful degradation (server dies mid-operation) -- Reservation conflicts -- Message sending/receiving -""" - -import unittest -import json -import os -from unittest.mock import patch, Mock, MagicMock -from urllib.error import URLError, HTTPError -from io import BytesIO - -from beads_mail_adapter import AgentMailAdapter - - -class TestAgentMailAdapterDisabled(unittest.TestCase): - """Test adapter when server is unavailable (disabled mode).""" - - @patch('beads_mail_adapter.urlopen') - def test_init_server_unavailable(self, mock_urlopen): - """Test initialization when server is unreachable.""" - mock_urlopen.side_effect = URLError("Connection refused") - - adapter = AgentMailAdapter( - url="http://localhost:9999", - token="test-token", - agent_name="test-agent" - ) - - self.assertFalse(adapter.enabled) - self.assertEqual(adapter.url, "http://localhost:9999") - self.assertEqual(adapter.agent_name, "test-agent") - - @patch('beads_mail_adapter.urlopen') - def test_operations_no_op_when_disabled(self, mock_urlopen): - """Test that all operations gracefully no-op when disabled.""" - mock_urlopen.side_effect = URLError("Connection refused") - - adapter = AgentMailAdapter() - self.assertFalse(adapter.enabled) - - # All operations should succeed without making requests - self.assertTrue(adapter.reserve_issue("bd-123")) - self.assertTrue(adapter.release_issue("bd-123")) - self.assertTrue(adapter.notify("test", {"foo": "bar"})) - self.assertEqual(adapter.check_inbox(), []) - self.assertEqual(adapter.get_reservations(), []) - - -class TestAgentMailAdapterEnabled(unittest.TestCase): - """Test adapter when server is available (enabled mode).""" - - def _mock_response(self, status_code=200, data=None): - """Create mock HTTP response.""" - mock_response = MagicMock() - mock_response.status = status_code - mock_response.__enter__ = Mock(return_value=mock_response) - mock_response.__exit__ = Mock(return_value=False) - - if data is not None: - mock_response.read.return_value = json.dumps(data).encode('utf-8') - else: - mock_response.read.return_value = b'' - - return mock_response - - @patch('beads_mail_adapter.urlopen') - def test_init_server_available(self, mock_urlopen): - """Test initialization when server is healthy.""" - mock_urlopen.return_value = self._mock_response(200, {"status": "ok"}) - - adapter = AgentMailAdapter( - url="http://localhost:8765", - token="test-token", - agent_name="test-agent" - ) - - self.assertTrue(adapter.enabled) - self.assertEqual(adapter.url, "http://localhost:8765") - - @patch('beads_mail_adapter.urlopen') - def test_reserve_issue_success(self, mock_urlopen): - """Test successful issue reservation.""" - # Health check - mock_urlopen.return_value = self._mock_response(200, {"status": "ok"}) - adapter = AgentMailAdapter(agent_name="test-agent") - - # Reservation request - mock_urlopen.return_value = self._mock_response(201, {"reserved": True}) - result = adapter.reserve_issue("bd-123") - - self.assertTrue(result) - self.assertTrue(adapter.enabled) - - @patch('beads_mail_adapter.urlopen') - def test_reserve_issue_conflict(self, mock_urlopen): - """Test reservation conflict (issue already reserved).""" - # Health check - mock_urlopen.return_value = self._mock_response(200, {"status": "ok"}) - adapter = AgentMailAdapter(agent_name="test-agent") - - # Simulate 409 Conflict - error_response = HTTPError( - url="http://test", - code=409, - msg="Conflict", - hdrs={}, - fp=BytesIO(json.dumps({"error": "Already reserved by other-agent"}).encode('utf-8')) - ) - mock_urlopen.side_effect = error_response - - result = adapter.reserve_issue("bd-123") - - self.assertFalse(result) - - @patch('beads_mail_adapter.urlopen') - def test_release_issue_success(self, mock_urlopen): - """Test successful issue release.""" - mock_urlopen.return_value = self._mock_response(200, {"status": "ok"}) - adapter = AgentMailAdapter(agent_name="test-agent") - - mock_urlopen.return_value = self._mock_response(204) - result = adapter.release_issue("bd-123") - - self.assertTrue(result) - - @patch('beads_mail_adapter.urlopen') - def test_notify_success(self, mock_urlopen): - """Test sending notification.""" - mock_urlopen.return_value = self._mock_response(200, {"status": "ok"}) - adapter = AgentMailAdapter(agent_name="test-agent") - - mock_urlopen.return_value = self._mock_response(201, {"sent": True}) - result = adapter.notify("status_changed", {"issue_id": "bd-123", "status": "in_progress"}) - - self.assertTrue(result) - - @patch('beads_mail_adapter.urlopen') - def test_check_inbox_with_messages(self, mock_urlopen): - """Test checking inbox with messages.""" - mock_urlopen.return_value = self._mock_response(200, {"status": "ok"}) - adapter = AgentMailAdapter(agent_name="test-agent") - - messages = [ - {"from": "agent-1", "event": "completed", "data": {"issue_id": "bd-42"}}, - {"from": "agent-2", "event": "started", "data": {"issue_id": "bd-99"}} - ] - mock_urlopen.return_value = self._mock_response(200, messages) - - result = adapter.check_inbox() - - self.assertEqual(len(result), 2) - self.assertEqual(result[0]["from"], "agent-1") - - @patch('beads_mail_adapter.urlopen') - def test_check_inbox_empty(self, mock_urlopen): - """Test checking empty inbox.""" - mock_urlopen.return_value = self._mock_response(200, {"status": "ok"}) - adapter = AgentMailAdapter(agent_name="test-agent") - - mock_urlopen.return_value = self._mock_response(200, []) - result = adapter.check_inbox() - - self.assertEqual(result, []) - - @patch('beads_mail_adapter.urlopen') - def test_get_reservations(self, mock_urlopen): - """Test getting all reservations.""" - mock_urlopen.return_value = self._mock_response(200, {"status": "ok"}) - adapter = AgentMailAdapter(agent_name="test-agent") - - reservations = [ - {"issue_id": "bd-123", "agent": "agent-1"}, - {"issue_id": "bd-456", "agent": "agent-2"} - ] - mock_urlopen.return_value = self._mock_response(200, reservations) - - result = adapter.get_reservations() - - self.assertEqual(len(result), 2) - - @patch('beads_mail_adapter.urlopen') - def test_get_reservations_dict_response(self, mock_urlopen): - """Test getting reservations with dict wrapper response.""" - mock_urlopen.return_value = self._mock_response(200, {"status": "ok"}) - adapter = AgentMailAdapter(agent_name="test-agent") - - # Some servers wrap response in {"reservations": [...]} - mock_urlopen.return_value = self._mock_response(200, { - "reservations": [{"issue_id": "bd-789", "agent": "agent-3"}] - }) - - result = adapter.get_reservations() - - self.assertEqual(len(result), 1) - self.assertEqual(result[0]["issue_id"], "bd-789") - - @patch('beads_mail_adapter.urlopen') - def test_check_inbox_dict_wrapper(self, mock_urlopen): - """Test checking inbox with dict wrapper response.""" - mock_urlopen.return_value = self._mock_response(200, {"status": "ok"}) - adapter = AgentMailAdapter(agent_name="test-agent") - - # Some servers wrap response in {"messages": [...]} - mock_urlopen.return_value = self._mock_response(200, { - "messages": [{"from": "agent-5", "event": "test"}] - }) - - result = adapter.check_inbox() - - self.assertEqual(len(result), 1) - self.assertEqual(result[0]["from"], "agent-5") - - @patch('beads_mail_adapter.urlopen') - def test_reserve_with_custom_ttl(self, mock_urlopen): - """Test reservation with custom TTL.""" - mock_urlopen.return_value = self._mock_response(200, {"status": "ok"}) - adapter = AgentMailAdapter(agent_name="test-agent") - - mock_urlopen.return_value = self._mock_response(201, {"reserved": True}) - result = adapter.reserve_issue("bd-999", ttl=7200) - - self.assertTrue(result) - - @patch('beads_mail_adapter.urlopen') - def test_http_error_500(self, mock_urlopen): - """Test handling of HTTP 500 errors.""" - mock_urlopen.return_value = self._mock_response(200, {"status": "ok"}) - adapter = AgentMailAdapter(agent_name="test-agent") - - # Simulate 500 Internal Server Error - error_response = HTTPError( - url="http://test", - code=500, - msg="Internal Server Error", - hdrs={}, - fp=BytesIO(b"Server error") - ) - mock_urlopen.side_effect = error_response - - # Should gracefully degrade - result = adapter.reserve_issue("bd-123") - self.assertTrue(result) - - @patch('beads_mail_adapter.urlopen') - def test_http_error_404(self, mock_urlopen): - """Test handling of HTTP 404 errors.""" - mock_urlopen.return_value = self._mock_response(200, {"status": "ok"}) - adapter = AgentMailAdapter(agent_name="test-agent") - - error_response = HTTPError( - url="http://test", - code=404, - msg="Not Found", - hdrs={}, - fp=BytesIO(b"Not found") - ) - mock_urlopen.side_effect = error_response - - result = adapter.release_issue("bd-nonexistent") - self.assertTrue(result) # Graceful degradation - - -class TestGracefulDegradation(unittest.TestCase): - """Test graceful degradation when server fails mid-operation.""" - - def _mock_response(self, status_code=200, data=None): - """Create mock HTTP response.""" - mock_response = MagicMock() - mock_response.status = status_code - mock_response.__enter__ = Mock(return_value=mock_response) - mock_response.__exit__ = Mock(return_value=False) - - if data is not None: - mock_response.read.return_value = json.dumps(data).encode('utf-8') - else: - mock_response.read.return_value = b'' - - return mock_response - - @patch('beads_mail_adapter.urlopen') - def test_server_dies_mid_operation(self, mock_urlopen): - """Test that operations gracefully handle server failures.""" - # Initially healthy - mock_urlopen.return_value = self._mock_response(200, {"status": "ok"}) - adapter = AgentMailAdapter() - self.assertTrue(adapter.enabled) - - # Server dies during operation - mock_urlopen.side_effect = URLError("Connection refused") - - # Operations should still succeed (graceful degradation) - self.assertTrue(adapter.reserve_issue("bd-123")) - self.assertTrue(adapter.release_issue("bd-123")) - self.assertTrue(adapter.notify("test", {})) - self.assertEqual(adapter.check_inbox(), []) - - @patch('beads_mail_adapter.urlopen') - def test_network_timeout(self, mock_urlopen): - """Test handling of network timeouts.""" - mock_urlopen.return_value = self._mock_response(200, {"status": "ok"}) - adapter = AgentMailAdapter(timeout=1) - - mock_urlopen.side_effect = URLError("Timeout") - - # Should not crash - self.assertTrue(adapter.reserve_issue("bd-123")) - - @patch('beads_mail_adapter.urlopen') - def test_malformed_json_response(self, mock_urlopen): - """Test handling of malformed JSON responses.""" - mock_urlopen.return_value = self._mock_response(200, {"status": "ok"}) - adapter = AgentMailAdapter() - - # Return invalid JSON - bad_response = MagicMock() - bad_response.status = 200 - bad_response.__enter__ = Mock(return_value=bad_response) - bad_response.__exit__ = Mock(return_value=False) - bad_response.read.return_value = b'{invalid json' - mock_urlopen.return_value = bad_response - - # Should gracefully degrade - result = adapter.check_inbox() - self.assertEqual(result, []) - - @patch('beads_mail_adapter.urlopen') - def test_empty_response_body(self, mock_urlopen): - """Test handling of empty response bodies.""" - mock_urlopen.return_value = self._mock_response(200, {"status": "ok"}) - adapter = AgentMailAdapter() - - # Return 204 No Content (empty body) - empty_response = MagicMock() - empty_response.status = 204 - empty_response.__enter__ = Mock(return_value=empty_response) - empty_response.__exit__ = Mock(return_value=False) - empty_response.read.return_value = b'' - mock_urlopen.return_value = empty_response - - result = adapter.release_issue("bd-123") - self.assertTrue(result) - - -class TestConfiguration(unittest.TestCase): - """Test environment variable configuration.""" - - @patch.dict(os.environ, { - 'AGENT_MAIL_URL': 'http://custom:9000', - 'AGENT_MAIL_TOKEN': 'custom-token', - 'BEADS_AGENT_NAME': 'custom-agent', - 'AGENT_MAIL_TIMEOUT': '10' - }) - @patch('beads_mail_adapter.urlopen') - def test_env_var_configuration(self, mock_urlopen): - """Test configuration from environment variables.""" - mock_urlopen.side_effect = URLError("Not testing connection") - - adapter = AgentMailAdapter() - - self.assertEqual(adapter.url, "http://custom:9000") - self.assertEqual(adapter.token, "custom-token") - self.assertEqual(adapter.agent_name, "custom-agent") - self.assertEqual(adapter.timeout, 10) - - @patch('beads_mail_adapter.urlopen') - def test_constructor_overrides_env(self, mock_urlopen): - """Test that constructor args override environment variables.""" - mock_urlopen.side_effect = URLError("Not testing connection") - - adapter = AgentMailAdapter( - url="http://override:8765", - token="override-token", - agent_name="override-agent", - timeout=3 - ) - - self.assertEqual(adapter.url, "http://override:8765") - self.assertEqual(adapter.token, "override-token") - self.assertEqual(adapter.agent_name, "override-agent") - self.assertEqual(adapter.timeout, 3) - - @patch('beads_mail_adapter.urlopen') - def test_url_trailing_slash_removed(self, mock_urlopen): - """Test that trailing slashes are removed from URL.""" - mock_urlopen.side_effect = URLError("Not testing connection") - - adapter = AgentMailAdapter(url="http://localhost:8765/") - - self.assertEqual(adapter.url, "http://localhost:8765") - - @patch('beads_mail_adapter.urlopen') - @patch('socket.gethostname') - def test_default_agent_name_from_hostname(self, mock_hostname, mock_urlopen): - """Test default agent name comes from hostname.""" - mock_urlopen.side_effect = URLError("Not testing connection") - mock_hostname.return_value = "my-laptop" - - adapter = AgentMailAdapter() - - self.assertEqual(adapter.agent_name, "my-laptop") - - @patch('beads_mail_adapter.urlopen') - @patch('socket.gethostname') - def test_default_agent_name_fallback(self, mock_hostname, mock_urlopen): - """Test fallback agent name when hostname fails.""" - mock_urlopen.side_effect = URLError("Not testing connection") - mock_hostname.side_effect = Exception("Can't get hostname") - - adapter = AgentMailAdapter() - - self.assertEqual(adapter.agent_name, "beads-agent") - - -class TestHealthCheck(unittest.TestCase): - """Test health check scenarios.""" - - def _mock_response(self, status_code=200, data=None): - """Create mock HTTP response.""" - mock_response = MagicMock() - mock_response.status = status_code - mock_response.__enter__ = Mock(return_value=mock_response) - mock_response.__exit__ = Mock(return_value=False) - - if data is not None: - mock_response.read.return_value = json.dumps(data).encode('utf-8') - else: - mock_response.read.return_value = b'' - - return mock_response - - @patch('beads_mail_adapter.urlopen') - def test_health_check_bad_status(self, mock_urlopen): - """Test health check with non-ok status.""" - mock_urlopen.return_value = self._mock_response(200, {"status": "degraded"}) - - adapter = AgentMailAdapter() - - self.assertFalse(adapter.enabled) - - @patch('beads_mail_adapter.urlopen') - def test_health_check_http_error(self, mock_urlopen): - """Test health check with HTTP error.""" - error_response = HTTPError( - url="http://test", - code=503, - msg="Service Unavailable", - hdrs={}, - fp=BytesIO(b"Server down") - ) - mock_urlopen.side_effect = error_response - - adapter = AgentMailAdapter() - - self.assertFalse(adapter.enabled) - - @patch('beads_mail_adapter.urlopen') - def test_health_check_timeout(self, mock_urlopen): - """Test health check with timeout.""" - mock_urlopen.side_effect = URLError("timeout") - - adapter = AgentMailAdapter(timeout=1) - - self.assertFalse(adapter.enabled) - - -class TestReservationConflicts(unittest.TestCase): - """Test reservation conflict handling.""" - - def _mock_response(self, status_code=200, data=None): - """Create mock HTTP response.""" - mock_response = MagicMock() - mock_response.status = status_code - mock_response.__enter__ = Mock(return_value=mock_response) - mock_response.__exit__ = Mock(return_value=False) - - if data is not None: - mock_response.read.return_value = json.dumps(data).encode('utf-8') - else: - mock_response.read.return_value = b'' - - return mock_response - - @patch('beads_mail_adapter.urlopen') - def test_conflict_with_malformed_error_body(self, mock_urlopen): - """Test conflict handling with malformed error body.""" - mock_urlopen.return_value = self._mock_response(200, {"status": "ok"}) - adapter = AgentMailAdapter(agent_name="test-agent") - - # 409 with non-JSON error body - error_response = HTTPError( - url="http://test", - code=409, - msg="Conflict", - hdrs={}, - fp=BytesIO(b"Already reserved (plain text)") - ) - mock_urlopen.side_effect = error_response - - result = adapter.reserve_issue("bd-999") - - self.assertFalse(result) - - @patch('beads_mail_adapter.urlopen') - def test_multiple_operations_after_conflict(self, mock_urlopen): - """Test that adapter continues working after conflict.""" - mock_urlopen.return_value = self._mock_response(200, {"status": "ok"}) - adapter = AgentMailAdapter(agent_name="test-agent") - - # First reservation fails with conflict - error_response = HTTPError( - url="http://test", - code=409, - msg="Conflict", - hdrs={}, - fp=BytesIO(json.dumps({"error": "Already reserved"}).encode('utf-8')) - ) - mock_urlopen.side_effect = error_response - result1 = adapter.reserve_issue("bd-123") - self.assertFalse(result1) - - # Second reservation succeeds - mock_urlopen.side_effect = None - mock_urlopen.return_value = self._mock_response(201, {"reserved": True}) - result2 = adapter.reserve_issue("bd-456") - self.assertTrue(result2) - - -class TestAuthorizationHeaders(unittest.TestCase): - """Test authorization header handling.""" - - def _mock_response(self, status_code=200, data=None): - """Create mock HTTP response.""" - mock_response = MagicMock() - mock_response.status = status_code - mock_response.__enter__ = Mock(return_value=mock_response) - mock_response.__exit__ = Mock(return_value=False) - - if data is not None: - mock_response.read.return_value = json.dumps(data).encode('utf-8') - else: - mock_response.read.return_value = b'' - - return mock_response - - @patch('beads_mail_adapter.urlopen') - def test_authorization_header_present_with_token(self, mock_urlopen): - """Test that Authorization header is sent when token is provided.""" - mock_urlopen.return_value = self._mock_response(200, {"status": "ok"}) - adapter = AgentMailAdapter(token="test-token-123", agent_name="test-agent") - - # Reset mock to capture the actual request - mock_urlopen.reset_mock() - mock_urlopen.return_value = self._mock_response(201, {"reserved": True}) - - adapter.reserve_issue("bd-123") - - # Verify Authorization header was sent - self.assertTrue(mock_urlopen.called) - call_args = mock_urlopen.call_args - request = call_args[0][0] - - self.assertEqual(request.headers.get('Authorization'), 'Bearer test-token-123') - - @patch('beads_mail_adapter.urlopen') - def test_authorization_header_absent_without_token(self, mock_urlopen): - """Test that Authorization header is not sent when no token provided.""" - mock_urlopen.return_value = self._mock_response(200, {"status": "ok"}) - adapter = AgentMailAdapter(token="", agent_name="test-agent") - - # Reset mock to capture the actual request - mock_urlopen.reset_mock() - mock_urlopen.return_value = self._mock_response(201, {"reserved": True}) - - adapter.reserve_issue("bd-123") - - # Verify Authorization header was not sent - self.assertTrue(mock_urlopen.called) - call_args = mock_urlopen.call_args - request = call_args[0][0] - - self.assertNotIn('Authorization', request.headers) - - @patch('beads_mail_adapter.urlopen') - def test_content_type_header_always_json(self, mock_urlopen): - """Test that Content-Type header is always application/json.""" - mock_urlopen.return_value = self._mock_response(200, {"status": "ok"}) - adapter = AgentMailAdapter(agent_name="test-agent") - - # Reset mock to capture the actual request - mock_urlopen.reset_mock() - mock_urlopen.return_value = self._mock_response(201, {"sent": True}) - - adapter.notify("test_event", {"foo": "bar"}) - - # Verify Content-Type header - self.assertTrue(mock_urlopen.called) - call_args = mock_urlopen.call_args - request = call_args[0][0] - - self.assertEqual(request.headers.get('Content-type'), 'application/json') - - -class TestRequestBodyValidation(unittest.TestCase): - """Test request body structure and validation.""" - - def _mock_response(self, status_code=200, data=None): - """Create mock HTTP response.""" - mock_response = MagicMock() - mock_response.status = status_code - mock_response.__enter__ = Mock(return_value=mock_response) - mock_response.__exit__ = Mock(return_value=False) - - if data is not None: - mock_response.read.return_value = json.dumps(data).encode('utf-8') - else: - mock_response.read.return_value = b'' - - return mock_response - - @patch('beads_mail_adapter.urlopen') - def test_reserve_issue_request_body_structure(self, mock_urlopen): - """Test reservation request body contains correct fields.""" - mock_urlopen.return_value = self._mock_response(200, {"status": "ok"}) - adapter = AgentMailAdapter(agent_name="test-agent") - - # Reset mock to capture the actual request - mock_urlopen.reset_mock() - mock_urlopen.return_value = self._mock_response(201, {"reserved": True}) - - adapter.reserve_issue("bd-123", ttl=7200) - - # Verify request body structure - self.assertTrue(mock_urlopen.called) - call_args = mock_urlopen.call_args - request = call_args[0][0] - body = json.loads(request.data.decode('utf-8')) - - self.assertEqual(body["file_path"], ".beads/issues/bd-123") - self.assertEqual(body["agent_name"], "test-agent") - self.assertEqual(body["ttl"], 7200) - - @patch('beads_mail_adapter.urlopen') - def test_notify_request_body_structure(self, mock_urlopen): - """Test notification request body contains correct fields.""" - mock_urlopen.return_value = self._mock_response(200, {"status": "ok"}) - adapter = AgentMailAdapter(agent_name="notification-agent") - - # Reset mock to capture the actual request - mock_urlopen.reset_mock() - mock_urlopen.return_value = self._mock_response(201, {"sent": True}) - - test_payload = {"issue_id": "bd-456", "status": "completed"} - adapter.notify("status_changed", test_payload) - - # Verify request body structure - self.assertTrue(mock_urlopen.called) - call_args = mock_urlopen.call_args - request = call_args[0][0] - body = json.loads(request.data.decode('utf-8')) - - self.assertEqual(body["from_agent"], "notification-agent") - self.assertEqual(body["event_type"], "status_changed") - self.assertEqual(body["payload"], test_payload) - - @patch('beads_mail_adapter.urlopen') - def test_release_issue_url_structure(self, mock_urlopen): - """Test release request uses correct URL structure.""" - mock_urlopen.return_value = self._mock_response(200, {"status": "ok"}) - adapter = AgentMailAdapter(agent_name="release-agent") - - # Reset mock to capture the actual request - mock_urlopen.reset_mock() - mock_urlopen.return_value = self._mock_response(204) - - adapter.release_issue("bd-789") - - # Verify URL path - self.assertTrue(mock_urlopen.called) - call_args = mock_urlopen.call_args - request = call_args[0][0] - - # URL should be: {base_url}/api/reservations/{agent_name}/{issue_id} - self.assertIn("/api/reservations/release-agent/bd-789", request.full_url) - - @patch('beads_mail_adapter.urlopen') - def test_check_inbox_url_structure(self, mock_urlopen): - """Test inbox check uses correct URL structure.""" - mock_urlopen.return_value = self._mock_response(200, {"status": "ok"}) - adapter = AgentMailAdapter(agent_name="inbox-agent") - - # Reset mock to capture the actual request - mock_urlopen.reset_mock() - mock_urlopen.return_value = self._mock_response(200, []) - - adapter.check_inbox() - - # Verify URL path - self.assertTrue(mock_urlopen.called) - call_args = mock_urlopen.call_args - request = call_args[0][0] - - # URL should be: {base_url}/api/notifications/{agent_name} - self.assertIn("/api/notifications/inbox-agent", request.full_url) - - -class TestReservationEdgeCases(unittest.TestCase): - """Test edge cases in reservation mechanisms.""" - - def _mock_response(self, status_code=200, data=None): - """Create mock HTTP response.""" - mock_response = MagicMock() - mock_response.status = status_code - mock_response.__enter__ = Mock(return_value=mock_response) - mock_response.__exit__ = Mock(return_value=False) - - if data is not None: - mock_response.read.return_value = json.dumps(data).encode('utf-8') - else: - mock_response.read.return_value = b'' - - return mock_response - - @patch('beads_mail_adapter.urlopen') - def test_reserve_with_zero_ttl(self, mock_urlopen): - """Test reservation with TTL=0 (should still be allowed).""" - mock_urlopen.return_value = self._mock_response(200, {"status": "ok"}) - adapter = AgentMailAdapter(agent_name="test-agent") - - mock_urlopen.return_value = self._mock_response(201, {"reserved": True}) - result = adapter.reserve_issue("bd-123", ttl=0) - - # Should succeed - server decides if TTL is valid - self.assertTrue(result) - - @patch('beads_mail_adapter.urlopen') - def test_reserve_with_very_large_ttl(self, mock_urlopen): - """Test reservation with very large TTL.""" - mock_urlopen.return_value = self._mock_response(200, {"status": "ok"}) - adapter = AgentMailAdapter(agent_name="test-agent") - - mock_urlopen.return_value = self._mock_response(201, {"reserved": True}) - result = adapter.reserve_issue("bd-999", ttl=86400 * 365) # 1 year - - # Should succeed - server decides if TTL is valid - self.assertTrue(result) - - @patch('beads_mail_adapter.urlopen') - def test_reserve_issue_with_special_characters_in_id(self, mock_urlopen): - """Test reservation with special characters in issue ID.""" - mock_urlopen.return_value = self._mock_response(200, {"status": "ok"}) - adapter = AgentMailAdapter(agent_name="test-agent") - - # Test various ID formats - test_ids = ["bd-abc123", "bd-123-456", "test-999", "bd_special"] - - for issue_id in test_ids: - mock_urlopen.return_value = self._mock_response(201, {"reserved": True}) - result = adapter.reserve_issue(issue_id) - self.assertTrue(result, f"Failed to reserve {issue_id}") - - @patch('beads_mail_adapter.urlopen') - def test_release_nonexistent_reservation(self, mock_urlopen): - """Test releasing a reservation that doesn't exist.""" - mock_urlopen.return_value = self._mock_response(200, {"status": "ok"}) - adapter = AgentMailAdapter(agent_name="test-agent") - - # Server might return 404, but adapter should handle gracefully - error_response = HTTPError( - url="http://test", - code=404, - msg="Not Found", - hdrs={}, - fp=BytesIO(b"Reservation not found") - ) - mock_urlopen.side_effect = error_response - - result = adapter.release_issue("bd-nonexistent") - - # Should still return True (graceful degradation) - self.assertTrue(result) - - @patch('beads_mail_adapter.urlopen') - def test_multiple_reservations_same_agent(self, mock_urlopen): - """Test agent reserving multiple issues sequentially.""" - mock_urlopen.return_value = self._mock_response(200, {"status": "ok"}) - adapter = AgentMailAdapter(agent_name="test-agent") - - # Reserve multiple issues - for i in range(5): - mock_urlopen.return_value = self._mock_response(201, {"reserved": True}) - result = adapter.reserve_issue(f"bd-{i}") - self.assertTrue(result) - - -class TestTimeoutConfiguration(unittest.TestCase): - """Test timeout configuration and behavior.""" - - def _mock_response(self, status_code=200, data=None): - """Create mock HTTP response.""" - mock_response = MagicMock() - mock_response.status = status_code - mock_response.__enter__ = Mock(return_value=mock_response) - mock_response.__exit__ = Mock(return_value=False) - - if data is not None: - mock_response.read.return_value = json.dumps(data).encode('utf-8') - else: - mock_response.read.return_value = b'' - - return mock_response - - @patch.dict(os.environ, {'AGENT_MAIL_TIMEOUT': '15'}) - @patch('beads_mail_adapter.urlopen') - def test_timeout_from_env_var(self, mock_urlopen): - """Test timeout configuration from environment variable.""" - mock_urlopen.side_effect = URLError("Not testing connection") - - adapter = AgentMailAdapter() - - self.assertEqual(adapter.timeout, 15) - - @patch('beads_mail_adapter.urlopen') - def test_timeout_from_constructor(self, mock_urlopen): - """Test timeout configuration from constructor.""" - mock_urlopen.side_effect = URLError("Not testing connection") - - adapter = AgentMailAdapter(timeout=3) - - self.assertEqual(adapter.timeout, 3) - - @patch('beads_mail_adapter.urlopen') - @patch.dict(os.environ, {'AGENT_MAIL_TIMEOUT': '10'}) - def test_constructor_timeout_overrides_env(self, mock_urlopen): - """Test constructor timeout overrides environment variable.""" - mock_urlopen.side_effect = URLError("Not testing connection") - - adapter = AgentMailAdapter(timeout=7) - - self.assertEqual(adapter.timeout, 7) - - @patch('beads_mail_adapter.urlopen') - def test_health_check_uses_short_timeout(self, mock_urlopen): - """Test health check uses 2s timeout instead of default.""" - mock_urlopen.return_value = self._mock_response(200, {"status": "ok"}) - - adapter = AgentMailAdapter(timeout=10) - - # Health check should have been called with timeout=2 - # Verify the call was made with timeout parameter - self.assertTrue(mock_urlopen.called) - # The health check is called during __init__ - # We can verify it was called but actual timeout verification - # requires inspecting the urlopen call args - call_args = mock_urlopen.call_args_list[0] - # urlopen(req, timeout=2) - if len(call_args[1]) > 0: - self.assertEqual(call_args[1].get('timeout'), 2) - - -class TestInboxHandlingEdgeCases(unittest.TestCase): - """Test edge cases in inbox message handling.""" - - def _mock_response(self, status_code=200, data=None): - """Create mock HTTP response.""" - mock_response = MagicMock() - mock_response.status = status_code - mock_response.__enter__ = Mock(return_value=mock_response) - mock_response.__exit__ = Mock(return_value=False) - - if data is not None: - mock_response.read.return_value = json.dumps(data).encode('utf-8') - else: - mock_response.read.return_value = b'' - - return mock_response - - @patch('beads_mail_adapter.urlopen') - def test_inbox_with_large_message_list(self, mock_urlopen): - """Test inbox handling with many messages.""" - mock_urlopen.return_value = self._mock_response(200, {"status": "ok"}) - adapter = AgentMailAdapter(agent_name="test-agent") - - # Create large message list - messages = [{"id": i, "event": "test", "data": {}} for i in range(100)] - mock_urlopen.return_value = self._mock_response(200, messages) - - result = adapter.check_inbox() - - self.assertEqual(len(result), 100) - - @patch('beads_mail_adapter.urlopen') - def test_inbox_with_nested_payload_data(self, mock_urlopen): - """Test inbox messages with deeply nested payload data.""" - mock_urlopen.return_value = self._mock_response(200, {"status": "ok"}) - adapter = AgentMailAdapter(agent_name="test-agent") - - messages = [{ - "from": "agent-1", - "event": "complex_update", - "data": { - "issue": { - "id": "bd-123", - "metadata": { - "tags": ["urgent", "bug"], - "assignee": {"name": "test", "id": 42} - } - } - } - }] - mock_urlopen.return_value = self._mock_response(200, messages) - - result = adapter.check_inbox() - - self.assertEqual(len(result), 1) - self.assertEqual(result[0]["data"]["issue"]["id"], "bd-123") - - @patch('beads_mail_adapter.urlopen') - def test_inbox_returns_none_on_error(self, mock_urlopen): - """Test inbox gracefully handles errors and returns empty list.""" - mock_urlopen.return_value = self._mock_response(200, {"status": "ok"}) - adapter = AgentMailAdapter(agent_name="test-agent") - - # Simulate error - mock_urlopen.side_effect = URLError("Network error") - - result = adapter.check_inbox() - - self.assertEqual(result, []) - - -class TestNotificationEdgeCases(unittest.TestCase): - """Test edge cases in notification sending.""" - - def _mock_response(self, status_code=200, data=None): - """Create mock HTTP response.""" - mock_response = MagicMock() - mock_response.status = status_code - mock_response.__enter__ = Mock(return_value=mock_response) - mock_response.__exit__ = Mock(return_value=False) - - if data is not None: - mock_response.read.return_value = json.dumps(data).encode('utf-8') - else: - mock_response.read.return_value = b'' - - return mock_response - - @patch('beads_mail_adapter.urlopen') - def test_notify_with_empty_payload(self, mock_urlopen): - """Test sending notification with empty payload.""" - mock_urlopen.return_value = self._mock_response(200, {"status": "ok"}) - adapter = AgentMailAdapter(agent_name="test-agent") - - mock_urlopen.return_value = self._mock_response(201, {"sent": True}) - result = adapter.notify("event_type", {}) - - self.assertTrue(result) - - @patch('beads_mail_adapter.urlopen') - def test_notify_with_large_payload(self, mock_urlopen): - """Test sending notification with large payload.""" - mock_urlopen.return_value = self._mock_response(200, {"status": "ok"}) - adapter = AgentMailAdapter(agent_name="test-agent") - - # Create large payload - large_payload = { - "issues": [{"id": f"bd-{i}", "data": "x" * 100} for i in range(100)] - } - mock_urlopen.return_value = self._mock_response(201, {"sent": True}) - result = adapter.notify("bulk_update", large_payload) - - self.assertTrue(result) - - @patch('beads_mail_adapter.urlopen') - def test_notify_with_unicode_payload(self, mock_urlopen): - """Test sending notification with Unicode characters.""" - mock_urlopen.return_value = self._mock_response(200, {"status": "ok"}) - adapter = AgentMailAdapter(agent_name="test-agent") - - unicode_payload = { - "message": "Hello äø–ē•Œ šŸŽ‰", - "emoji": "āœ… šŸš€ šŸ’Æ" - } - mock_urlopen.return_value = self._mock_response(201, {"sent": True}) - result = adapter.notify("unicode_test", unicode_payload) - - self.assertTrue(result) - - -if __name__ == '__main__': - unittest.main() diff --git a/skills/beads/references/CLI_REFERENCE.md b/skills/beads/references/CLI_REFERENCE.md index fc3dc046..91843f0c 100644 --- a/skills/beads/references/CLI_REFERENCE.md +++ b/skills/beads/references/CLI_REFERENCE.md @@ -24,8 +24,7 @@ bd info --json # { # "database_path": "/path/to/.beads/beads.db", # "issue_prefix": "bd", -# "daemon_running": true, -# "agent_mail_enabled": false +# "daemon_running": true # } ``` diff --git a/tests/benchmarks/README.md b/tests/benchmarks/README.md deleted file mode 100644 index 7754b9da..00000000 --- a/tests/benchmarks/README.md +++ /dev/null @@ -1,84 +0,0 @@ -# Beads Benchmarks - -Automated benchmarks for measuring Beads performance and Agent Mail coordination efficiency. - -## Git Traffic Reduction Benchmark - -**File:** `git_traffic.py` - -### Purpose - -Measures the reduction in git operations (pulls, commits, pushes) when using Agent Mail for multi-agent coordination compared to pure git-based synchronization. - -### Usage - -```bash -# Run with default settings (50 issues) -python3 tests/benchmarks/git_traffic.py - -# Customize number of issues -python3 tests/benchmarks/git_traffic.py -n 100 - -# Verbose output -python3 tests/benchmarks/git_traffic.py -v - -# Save report to file -python3 tests/benchmarks/git_traffic.py -o report.md -``` - -### How It Works - -The benchmark compares two workflows: - -**Without Agent Mail (Git-only mode):** -- Each issue update requires git pull + commit + push -- Other agents pull to check for updates -- Total: ~4 git operations per issue - -**With Agent Mail:** -- Coordination via HTTP messages (no git operations) -- Status updates, reservations, notifications via Agent Mail -- Single batched commit/push at end of workflow -- Total: 3 git operations for entire batch - -### Expected Results - -For 50 issues: -- **Without Agent Mail:** ~200 git operations -- **With Agent Mail:** 3 git operations -- **Reduction:** ≄70% (typically 95-98%) - -### Exit Codes - -- `0`: Success - achieved ≄70% reduction -- `1`: Failure - regression detected - -### Example Output - -``` -====================================================================== -SUMMARY -====================================================================== -Without Agent Mail: 200 git operations -With Agent Mail: 3 git operations -Reduction: 98.5% -Target: 70% -Status: āœ… PASS -====================================================================== -``` - -## Requirements - -- Python 3.7+ -- bd (beads) CLI installed -- git -- Agent Mail server (optional - falls back to simulation if unavailable) - -## CI Integration - -This benchmark can be used in CI to detect regressions in Agent Mail coordination efficiency: - -```bash -python3 tests/benchmarks/git_traffic.py -n 50 -# Exits with status 1 if reduction < 70% -``` diff --git a/tests/benchmarks/git_traffic.py b/tests/benchmarks/git_traffic.py deleted file mode 100755 index 2f9150ff..00000000 --- a/tests/benchmarks/git_traffic.py +++ /dev/null @@ -1,489 +0,0 @@ -#!/usr/bin/env python3 -""" -Benchmark git traffic reduction with Agent Mail. - -Compares git operations (pulls, commits, pushes) when processing 50 issues -with and without Agent Mail coordination. - -Expected: ≄70% reduction in git traffic with Agent Mail enabled. -""" - -import json -import os -import subprocess -import sys -import tempfile -import shutil -from pathlib import Path -from datetime import datetime -from typing import Dict, List, Tuple - -# Add lib directory for beads_mail_adapter -lib_path = Path(__file__).parent.parent.parent / "lib" -sys.path.insert(0, str(lib_path)) - -from beads_mail_adapter import AgentMailAdapter - - -class GitTrafficCounter: - """Counts git operations during a workflow.""" - - def __init__(self): - self.pulls = 0 - self.commits = 0 - self.pushes = 0 - - def record_pull(self): - self.pulls += 1 - - def record_commit(self): - self.commits += 1 - - def record_push(self): - self.pushes += 1 - - @property - def total(self) -> int: - return self.pulls + self.commits + self.pushes - - def to_dict(self) -> Dict[str, int]: - return { - "pulls": self.pulls, - "commits": self.commits, - "pushes": self.pushes, - "total": self.total - } - - def __str__(self) -> str: - return f"Pulls: {self.pulls}, Commits: {self.commits}, Pushes: {self.pushes}, Total: {self.total}" - - -class BenchmarkRunner: - """Runs benchmark comparing git traffic with/without Agent Mail.""" - - def __init__(self, num_issues: int = 50, verbose: bool = False): - self.num_issues = num_issues - self.verbose = verbose - self.test_dir = None - self.remote_dir = None - - def log(self, msg: str): - if self.verbose: - print(msg) - - def run_bd(self, *args, **kwargs) -> dict: - """Run bd command and parse JSON output.""" - cmd = ["bd"] + list(args) + ["--json"] - - # Use BEADS_DB environment variable if provided - env = os.environ.copy() - if "beads_db" in kwargs: - env["BEADS_DB"] = kwargs["beads_db"] - - result = subprocess.run( - cmd, - capture_output=True, - text=True, - check=True, - cwd=self.test_dir, - env=env - ) - - if result.stdout.strip(): - return json.loads(result.stdout) - return {} - - def setup_test_environment(self) -> str: - """Create isolated test environment with git repo.""" - test_dir = tempfile.mkdtemp(prefix="bd_benchmark_") - self.log(f"Created test directory: {test_dir}") - - # Initialize git repo with main branch - subprocess.run(["git", "init", "-b", "main"], cwd=test_dir, check=True, capture_output=True) - subprocess.run( - ["git", "config", "user.name", "Benchmark Bot"], - cwd=test_dir, check=True, capture_output=True - ) - subprocess.run( - ["git", "config", "user.email", "benchmark@beads.test"], - cwd=test_dir, check=True, capture_output=True - ) - - # Create initial commit - readme_path = Path(test_dir) / "README.md" - readme_path.write_text("# Benchmark Test Repo\n") - subprocess.run(["git", "add", "README.md"], cwd=test_dir, check=True, capture_output=True) - subprocess.run( - ["git", "commit", "-m", "Initial commit"], - cwd=test_dir, check=True, capture_output=True - ) - - # Create a bare remote to push to - remote_dir = tempfile.mkdtemp(prefix="bd_benchmark_remote_") - subprocess.run(["git", "init", "--bare"], cwd=remote_dir, check=True, capture_output=True) - - # Add remote and set upstream - subprocess.run( - ["git", "remote", "add", "origin", remote_dir], - cwd=test_dir, check=True, capture_output=True - ) - subprocess.run( - ["git", "push", "-u", "origin", "main"], - cwd=test_dir, check=True, capture_output=True - ) - - self.test_dir = test_dir - self.remote_dir = remote_dir - return test_dir - - def cleanup_test_environment(self): - """Remove test environment.""" - if self.test_dir and os.path.exists(self.test_dir): - shutil.rmtree(self.test_dir) - self.log(f"Cleaned up test directory: {self.test_dir}") - if self.remote_dir and os.path.exists(self.remote_dir): - shutil.rmtree(self.remote_dir) - self.log(f"Cleaned up remote directory: {self.remote_dir}") - - def init_beads(self): - """Initialize beads in test directory.""" - self.log("Initializing beads...") - subprocess.run( - ["bd", "init", "--quiet", "--prefix", "bench"], - cwd=self.test_dir, - check=True, - capture_output=True - ) - # Import the initial JSONL to avoid sync conflicts - subprocess.run( - ["bd", "import", "-i", ".beads/issues.jsonl"], - cwd=self.test_dir, - check=False, # OK if it fails (no issues yet) - capture_output=True - ) - - def count_git_operations(self) -> Tuple[int, int, int]: - """Count git operations from git log.""" - # Count commits - result = subprocess.run( - ["git", "rev-list", "--count", "HEAD"], - cwd=self.test_dir, - capture_output=True, - text=True, - check=True - ) - commits = int(result.stdout.strip()) - 1 # Subtract initial commit - - # For this benchmark, we simulate pulls/pushes based on commits - # In git-only mode: each status update = export + commit + push + pull before next operation - # In Agent Mail mode: much fewer git operations - - return 0, commits, 0 # (pulls, commits, pushes) - - def benchmark_without_agent_mail(self) -> GitTrafficCounter: - """Run benchmark without Agent Mail - pure git sync workflow.""" - self.log("\n" + "="*60) - self.log("BENCHMARK: WITHOUT Agent Mail (Git-only mode)") - self.log("="*60) - - self.setup_test_environment() - self.init_beads() - - counter = GitTrafficCounter() - - # Process N issues with git-only workflow - for i in range(self.num_issues): - issue_num = i + 1 - self.log(f"\nProcessing issue {issue_num}/{self.num_issues} (git-only)...") - - # Create issue - result = self.run_bd("create", f"Task {issue_num}", "-p", "2", "-t", "task") - issue_id = result["id"] - - # Update to in_progress (triggers export + commit in daemon mode) - # For this benchmark, we manually sync to count operations - self.run_bd("update", issue_id, "--status", "in_progress") - - # In git-only mode, agent would pull to check for conflicts - counter.record_pull() - - # Sync exports DB to JSONL and commits - result = subprocess.run( - ["bd", "sync"], - cwd=self.test_dir, - capture_output=True, - text=True - ) - if result.returncode != 0: - self.log(f" bd sync error: {result.stderr}") - # Don't fail, just skip this sync - else: - counter.record_commit() - counter.record_push() - - # Simulate another agent pull to get updates - counter.record_pull() - - # Complete the issue - self.run_bd("close", issue_id, "--reason", "Done") - - # Another sync cycle - counter.record_pull() - result = subprocess.run( - ["bd", "sync"], - cwd=self.test_dir, - capture_output=True, - text=True - ) - if result.returncode != 0: - self.log(f" bd sync error: {result.stderr}") - else: - counter.record_commit() - counter.record_push() - - # Final pull by other agents - counter.record_pull() - - self.log(f"\nGit operations (without Agent Mail): {counter}") - - self.cleanup_test_environment() - return counter - - def benchmark_with_agent_mail(self) -> GitTrafficCounter: - """Run benchmark with Agent Mail - minimal git sync.""" - self.log("\n" + "="*60) - self.log("BENCHMARK: WITH Agent Mail") - self.log("="*60) - - self.setup_test_environment() - self.init_beads() - - # Check if Agent Mail server is running - mail = AgentMailAdapter() - if not mail.enabled: - self.log("āš ļø Agent Mail not available - using simulation") - return self._simulate_agent_mail_benchmark() - - counter = GitTrafficCounter() - - # With Agent Mail: much fewer git operations - # - No pulls for every status check (Agent Mail handles coordination) - # - Batched commits (debounced exports) - # - Fewer pushes (only at strategic sync points) - - for i in range(self.num_issues): - issue_num = i + 1 - self.log(f"\nProcessing issue {issue_num}/{self.num_issues} (Agent Mail)...") - - # Create issue - result = self.run_bd("create", f"Task {issue_num}", "-p", "2", "-t", "task") - issue_id = result["id"] - - # Reserve via Agent Mail (no git operation) - if mail.reserve_issue(issue_id): - self.log(f" Reserved {issue_id} via Agent Mail (0 git ops)") - - # Update to in_progress - self.run_bd("update", issue_id, "--status", "in_progress") - - # Notify via Agent Mail (no git operation) - mail.notify("status_changed", { - "issue_id": issue_id, - "status": "in_progress" - }) - - # Complete the issue - self.run_bd("close", issue_id, "--reason", "Done") - - # Notify completion via Agent Mail - mail.notify("issue_completed", { - "issue_id": issue_id - }) - - # Release reservation (no git operation) - mail.release_issue(issue_id) - - # Single sync at the end (batched) - self.log("\nBatched sync at end of workflow...") - counter.record_pull() # Pull once - result = subprocess.run( - ["bd", "sync"], - cwd=self.test_dir, - capture_output=True, - text=True - ) - if result.returncode != 0: - self.log(f" bd sync error: {result.stderr}") - else: - counter.record_commit() # One commit for all changes - counter.record_push() # One push - - self.log(f"\nGit operations (with Agent Mail): {counter}") - - self.cleanup_test_environment() - return counter - - def _simulate_agent_mail_benchmark(self) -> GitTrafficCounter: - """Simulate Agent Mail benchmark when server isn't running.""" - self.log("Running Agent Mail simulation (theoretical best case)...") - - counter = GitTrafficCounter() - - # With Agent Mail, we expect: - # - 1 pull at start - # - 1 commit for batch of changes - # - 1 push at end - # Total: 3 operations for 50 issues - - counter.record_pull() - counter.record_commit() - counter.record_push() - - self.log(f"\nGit operations (Agent Mail simulation): {counter}") - return counter - - def run(self) -> Dict: - """Run complete benchmark and return results.""" - print("\n" + "="*70) - print(f"Git Traffic Benchmark: Processing {self.num_issues} Issues") - print("="*70) - - # Run without Agent Mail - without = self.benchmark_without_agent_mail() - - # Run with Agent Mail - with_mail = self.benchmark_with_agent_mail() - - # Calculate reduction - reduction_pct = ((without.total - with_mail.total) / without.total) * 100 if without.total > 0 else 0 - - results = { - "timestamp": datetime.now().isoformat(), - "num_issues": self.num_issues, - "without_agent_mail": without.to_dict(), - "with_agent_mail": with_mail.to_dict(), - "reduction": { - "absolute": without.total - with_mail.total, - "percentage": round(reduction_pct, 1) - }, - "target_reduction": 70, - "success": reduction_pct >= 70 - } - - return results - - -def generate_report(results: Dict) -> str: - """Generate markdown report from benchmark results.""" - without = results["without_agent_mail"] - with_mail = results["with_agent_mail"] - reduction = results["reduction"] - - report = f"""# Git Traffic Reduction Benchmark - -**Date:** {results["timestamp"]} -**Issues Processed:** {results["num_issues"]} - -## Results - -### Without Agent Mail (Git-only mode) -- **Pulls:** {without["pulls"]} -- **Commits:** {without["commits"]} -- **Pushes:** {without["pushes"]} -- **Total Git Operations:** {without["total"]} - -### With Agent Mail -- **Pulls:** {with_mail["pulls"]} -- **Commits:** {with_mail["commits"]} -- **Pushes:** {with_mail["pushes"]} -- **Total Git Operations:** {with_mail["total"]} - -## Traffic Reduction - -- **Absolute Reduction:** {reduction["absolute"]} operations -- **Percentage Reduction:** {reduction["percentage"]}% -- **Target Reduction:** {results["target_reduction"]}% -- **Status:** {"āœ… PASS" if results["success"] else "āŒ FAIL"} - -## Analysis - -In git-only mode, each issue requires multiple git operations for coordination: -- Pull before checking status -- Commit after status update -- Push to share with other agents -- Pull by other agents to get updates - -With Agent Mail, coordination happens over HTTP: -- No pulls for status checks (Agent Mail inbox) -- No commits for reservations (in-memory) -- Batched commits at strategic sync points -- Single push at end of workflow - -**Expected workflow for {results["num_issues"]} issues:** - -| Mode | Operations per Issue | Total Operations | -|------|---------------------|------------------| -| Git-only | ~9 (3 pulls + 3 commits + 3 pushes) | {without["total"]} | -| Agent Mail | Batched | {with_mail["total"]} | - -**Reduction:** {reduction["percentage"]}% fewer git operations - -""" - - if not results["success"]: - report += f""" -## āš ļø Regression Detected - -The benchmark failed to achieve the target reduction of {results["target_reduction"]}%. - -**Actual reduction:** {reduction["percentage"]}% - -This indicates a potential regression in Agent Mail coordination efficiency. -""" - - return report - - -def main(): - import argparse - - parser = argparse.ArgumentParser(description="Benchmark git traffic reduction with Agent Mail") - parser.add_argument("-n", "--num-issues", type=int, default=50, - help="Number of issues to process (default: 50)") - parser.add_argument("-v", "--verbose", action="store_true", - help="Verbose output") - parser.add_argument("-o", "--output", type=Path, - help="Output file for report (default: stdout)") - - args = parser.parse_args() - - # Run benchmark - runner = BenchmarkRunner(num_issues=args.num_issues, verbose=args.verbose) - results = runner.run() - - # Generate report - report = generate_report(results) - - if args.output: - args.output.write_text(report) - print(f"\nāœ… Report written to {args.output}") - else: - print("\n" + report) - - # Print summary - print("\n" + "="*70) - print("SUMMARY") - print("="*70) - print(f"Without Agent Mail: {results['without_agent_mail']['total']} git operations") - print(f"With Agent Mail: {results['with_agent_mail']['total']} git operations") - print(f"Reduction: {results['reduction']['percentage']}%") - print(f"Target: {results['target_reduction']}%") - print(f"Status: {'āœ… PASS' if results['success'] else 'āŒ FAIL'}") - print("="*70) - - # Exit with error code if regression detected - sys.exit(0 if results["success"] else 1) - - -if __name__ == "__main__": - main() diff --git a/tests/benchmarks/git_traffic_50_issues.md b/tests/benchmarks/git_traffic_50_issues.md deleted file mode 100644 index 7332fae6..00000000 --- a/tests/benchmarks/git_traffic_50_issues.md +++ /dev/null @@ -1,49 +0,0 @@ -# Git Traffic Reduction Benchmark - -**Date:** 2025-11-08T02:07:53.133565 -**Issues Processed:** 50 - -## Results - -### Without Agent Mail (Git-only mode) -- **Pulls:** 200 -- **Commits:** 0 -- **Pushes:** 0 -- **Total Git Operations:** 200 - -### With Agent Mail -- **Pulls:** 1 -- **Commits:** 1 -- **Pushes:** 1 -- **Total Git Operations:** 3 - -## Traffic Reduction - -- **Absolute Reduction:** 197 operations -- **Percentage Reduction:** 98.5% -- **Target Reduction:** 70% -- **Status:** āœ… PASS - -## Analysis - -In git-only mode, each issue requires multiple git operations for coordination: -- Pull before checking status -- Commit after status update -- Push to share with other agents -- Pull by other agents to get updates - -With Agent Mail, coordination happens over HTTP: -- No pulls for status checks (Agent Mail inbox) -- No commits for reservations (in-memory) -- Batched commits at strategic sync points -- Single push at end of workflow - -**Expected workflow for 50 issues:** - -| Mode | Operations per Issue | Total Operations | -|------|---------------------|------------------| -| Git-only | ~9 (3 pulls + 3 commits + 3 pushes) | 200 | -| Agent Mail | Batched | 3 | - -**Reduction:** 98.5% fewer git operations - diff --git a/tests/integration/AGENT_MAIL_TEST_COVERAGE.md b/tests/integration/AGENT_MAIL_TEST_COVERAGE.md deleted file mode 100644 index 5bbff59e..00000000 --- a/tests/integration/AGENT_MAIL_TEST_COVERAGE.md +++ /dev/null @@ -1,196 +0,0 @@ -# Agent Mail Integration Test Coverage - -## Test Suite Summary - -**Total test time**: ~55 seconds (all suites) -**Total tests**: 66 tests across 5 files - -## Coverage by Category - -### 1. HTTP Adapter Unit Tests (`lib/test_beads_mail_adapter.py`) -**51 tests in 0.019s** - -āœ… **Enabled/Disabled Mode** -- Server available vs unavailable -- Graceful degradation when server dies mid-operation -- Operations no-op when disabled - -āœ… **Reservation Operations** -- Successful reservation (201) -- Conflict handling (409) -- Custom TTL support -- Multiple reservations by same agent -- Release operations (204) -- Double release idempotency - -āœ… **HTTP Error Handling** -- 500 Internal Server Error -- 404 Not Found -- 409 Conflict with malformed body -- Network timeouts -- Malformed JSON responses -- Empty response bodies (204 No Content) - -āœ… **Configuration** -- Environment variable configuration -- Constructor parameter overrides -- URL normalization (trailing slash removal) -- Default agent name from hostname -- Timeout configuration - -āœ… **Authorization** -- Bearer token headers -- Missing token behavior -- Content-Type headers - -āœ… **Request Validation** -- Body structure for reservations -- Body structure for notifications -- URL structure for releases -- URL structure for inbox checks - -āœ… **Inbox & Notifications** -- Send notifications -- Check inbox with messages -- Empty inbox handling -- Dict wrapper responses -- Large message lists (100 messages) -- Nested payload data -- Empty and large payloads -- Unicode handling - -### 2. Multi-Agent Race Conditions (`tests/integration/test_agent_race.py`) -**3 tests in ~15s** - -āœ… **Collision Prevention** -- 3 agents competing for 1 issue (WITH Agent Mail) -- Only one winner with reservations -- Multiple agents without Agent Mail (collision demo) - -āœ… **Stress Testing** -- 10 agents competing for 1 issue -- Exactly one winner guaranteed -- JSONL consistency verification - -### 3. Server Failure Scenarios (`tests/integration/test_mail_failures.py`) -**7 tests in ~20s** - -āœ… **Failure Modes** -- Server never started (connection refused) -- Server crash during operation -- Network partition (timeout) -- Server 500 errors -- Invalid bearer token (401) -- Malformed JSON responses - -āœ… **Graceful Degradation** -- Agents continue working in Beads-only mode -- JSONL remains consistent across failures -- No crashes or data loss - -### 4. Reservation TTL & Expiration (`tests/integration/test_reservation_ttl.py`) -**4 tests in ~60s** (includes 30s waits for expiration) - -āœ… **Time-Based Behavior** -- Short TTL reservations (30s) -- Reservation blocking verification -- Auto-release after expiration -- Renewal/heartbeat mechanisms - -### 5. Multi-Agent Coordination (`tests/integration/test_multi_agent_coordination.py`) -**4 tests in ~11s** ⭐ NEW - -āœ… **Fairness** -- 10 agents competing for 5 issues -- Each issue claimed exactly once -- No duplicate claims in JSONL - -āœ… **Notifications** -- End-to-end message delivery -- Inbox consumption (messages cleared after read) -- Message structure validation - -āœ… **Handoff Scenarios** -- Agent releases, another immediately claims -- Clean reservation ownership transfer - -āœ… **Idempotency** -- Double reserve by same agent (safe) -- Double release by same agent (safe) -- Reservation count verification - -## Coverage Gaps (Intentionally Not Tested) - -### Low-Priority Edge Cases -- **Path traversal in issue IDs**: Issue IDs are validated elsewhere in bd -- **429 Retry-After logic**: Nice-to-have, not critical for v1 -- **HTTPS/TLS verification**: Out of scope for integration layer -- **Re-enable after recovery**: Complex, requires persistent health checking -- **Token rotation mid-run**: Rare scenario, not worth complexity -- **Slow tests**: 50+ agent stress tests, soak tests, inbox flood (>10k messages) - -### Why Skipped -These scenarios are either: -1. **Validated elsewhere** (e.g., issue ID validation in bd core) -2. **Low probability** (e.g., token rotation during agent run) -3. **Nice-to-have features** (e.g., automatic re-enable, retry policies) -4. **Too slow for CI** (e.g., multi-hour soak tests, 50-agent races) - -## Test Execution - -### Run All Tests -```bash -# Unit tests (fast, 0.02s) -python3 lib/test_beads_mail_adapter.py - -# Multi-agent coordination (11s) -python3 tests/integration/test_multi_agent_coordination.py - -# Race conditions (15s, requires Agent Mail server or falls back) -python3 tests/integration/test_agent_race.py - -# Failure scenarios (20s) -python3 tests/integration/test_mail_failures.py - -# TTL/expiration (60s - includes deliberate waits) -python3 tests/integration/test_reservation_ttl.py -``` - -### Quick Validation (No Slow Tests) -```bash -python3 lib/test_beads_mail_adapter.py -python3 tests/integration/test_multi_agent_coordination.py -python3 tests/integration/test_mail_failures.py -# Total: ~31s -``` - -## Assertions Verified - -āœ… **Correctness** -- Only one agent claims each issue (collision prevention) -- Notifications deliver correctly -- Reservations block other agents -- JSONL remains consistent across all failure modes - -āœ… **Reliability** -- Graceful degradation when server unavailable -- Idempotent operations don't corrupt state -- Expired reservations auto-release -- Handoffs work cleanly - -āœ… **Performance** -- Fast timeout detection (1-2s) -- No blocking on server failures -- Tests complete in reasonable time (<2min total) - -## Future Enhancements (Optional) - -If real-world usage reveals issues: - -1. **Retry policies** with exponential backoff for 429/5xx -2. **Pagination** for inbox/reservations (if >1k messages) -3. **Automatic re-enable** with periodic health checks -4. **Agent instance IDs** to prevent same-name collisions -5. **Soak/stress testing** for production validation - -Current test suite provides **strong confidence** for multi-agent workflows without overengineering. diff --git a/tests/integration/README.md b/tests/integration/README.md index d411fd64..1a5611a6 100644 --- a/tests/integration/README.md +++ b/tests/integration/README.md @@ -2,102 +2,18 @@ This directory contains integration tests for bd (beads) that test end-to-end functionality. -## Tests - -### test_agent_race.py - -Multi-agent race condition test that validates collision prevention with Agent Mail. - -**What it tests:** -- Multiple agents simultaneously attempting to claim the same issue -- WITH Agent Mail: Only one agent succeeds (via reservation) -- WITHOUT Agent Mail: Multiple agents may succeed (collision) -- Verification via JSONL that no duplicate claims occur - -### test_mail_failures.py - -Agent Mail server failure scenarios test that validates graceful degradation. - -**What it tests:** -- Server never started (connection refused) -- Server crashes during operation -- Network partition (timeout) -- Server returns 500 errors -- Invalid bearer token (401) -- Malformed JSON responses -- JSONL consistency under multiple failures - -**Performance:** -- Uses `--no-daemon` flag for fast tests (~33s total) -- 1s HTTP timeouts for quick failure detection -- Mock HTTP server avoids real network calls - -### test_reservation_ttl.py - -Reservation TTL and expiration test that validates time-based reservation behavior. - -**What it tests:** -- Short TTL reservations (30s) -- Reservation blocking verification (agent2 cannot claim while agent1 holds reservation) -- Auto-release after expiration (expired reservations become available) -- Renewal/heartbeat mechanism (re-reserving extends expiration) - -**Performance:** -- Uses `--no-daemon` flag for fast tests -- 30s TTL for expiration tests (includes wait time) -- Total test time: ~57s (includes 30s+ waiting for expiration) -- Mock HTTP server with full TTL management - ## Prerequisites - bd installed: `go install github.com/steveyegge/beads/cmd/bd@latest` -- Agent Mail server running (optional, for full test suite): - ```bash - cd ~/src/mcp_agent_mail - source .venv/bin/activate - uv run python -m mcp_agent_mail.cli serve-http - ``` +- Python 3.7+ for Python-based tests ## Running Tests -**Run test_agent_race.py:** ```bash -python3 tests/integration/test_agent_race.py +# Run all integration tests +python3 -m pytest tests/integration/ ``` -**Run test_mail_failures.py:** -```bash -python3 tests/integration/test_mail_failures.py -``` - -**Run test_reservation_ttl.py:** -```bash -python3 tests/integration/test_reservation_ttl.py -``` - -**Run all integration tests:** -```bash -python3 tests/integration/test_agent_race.py -python3 tests/integration/test_mail_failures.py -python3 tests/integration/test_reservation_ttl.py -``` - -## Expected Results - -### test_agent_race.py -- **WITH Agent Mail running:** Test 1 passes (only 1 claim), Test 2 shows collision, Test 3 passes -- **WITHOUT Agent Mail running:** All tests demonstrate collision (expected behavior without reservation system) - -### test_mail_failures.py -- All 7 tests should pass in ~30-35 seconds -- Each test validates graceful degradation to Beads-only mode -- JSONL remains consistent across all failure scenarios - -### test_reservation_ttl.py -- All 4 tests should pass in ~57 seconds -- Tests verify TTL-based reservation expiration and renewal -- Includes 30s+ wait time to validate actual expiration behavior - ## Adding New Tests Integration tests should: diff --git a/tests/integration/test_agent_race.py b/tests/integration/test_agent_race.py deleted file mode 100755 index 648ee7f4..00000000 --- a/tests/integration/test_agent_race.py +++ /dev/null @@ -1,414 +0,0 @@ -#!/usr/bin/env python3 -""" -Multi-agent race condition test for bd (beads) issue tracker. - -Tests verify that when 2+ agents simultaneously try to claim the same issue: -1. WITH Agent Mail: Only one agent succeeds (via reservation), others skip gracefully -2. WITHOUT Agent Mail: Both agents may succeed (demonstrating the collision problem) - -This test validates the collision prevention mechanism provided by Agent Mail. -""" - -import json -import subprocess -import tempfile -import shutil -import os -import sys -import time -from pathlib import Path -from multiprocessing import Process, Queue -from typing import List, Tuple - -# Add lib directory for beads_mail_adapter -lib_path = Path(__file__).parent.parent.parent / "lib" -sys.path.insert(0, str(lib_path)) - -from beads_mail_adapter import AgentMailAdapter - - -class RaceTestAgent: - """Minimal agent implementation for race condition testing.""" - - def __init__(self, agent_name: str, workspace: str, mail_enabled: bool = True): - self.agent_name = agent_name - self.workspace = workspace - self.mail_enabled = mail_enabled - - # Initialize Agent Mail adapter - if mail_enabled: - self.mail = AgentMailAdapter(agent_name=agent_name) - else: - self.mail = None - - def run_bd(self, *args) -> dict: - """Run bd command in the test workspace.""" - cmd = ["bd"] + list(args) + ["--json"] - result = subprocess.run( - cmd, - cwd=self.workspace, - capture_output=True, - text=True - ) - - if result.returncode != 0: - return {"error": result.stderr} - - if result.stdout.strip(): - try: - return json.loads(result.stdout) - except json.JSONDecodeError: - return {"error": "Invalid JSON", "output": result.stdout} - return {} - - def try_claim_issue(self, issue_id: str) -> Tuple[bool, str]: - """ - Attempt to claim an issue. - - Returns: - (success: bool, message: str) - """ - # Integration Point 2: Reserve before claiming (if Agent Mail enabled) - if self.mail and self.mail.enabled: - reserved = self.mail.reserve_issue(issue_id) - if not reserved: - return False, f"Reservation failed for {issue_id}" - - # Claim the issue - result = self.run_bd("update", issue_id, "--status", "in_progress") - - if "error" in result: - if self.mail and self.mail.enabled: - self.mail.release_issue(issue_id) - return False, f"Update failed: {result['error']}" - - return True, f"Successfully claimed {issue_id}" - - def release_issue(self, issue_id: str): - """Release an issue after claiming.""" - if self.mail and self.mail.enabled: - self.mail.release_issue(issue_id) - - -def agent_worker(agent_name: str, workspace: str, target_issue_id: str, - mail_enabled: bool, result_queue: Queue): - """ - Worker function for multiprocessing. - - Each worker tries to claim the same issue. Result is put in queue. - """ - try: - agent = RaceTestAgent(agent_name, workspace, mail_enabled) - - # Small random delay to increase likelihood of collision - time.sleep(0.01 * hash(agent_name) % 10) - - success, message = agent.try_claim_issue(target_issue_id) - - result_queue.put({ - "agent": agent_name, - "success": success, - "message": message, - "mail_enabled": mail_enabled - }) - except Exception as e: - result_queue.put({ - "agent": agent_name, - "success": False, - "message": f"Exception: {str(e)}", - "mail_enabled": mail_enabled - }) - - -def run_race_test(num_agents: int, mail_enabled: bool) -> List[dict]: - """ - Run a race test with N agents trying to claim the same issue. - - Args: - num_agents: Number of agents to spawn - mail_enabled: Whether Agent Mail is enabled - - Returns: - List of result dicts from each agent - """ - # Create temporary workspace - workspace = tempfile.mkdtemp(prefix="bd-race-test-") - - try: - # Initialize bd in workspace - subprocess.run( - ["bd", "init", "--quiet", "--prefix", "test"], - cwd=workspace, - check=True, - capture_output=True - ) - - # Create a test issue - result = subprocess.run( - ["bd", "create", "Contested issue", "-p", "1", "--json"], - cwd=workspace, - capture_output=True, - text=True, - check=True - ) - issue_data = json.loads(result.stdout) - issue_id = issue_data["id"] - - # Spawn agents in parallel - result_queue = Queue() - processes = [] - - for i in range(num_agents): - agent_name = f"agent-{i+1}" - p = Process( - target=agent_worker, - args=(agent_name, workspace, issue_id, mail_enabled, result_queue) - ) - processes.append(p) - - # Start all processes simultaneously - start_time = time.time() - for p in processes: - p.start() - - # Wait for completion - for p in processes: - p.join(timeout=10) - - elapsed = time.time() - start_time - - # Collect results - results = [] - while not result_queue.empty(): - results.append(result_queue.get()) - - # Verify JSONL for duplicate claims - jsonl_path = Path(workspace) / ".beads" / "issues.jsonl" - jsonl_claims = verify_jsonl_claims(jsonl_path, issue_id) - - return { - "issue_id": issue_id, - "agents": results, - "elapsed_seconds": elapsed, - "jsonl_status_changes": jsonl_claims, - "mail_enabled": mail_enabled - } - - finally: - # Cleanup - shutil.rmtree(workspace, ignore_errors=True) - - -def verify_jsonl_claims(jsonl_path: Path, issue_id: str) -> List[dict]: - """ - Parse JSONL and count how many times the issue status was changed to in_progress. - - Returns list of status change events. - """ - if not jsonl_path.exists(): - return [] - - status_changes = [] - - with open(jsonl_path) as f: - for line in f: - if not line.strip(): - continue - - try: - record = json.loads(line) - if record.get("id") == issue_id and record.get("status") == "in_progress": - status_changes.append({ - "updated_at": record.get("updated_at"), - "assignee": record.get("assignee") - }) - except json.JSONDecodeError: - continue - - return status_changes - - -def test_agent_race_with_mail(): - """Test that WITH Agent Mail, only one agent succeeds.""" - print("\n" + "="*70) - print("TEST 1: Race condition WITH Agent Mail (collision prevention)") - print("="*70) - - num_agents = 3 - result = run_race_test(num_agents, mail_enabled=True) - - # Analyze results - successful_agents = [a for a in result["agents"] if a["success"]] - failed_agents = [a for a in result["agents"] if not a["success"]] - - print(f"\nšŸ“Š Results ({result['elapsed_seconds']:.3f}s):") - print(f" • Total agents: {num_agents}") - print(f" • Successful claims: {len(successful_agents)}") - print(f" • Failed claims: {len(failed_agents)}") - print(f" • JSONL status changes: {len(result['jsonl_status_changes'])}") - - for agent in result["agents"]: - status = "āœ…" if agent["success"] else "āŒ" - print(f" {status} {agent['agent']}: {agent['message']}") - - # Verify: Only one agent should succeed - assert len(successful_agents) == 1, \ - f"Expected 1 successful claim, got {len(successful_agents)}" - - # Verify: JSONL should have exactly 1 in_progress status - assert len(result['jsonl_status_changes']) == 1, \ - f"Expected 1 JSONL status change, got {len(result['jsonl_status_changes'])}" - - print("\nāœ… PASS: Agent Mail prevented duplicate claims") - return True - - -def test_agent_race_without_mail(): - """Test that WITHOUT Agent Mail, multiple agents may succeed (collision).""" - print("\n" + "="*70) - print("TEST 2: Race condition WITHOUT Agent Mail (collision demonstration)") - print("="*70) - print("āš ļø Note: This test may occasionally pass if timing prevents collision") - - num_agents = 3 - result = run_race_test(num_agents, mail_enabled=False) - - # Analyze results - successful_agents = [a for a in result["agents"] if a["success"]] - failed_agents = [a for a in result["agents"] if not a["success"]] - - print(f"\nšŸ“Š Results ({result['elapsed_seconds']:.3f}s):") - print(f" • Total agents: {num_agents}") - print(f" • Successful claims: {len(successful_agents)}") - print(f" • Failed claims: {len(failed_agents)}") - print(f" • JSONL status changes: {len(result['jsonl_status_changes'])}") - - for agent in result["agents"]: - status = "āœ…" if agent["success"] else "āŒ" - print(f" {status} {agent['agent']}: {agent['message']}") - - # Without Agent Mail, we expect potential for duplicates - # (though timing may occasionally prevent it) - if len(successful_agents) > 1: - print(f"\nāš ļø EXPECTED: Multiple agents ({len(successful_agents)}) claimed same issue") - print(" This demonstrates the collision problem Agent Mail prevents") - else: - print("\nāš ļø NOTE: Only one agent succeeded (timing prevented collision this run)") - print(" Without Agent Mail, collisions are possible but not guaranteed") - - return True - - -def test_agent_race_stress_test(): - """Stress test with many agents.""" - print("\n" + "="*70) - print("TEST 3: Stress test with 10 agents (Agent Mail enabled)") - print("="*70) - - num_agents = 10 - result = run_race_test(num_agents, mail_enabled=True) - - successful_agents = [a for a in result["agents"] if a["success"]] - - print(f"\nšŸ“Š Results ({result['elapsed_seconds']:.3f}s):") - print(f" • Total agents: {num_agents}") - print(f" • Successful claims: {len(successful_agents)}") - print(f" • JSONL status changes: {len(result['jsonl_status_changes'])}") - - # Verify: Exactly one winner - assert len(successful_agents) == 1, \ - f"Expected 1 successful claim, got {len(successful_agents)}" - assert len(result['jsonl_status_changes']) == 1, \ - f"Expected 1 JSONL status change, got {len(result['jsonl_status_changes'])}" - - print(f"\nāœ… PASS: Only {successful_agents[0]['agent']} succeeded") - return True - - -def check_agent_mail_server() -> bool: - """Check if Agent Mail server is running.""" - try: - import urllib.request - req = urllib.request.Request("http://localhost:8765/api/health") - with urllib.request.urlopen(req, timeout=1) as response: - return response.status == 200 - except: - return False - - -def main(): - """Run all race condition tests.""" - print("🧪 Multi-Agent Race Condition Test Suite") - print("Testing collision prevention with Agent Mail") - - try: - # Check if bd is available - subprocess.run(["bd", "--version"], capture_output=True, check=True) - except (subprocess.CalledProcessError, FileNotFoundError): - print("āŒ ERROR: bd command not found") - print(" Install: go install github.com/steveyegge/beads/cmd/bd@latest") - sys.exit(1) - - # Check if Agent Mail server is running - agent_mail_running = check_agent_mail_server() - if not agent_mail_running: - print("\nāš ļø WARNING: Agent Mail server is not running") - print(" Tests will fall back to beads-only mode (demonstrating collision)") - print("\n To enable full collision prevention testing:") - print(" $ cd ~/src/mcp_agent_mail") - print(" $ source .venv/bin/activate") - print(" $ uv run python -m mcp_agent_mail.cli serve-http") - print() - - # Check if running in non-interactive mode (CI/automation) - if not sys.stdin.isatty(): - print(" Running in non-interactive mode, continuing with tests...") - else: - print(" Press Enter to continue or Ctrl+C to exit") - try: - input() - except KeyboardInterrupt: - print("\n\nšŸ‘‹ Exiting - start Agent Mail server and try again") - sys.exit(0) - else: - print("\nāœ… Agent Mail server is running on http://localhost:8765") - - # Run tests - tests = [ - ("Agent Mail enabled (collision prevention)", test_agent_race_with_mail), - ("Agent Mail disabled (collision demonstration)", test_agent_race_without_mail), - ("Stress test (10 agents)", test_agent_race_stress_test), - ] - - passed = 0 - failed = 0 - - for name, test_func in tests: - try: - if test_func(): - passed += 1 - except AssertionError as e: - print(f"\nāŒ FAIL: {name}") - print(f" {e}") - failed += 1 - except Exception as e: - print(f"\nšŸ’„ ERROR in {name}: {e}") - failed += 1 - - # Summary - print("\n" + "="*70) - print("SUMMARY") - print("="*70) - print(f"āœ… Passed: {passed}/{len(tests)}") - print(f"āŒ Failed: {failed}/{len(tests)}") - - if failed == 0: - print("\nšŸŽ‰ All tests passed!") - sys.exit(0) - else: - print(f"\nāš ļø {failed} test(s) failed") - sys.exit(1) - - -if __name__ == "__main__": - main() diff --git a/tests/integration/test_mail_failures.py b/tests/integration/test_mail_failures.py deleted file mode 100644 index f8909341..00000000 --- a/tests/integration/test_mail_failures.py +++ /dev/null @@ -1,797 +0,0 @@ -#!/usr/bin/env python3 -""" -Agent Mail Server Failure Scenarios Test Suite - -Tests verify graceful degradation across various failure modes: -- Server never started (connection refused) -- Server crashes during operation (connection reset) -- Network partition (timeout) -- Server returns 500 errors -- Invalid bearer token (401/403) -- Malformed responses - -Validates: -- Agents continue working in Beads-only mode -- Clear log messages about degradation -- No crashes or data loss -- JSONL remains consistent - -Performance notes: -- Uses 1s HTTP timeouts for fast failure detection -- Uses --no-daemon flag to avoid 5s debounce delays -- Mock HTTP server with minimal overhead -- Each test ~2-5s (much faster without daemon) -- Full suite ~15-30s (7 tests with workspace setup) -""" - -import json -import subprocess -import tempfile -import shutil -import os -import sys -import time -import logging -from pathlib import Path -from http.server import HTTPServer, BaseHTTPRequestHandler -from threading import Thread -from typing import Optional, Dict, Any, List -import socket - -# Add lib directory for beads_mail_adapter -lib_path = Path(__file__).parent.parent.parent / "lib" -sys.path.insert(0, str(lib_path)) - -from beads_mail_adapter import AgentMailAdapter - -# Configure logging (WARNING to reduce noise) -logging.basicConfig( - level=logging.WARNING, - format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' -) -logger = logging.getLogger(__name__) - -# Fast timeout for tests (1s instead of default 5s) -TEST_TIMEOUT = 1 - - -class MockAgentMailServer: - """Mock Agent Mail server for testing various failure scenarios.""" - - def __init__(self, port: int = 0, failure_mode: Optional[str] = None): - """ - Initialize mock server. - - Args: - port: Port to listen on (0 = auto-assign) - failure_mode: Type of failure to simulate: - - None: Normal operation - - "500_error": Always return 500 - - "timeout": Hang requests indefinitely - - "invalid_json": Return malformed JSON - - "crash_after_health": Crash after first health check - """ - self.port = port - self.failure_mode = failure_mode - self.server: Optional[HTTPServer] = None - self.thread: Optional[Thread] = None - self.request_count = 0 - self.crash_triggered = False - - def start(self) -> int: - """Start the mock server. Returns actual port number.""" - handler_class = self._create_handler() - - # Find available port if port=0 - if self.port == 0: - with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: - s.bind(('', 0)) - s.listen(1) - self.port = s.getsockname()[1] - - self.server = HTTPServer(('127.0.0.1', self.port), handler_class) - self.thread = Thread(target=self.server.serve_forever, daemon=True) - self.thread.start() - - # Wait for server to be ready - time.sleep(0.1) - - logger.info(f"Mock Agent Mail server started on port {self.port} (mode={self.failure_mode})") - return self.port - - def stop(self): - """Stop the mock server.""" - if self.server: - self.server.shutdown() - self.server.server_close() - logger.info(f"Mock Agent Mail server stopped (handled {self.request_count} requests)") - - def crash(self): - """Simulate server crash.""" - self.crash_triggered = True - self.stop() - logger.info("Mock Agent Mail server CRASHED") - - def _create_handler(self): - """Create request handler class with access to server state.""" - parent = self - - class MockHandler(BaseHTTPRequestHandler): - def log_message(self, format, *args): - """Suppress default logging.""" - pass - - def do_GET(self): - parent.request_count += 1 - - # Handle crash_after_health mode - if parent.failure_mode == "crash_after_health" and parent.request_count > 1: - parent.crash() - return - - # Handle timeout mode (hang long enough to trigger timeout) - if parent.failure_mode == "timeout": - time.sleep(10) # Hang longer than test timeout - return - - # Handle 500 error mode - if parent.failure_mode == "500_error": - self.send_response(500) - self.send_header('Content-Type', 'application/json') - self.end_headers() - self.wfile.write(json.dumps({"error": "Internal server error"}).encode()) - return - - # Normal health check response - if self.path == "/api/health": - response = {"status": "ok"} - if parent.failure_mode == "invalid_json": - # Return malformed JSON - self.send_response(200) - self.send_header('Content-Type', 'application/json') - self.end_headers() - self.wfile.write(b'{invalid json') - return - - self.send_response(200) - self.send_header('Content-Type', 'application/json') - self.end_headers() - self.wfile.write(json.dumps(response).encode()) - else: - self.send_response(404) - self.end_headers() - - def do_POST(self): - parent.request_count += 1 - - # Read request body - content_length = int(self.headers.get('Content-Length', 0)) - if content_length > 0: - body = self.rfile.read(content_length) - - # Check authorization for invalid_token mode - if parent.failure_mode == "invalid_token": - auth = self.headers.get('Authorization', '') - if not auth or auth != "Bearer valid_token": - self.send_response(401) - self.send_header('Content-Type', 'application/json') - self.end_headers() - self.wfile.write(json.dumps({"error": "Invalid token"}).encode()) - return - - # Handle timeout mode (hang long enough to trigger timeout) - if parent.failure_mode == "timeout": - time.sleep(10) # Hang longer than test timeout - return - - # Handle 500 error mode - if parent.failure_mode == "500_error": - self.send_response(500) - self.send_header('Content-Type', 'application/json') - self.end_headers() - self.wfile.write(json.dumps({"error": "Internal server error"}).encode()) - return - - # Normal responses for reservations/notifications - if self.path == "/api/reservations": - self.send_response(201) - self.send_header('Content-Type', 'application/json') - self.end_headers() - self.wfile.write(json.dumps({"status": "reserved"}).encode()) - elif self.path == "/api/notifications": - self.send_response(201) - self.send_header('Content-Type', 'application/json') - self.end_headers() - self.wfile.write(json.dumps({"status": "sent"}).encode()) - else: - self.send_response(404) - self.end_headers() - - def do_DELETE(self): - parent.request_count += 1 - - # Handle timeout mode (hang long enough to trigger timeout) - if parent.failure_mode == "timeout": - time.sleep(10) # Hang longer than test timeout - return - - # Normal release response - self.send_response(204) - self.end_headers() - - return MockHandler - - -class TestAgent: - """Test agent that performs basic bd operations.""" - - def __init__(self, workspace: str, agent_name: str = "test-agent", - mail_url: Optional[str] = None, mail_token: Optional[str] = None): - self.workspace = workspace - self.agent_name = agent_name - self.mail_url = mail_url - self.mail_token = mail_token - - # Initialize adapter if URL provided - if mail_url: - self.mail = AgentMailAdapter( - url=mail_url, - token=mail_token, - agent_name=agent_name, - timeout=TEST_TIMEOUT # Use global test timeout - ) - else: - self.mail = None - - def run_bd(self, *args) -> dict: - """Run bd command and return JSON output.""" - # Use --no-daemon for fast tests (avoid 5s debounce timer) - cmd = ["bd", "--no-daemon"] + list(args) + ["--json"] - result = subprocess.run( - cmd, - cwd=self.workspace, - capture_output=True, - text=True - ) - - if result.returncode != 0: - return {"error": result.stderr} - - if result.stdout.strip(): - try: - return json.loads(result.stdout) - except json.JSONDecodeError: - return {"error": "Invalid JSON", "output": result.stdout} - return {} - - def create_issue(self, title: str, priority: int = 1) -> Optional[str]: - """Create an issue and return its ID.""" - result = self.run_bd("create", title, "-p", str(priority)) - if "error" in result: - logger.error(f"Failed to create issue: {result['error']}") - return None - return result.get("id") - - def claim_issue(self, issue_id: str) -> bool: - """Attempt to claim an issue (with optional reservation).""" - # Try to reserve if Agent Mail is enabled - if self.mail and self.mail.enabled: - reserved = self.mail.reserve_issue(issue_id) - if not reserved: - logger.warning(f"Failed to reserve {issue_id}") - return False - - # Update status - result = self.run_bd("update", issue_id, "--status", "in_progress") - - if "error" in result: - logger.error(f"Failed to claim {issue_id}: {result['error']}") - if self.mail and self.mail.enabled: - self.mail.release_issue(issue_id) - return False - - return True - - def complete_issue(self, issue_id: str) -> bool: - """Complete an issue.""" - result = self.run_bd("close", issue_id, "--reason", "Done") - - if "error" in result: - logger.error(f"Failed to complete {issue_id}: {result['error']}") - return False - - # Release reservation if Agent Mail enabled - if self.mail and self.mail.enabled: - self.mail.release_issue(issue_id) - - return True - - -def verify_jsonl_consistency(workspace: str) -> Dict[str, Any]: - """ - Verify JSONL file is valid and consistent. - - Returns dict with: - - valid: bool - - issue_count: int - - errors: list of error messages - """ - jsonl_path = Path(workspace) / ".beads" / "issues.jsonl" - - if not jsonl_path.exists(): - return {"valid": False, "issue_count": 0, "errors": ["JSONL file does not exist"]} - - issues = {} - errors = [] - - try: - with open(jsonl_path) as f: - for line_num, line in enumerate(f, 1): - if not line.strip(): - continue - - try: - record = json.loads(line) - issue_id = record.get("id") - if not issue_id: - errors.append(f"Line {line_num}: Missing issue ID") - continue - - issues[issue_id] = record - except json.JSONDecodeError as e: - errors.append(f"Line {line_num}: Invalid JSON - {e}") - except Exception as e: - errors.append(f"Failed to read JSONL: {e}") - return {"valid": False, "issue_count": 0, "errors": errors} - - return { - "valid": len(errors) == 0, - "issue_count": len(issues), - "errors": errors - } - - -def test_server_never_started(): - """Test that agents work when Agent Mail server is not running.""" - print("\n" + "="*70) - print("TEST 1: Server Never Started (Connection Refused)") - print("="*70) - - test_start = time.time() - - workspace = tempfile.mkdtemp(prefix="bd-test-noserver-") - - try: - # Initialize workspace - subprocess.run( - ["bd", "init", "--quiet", "--prefix", "test"], - cwd=workspace, - check=True, - capture_output=True - ) - - # Create agent with non-existent server - agent = TestAgent(workspace, "test-agent", mail_url="http://127.0.0.1:9999") - - # Verify Agent Mail is disabled - assert agent.mail is not None, "Agent Mail adapter should exist" - assert not agent.mail.enabled, "Agent Mail should be disabled (server not running)" - - # Perform normal operations - issue_id = agent.create_issue("Test issue when server down") - assert issue_id is not None, "Should create issue without Agent Mail" - - claimed = agent.claim_issue(issue_id) - assert claimed, "Should claim issue without Agent Mail" - - completed = agent.complete_issue(issue_id) - assert completed, "Should complete issue without Agent Mail" - - # Verify JSONL consistency - jsonl_check = verify_jsonl_consistency(workspace) - assert jsonl_check["valid"], f"JSONL should be valid: {jsonl_check['errors']}" - assert jsonl_check["issue_count"] == 1, "Should have 1 issue in JSONL" - - test_elapsed = time.time() - test_start - print("āœ… PASS: Agent worked correctly without server") - print(f" • Created, claimed, and completed issue: {issue_id}") - print(f" • JSONL valid with {jsonl_check['issue_count']} issue(s)") - print(f" • Test duration: {test_elapsed:.2f}s") - return True - - finally: - shutil.rmtree(workspace, ignore_errors=True) - - -def test_server_crash_during_operation(): - """Test that agents handle server crash gracefully.""" - print("\n" + "="*70) - print("TEST 2: Server Crashes During Operation") - print("="*70) - - workspace = tempfile.mkdtemp(prefix="bd-test-crash-") - server = MockAgentMailServer(failure_mode="crash_after_health") - - try: - # Initialize workspace - subprocess.run( - ["bd", "init", "--quiet", "--prefix", "test"], - cwd=workspace, - check=True, - capture_output=True - ) - - # Start server - port = server.start() - mail_url = f"http://127.0.0.1:{port}" - - # Create agent - agent = TestAgent(workspace, "test-agent", mail_url=mail_url) - - # Verify Agent Mail is initially enabled - assert agent.mail.enabled, "Agent Mail should be enabled initially" - - # Create issue (triggers health check, count=1) - issue_id = agent.create_issue("Test issue before crash") - assert issue_id is not None, "Should create issue before crash" - - # Server will crash on next request (count=2) - # Agent should handle gracefully and continue in Beads-only mode - claimed = agent.claim_issue(issue_id) - assert claimed, "Should claim issue even after server crash" - - completed = agent.complete_issue(issue_id) - assert completed, "Should complete issue after server crash" - - # Verify JSONL consistency - jsonl_check = verify_jsonl_consistency(workspace) - assert jsonl_check["valid"], f"JSONL should be valid: {jsonl_check['errors']}" - - print("āœ… PASS: Agent handled server crash gracefully") - print(f" • Server crashed after request #{server.request_count}") - print(f" • Agent continued in Beads-only mode") - print(f" • JSONL valid with {jsonl_check['issue_count']} issue(s)") - return True - - finally: - server.stop() - shutil.rmtree(workspace, ignore_errors=True) - - -def test_network_partition_timeout(): - """Test that agents handle network timeouts without blocking indefinitely.""" - print("\n" + "="*70) - print("TEST 3: Network Partition (Timeout)") - print("="*70) - - workspace = tempfile.mkdtemp(prefix="bd-test-timeout-") - server = MockAgentMailServer(failure_mode="timeout") - - try: - # Initialize workspace - subprocess.run( - ["bd", "init", "--quiet", "--prefix", "test"], - cwd=workspace, - check=True, - capture_output=True - ) - - # Start server (will hang all requests) - port = server.start() - mail_url = f"http://127.0.0.1:{port}" - - # Measure how long initialization takes (includes health check timeout) - init_start = time.time() - - # Create agent with short timeout (2s set in TestAgent) - agent = TestAgent(workspace, "test-agent", mail_url=mail_url) - - init_elapsed = time.time() - init_start - - # Agent Mail should be disabled after health check timeout - # The health check itself will take ~2s to timeout - assert not agent.mail.enabled, "Agent Mail should be disabled (health check timeout)" - - # Operations should proceed quickly in Beads-only mode (no more server calls) - ops_start = time.time() - issue_id = agent.create_issue("Test issue with timeout") - claimed = agent.claim_issue(issue_id) - ops_elapsed = time.time() - ops_start - - # Operations should be fast (not waiting on server) - allow up to 15s for bd commands - assert ops_elapsed < 15, f"Operations took too long: {ops_elapsed:.2f}s (should be quick in Beads-only mode)" - assert issue_id is not None, "Should create issue despite timeout" - assert claimed, "Should claim issue despite timeout" - - # Verify JSONL consistency - jsonl_check = verify_jsonl_consistency(workspace) - assert jsonl_check["valid"], f"JSONL should be valid: {jsonl_check['errors']}" - - print("āœ… PASS: Agent handled network timeout gracefully") - print(f" • Health check timeout: {init_elapsed:.2f}s") - print(f" • Operations completed in {ops_elapsed:.2f}s (Beads-only mode)") - print(f" • JSONL valid with {jsonl_check['issue_count']} issue(s)") - return True - - finally: - server.stop() - shutil.rmtree(workspace, ignore_errors=True) - - -def test_server_500_errors(): - """Test that agents handle 500 errors gracefully.""" - print("\n" + "="*70) - print("TEST 4: Server Returns 500 Errors") - print("="*70) - - workspace = tempfile.mkdtemp(prefix="bd-test-500-") - server = MockAgentMailServer(failure_mode="500_error") - - try: - # Initialize workspace - subprocess.run( - ["bd", "init", "--quiet", "--prefix", "test"], - cwd=workspace, - check=True, - capture_output=True - ) - - # Start server (returns 500 for all requests) - port = server.start() - mail_url = f"http://127.0.0.1:{port}" - - # Create agent - agent = TestAgent(workspace, "test-agent", mail_url=mail_url) - - # Agent Mail should be disabled (health check returns 500) - assert not agent.mail.enabled, "Agent Mail should be disabled (500 error)" - - # Operations should work in Beads-only mode - issue_id = agent.create_issue("Test issue with 500 errors") - assert issue_id is not None, "Should create issue despite 500 errors" - - claimed = agent.claim_issue(issue_id) - assert claimed, "Should claim issue despite 500 errors" - - # Verify JSONL consistency - jsonl_check = verify_jsonl_consistency(workspace) - assert jsonl_check["valid"], f"JSONL should be valid: {jsonl_check['errors']}" - - print("āœ… PASS: Agent handled 500 errors gracefully") - print(f" • Server returned {server.request_count} 500 errors") - print(f" • JSONL valid with {jsonl_check['issue_count']} issue(s)") - return True - - finally: - server.stop() - shutil.rmtree(workspace, ignore_errors=True) - - -def test_invalid_bearer_token(): - """Test that agents handle invalid bearer token (401) gracefully.""" - print("\n" + "="*70) - print("TEST 5: Invalid Bearer Token (401)") - print("="*70) - - workspace = tempfile.mkdtemp(prefix="bd-test-token-") - server = MockAgentMailServer(failure_mode="invalid_token") - - try: - # Initialize workspace - subprocess.run( - ["bd", "init", "--quiet", "--prefix", "test"], - cwd=workspace, - check=True, - capture_output=True - ) - - # Start server (requires "Bearer valid_token") - port = server.start() - mail_url = f"http://127.0.0.1:{port}" - - # Create agent with invalid token - agent = TestAgent(workspace, "test-agent", mail_url=mail_url, mail_token="invalid_token") - - # Note: The health check endpoint doesn't require auth in our mock server, - # so Agent Mail may be enabled initially. However, reservation requests - # will fail with 401, causing graceful degradation. - # This tests that the adapter handles auth failures during actual operations. - - # Operations should work (graceful degradation on auth failure) - issue_id = agent.create_issue("Test issue with invalid token") - assert issue_id is not None, "Should create issue despite auth issues" - - claimed = agent.claim_issue(issue_id) - assert claimed, "Should claim issue (reservation may fail but claim succeeds)" - - # Verify JSONL consistency - jsonl_check = verify_jsonl_consistency(workspace) - assert jsonl_check["valid"], f"JSONL should be valid: {jsonl_check['errors']}" - - print("āœ… PASS: Agent handled invalid token gracefully") - print(f" • Server requests: {server.request_count}") - print(f" • Agent Mail enabled: {agent.mail.enabled}") - print(f" • Operations succeeded via graceful degradation") - print(f" • JSONL valid with {jsonl_check['issue_count']} issue(s)") - return True - - finally: - server.stop() - shutil.rmtree(workspace, ignore_errors=True) - - -def test_malformed_json_response(): - """Test that agents handle malformed JSON responses gracefully.""" - print("\n" + "="*70) - print("TEST 6: Malformed JSON Response") - print("="*70) - - workspace = tempfile.mkdtemp(prefix="bd-test-badjson-") - server = MockAgentMailServer(failure_mode="invalid_json") - - try: - # Initialize workspace - subprocess.run( - ["bd", "init", "--quiet", "--prefix", "test"], - cwd=workspace, - check=True, - capture_output=True - ) - - # Start server (returns malformed JSON) - port = server.start() - mail_url = f"http://127.0.0.1:{port}" - - # Create agent - agent = TestAgent(workspace, "test-agent", mail_url=mail_url) - - # Agent Mail should be disabled (malformed health check response) - assert not agent.mail.enabled, "Agent Mail should be disabled (invalid JSON)" - - # Operations should work in Beads-only mode - issue_id = agent.create_issue("Test issue with malformed JSON") - assert issue_id is not None, "Should create issue despite malformed JSON" - - claimed = agent.claim_issue(issue_id) - assert claimed, "Should claim issue despite malformed JSON" - - # Verify JSONL consistency - jsonl_check = verify_jsonl_consistency(workspace) - assert jsonl_check["valid"], f"JSONL should be valid: {jsonl_check['errors']}" - - print("āœ… PASS: Agent handled malformed JSON gracefully") - print(f" • JSONL valid with {jsonl_check['issue_count']} issue(s)") - return True - - finally: - server.stop() - shutil.rmtree(workspace, ignore_errors=True) - - -def test_jsonl_consistency_under_failures(): - """Test JSONL remains consistent across multiple failure scenarios.""" - print("\n" + "="*70) - print("TEST 7: JSONL Consistency Under Multiple Failures") - print("="*70) - - workspace = tempfile.mkdtemp(prefix="bd-test-consistency-") - - try: - # Initialize workspace - subprocess.run( - ["bd", "init", "--quiet", "--prefix", "test"], - cwd=workspace, - check=True, - capture_output=True - ) - - # Scenario 1: No server - agent1 = TestAgent(workspace, "agent1", mail_url="http://127.0.0.1:9999") - id1 = agent1.create_issue("Issue 1 - no server") - agent1.claim_issue(id1) - - # Scenario 2: Server crash - server2 = MockAgentMailServer(failure_mode="crash_after_health") - port2 = server2.start() - agent2 = TestAgent(workspace, "agent2", mail_url=f"http://127.0.0.1:{port2}") - id2 = agent2.create_issue("Issue 2 - server crash") - agent2.claim_issue(id2) # Triggers crash - server2.stop() - - # Scenario 3: 500 errors - server3 = MockAgentMailServer(failure_mode="500_error") - port3 = server3.start() - agent3 = TestAgent(workspace, "agent3", mail_url=f"http://127.0.0.1:{port3}") - id3 = agent3.create_issue("Issue 3 - 500 errors") - agent3.claim_issue(id3) - server3.stop() - - # Verify JSONL is still consistent - jsonl_check = verify_jsonl_consistency(workspace) - assert jsonl_check["valid"], f"JSONL should be valid: {jsonl_check['errors']}" - assert jsonl_check["issue_count"] == 3, f"Expected 3 issues, got {jsonl_check['issue_count']}" - - # Verify we can still read issues with bd - result = subprocess.run( - ["bd", "list", "--json"], - cwd=workspace, - capture_output=True, - text=True, - check=True - ) - issues = json.loads(result.stdout) - assert len(issues) == 3, f"Expected 3 issues from bd list, got {len(issues)}" - - print("āœ… PASS: JSONL remained consistent across all failure scenarios") - print(f" • Created 3 issues across 3 different failure modes") - print(f" • JSONL valid with {jsonl_check['issue_count']} issues") - print(f" • All issues readable via bd CLI") - return True - - finally: - shutil.rmtree(workspace, ignore_errors=True) - - -def main(): - """Run all failure scenario tests.""" - print("🧪 Agent Mail Server Failure Scenarios Test Suite") - print("Testing graceful degradation across various failure modes") - - # Check if bd is available - try: - subprocess.run(["bd", "--version"], capture_output=True, check=True) - except (subprocess.CalledProcessError, FileNotFoundError): - print("āŒ ERROR: bd command not found") - print(" Install: go install github.com/steveyegge/beads/cmd/bd@latest") - sys.exit(1) - - # Run tests - tests = [ - ("Server never started", test_server_never_started), - ("Server crash during operation", test_server_crash_during_operation), - ("Network partition timeout", test_network_partition_timeout), - ("Server 500 errors", test_server_500_errors), - ("Invalid bearer token", test_invalid_bearer_token), - ("Malformed JSON response", test_malformed_json_response), - ("JSONL consistency under failures", test_jsonl_consistency_under_failures), - ] - - passed = 0 - failed = 0 - start_time = time.time() - - for name, test_func in tests: - try: - if test_func(): - passed += 1 - except AssertionError as e: - print(f"\nāŒ FAIL: {name}") - print(f" {e}") - failed += 1 - except Exception as e: - print(f"\nšŸ’„ ERROR in {name}: {e}") - import traceback - traceback.print_exc() - failed += 1 - - elapsed = time.time() - start_time - - # Summary - print("\n" + "="*70) - print("SUMMARY") - print("="*70) - print(f"āœ… Passed: {passed}/{len(tests)}") - print(f"āŒ Failed: {failed}/{len(tests)}") - print(f"ā±ļø Total time: {elapsed:.2f}s") - - if failed == 0: - print("\nšŸŽ‰ All failure scenario tests passed!") - print(" Agents gracefully degrade to Beads-only mode in all failure cases") - sys.exit(0) - else: - print(f"\nāš ļø {failed} test(s) failed") - sys.exit(1) - - -if __name__ == "__main__": - main() diff --git a/tests/integration/test_multi_agent_coordination.py b/tests/integration/test_multi_agent_coordination.py deleted file mode 100755 index 5234d786..00000000 --- a/tests/integration/test_multi_agent_coordination.py +++ /dev/null @@ -1,510 +0,0 @@ -#!/usr/bin/env python3 -""" -Multi-Agent Coordination Test Suite - -Fast tests (<30s total) covering critical multi-agent scenarios: -- Fairness: N agents claiming M issues -- Notifications: End-to-end message passing -- Handoff: Release → immediate claim by another agent -- Idempotency: Double operations by same agent -""" - -import json -import subprocess -import tempfile -import shutil -import sys -import time -from pathlib import Path -from multiprocessing import Process, Queue -from threading import Thread, Lock -from http.server import HTTPServer, BaseHTTPRequestHandler -import socket - -# Add lib directory for beads_mail_adapter -lib_path = Path(__file__).parent.parent.parent / "lib" -sys.path.insert(0, str(lib_path)) - -from beads_mail_adapter import AgentMailAdapter - - -class MockAgentMailServer: - """Lightweight mock server with reservations and notifications.""" - - def __init__(self, port: int = 0): - self.port = port - self.server = None - self.thread = None - self.reservations = {} # file_path -> agent_name - self.notifications = {} # agent_name -> [messages] - self.lock = Lock() - - def start(self) -> int: - """Start server and return port.""" - handler = self._create_handler() - - if self.port == 0: - with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: - s.bind(('', 0)) - s.listen(1) - self.port = s.getsockname()[1] - - self.server = HTTPServer(('127.0.0.1', self.port), handler) - self.thread = Thread(target=self.server.serve_forever, daemon=True) - self.thread.start() - time.sleep(0.1) - return self.port - - def stop(self): - if self.server: - self.server.shutdown() - self.server.server_close() - - def _create_handler(self): - parent = self - - class Handler(BaseHTTPRequestHandler): - def log_message(self, *args): - pass - - def do_GET(self): - if self.path == "/api/health": - self.send_response(200) - self.send_header('Content-Type', 'application/json') - self.end_headers() - self.wfile.write(b'{"status": "ok"}') - - # Get inbox: /api/notifications/{agent_name} - elif self.path.startswith("/api/notifications/"): - agent_name = self.path.split('/')[-1] - with parent.lock: - messages = parent.notifications.get(agent_name, []) - parent.notifications[agent_name] = [] # Clear after read - - self.send_response(200) - self.send_header('Content-Type', 'application/json') - self.end_headers() - self.wfile.write(json.dumps(messages).encode()) - - elif self.path == "/api/reservations": - with parent.lock: - res_list = [ - {"file_path": fp, "agent_name": agent} - for fp, agent in parent.reservations.items() - ] - - self.send_response(200) - self.send_header('Content-Type', 'application/json') - self.end_headers() - self.wfile.write(json.dumps(res_list).encode()) - - else: - self.send_response(404) - self.end_headers() - - def do_POST(self): - content_length = int(self.headers.get('Content-Length', 0)) - body = self.rfile.read(content_length) if content_length > 0 else b'{}' - data = json.loads(body.decode('utf-8')) - - # Reserve: /api/reservations - if self.path == "/api/reservations": - file_path = data.get("file_path") - agent_name = data.get("agent_name") - - with parent.lock: - if file_path in parent.reservations: - existing = parent.reservations[file_path] - if existing != agent_name: - # Conflict - self.send_response(409) - self.send_header('Content-Type', 'application/json') - self.end_headers() - self.wfile.write(json.dumps({ - "error": f"Already reserved by {existing}" - }).encode()) - return - # else: same agent re-reserving (idempotent) - - parent.reservations[file_path] = agent_name - - self.send_response(201) - self.send_header('Content-Type', 'application/json') - self.end_headers() - self.wfile.write(b'{"status": "reserved"}') - - # Notify: /api/notifications - elif self.path == "/api/notifications": - from_agent = data.get("from_agent") - event_type = data.get("event_type") - payload = data.get("payload", {}) - - # Broadcast to all OTHER agents - with parent.lock: - for agent_name in list(parent.notifications.keys()): - if agent_name != from_agent: - parent.notifications[agent_name].append({ - "from": from_agent, - "event": event_type, - "data": payload - }) - - # If target agent specified, ensure they get it - to_agent = payload.get("to_agent") - if to_agent and to_agent not in parent.notifications: - parent.notifications[to_agent] = [{ - "from": from_agent, - "event": event_type, - "data": payload - }] - - self.send_response(201) - self.send_header('Content-Type', 'application/json') - self.end_headers() - self.wfile.write(b'{"status": "sent"}') - - else: - self.send_response(404) - self.end_headers() - - def do_DELETE(self): - # Release: /api/reservations/{agent}/{issue_id} - parts = self.path.split('/') - if len(parts) >= 5: - agent_name = parts[3] - issue_id = parts[4] - file_path = f".beads/issues/{issue_id}" - - with parent.lock: - if file_path in parent.reservations: - if parent.reservations[file_path] == agent_name: - del parent.reservations[file_path] - - self.send_response(204) - self.end_headers() - else: - self.send_response(404) - self.end_headers() - - return Handler - - -class TestAgent: - """Minimal test agent.""" - - def __init__(self, workspace: str, agent_name: str, mail_url: str): - self.workspace = workspace - self.agent_name = agent_name - self.mail = AgentMailAdapter(url=mail_url, agent_name=agent_name, timeout=2) - - def run_bd(self, *args): - cmd = ["bd", "--no-daemon"] + list(args) + ["--json"] - result = subprocess.run(cmd, cwd=self.workspace, capture_output=True, text=True) - if result.returncode != 0: - return {"error": result.stderr} - if result.stdout.strip(): - try: - return json.loads(result.stdout) - except: - return {"error": "Invalid JSON"} - return {} - - def create_issue(self, title: str) -> str: - result = self.run_bd("create", title, "-p", "1") - return result.get("id") - - def claim_issue(self, issue_id: str) -> bool: - if self.mail.enabled and not self.mail.reserve_issue(issue_id): - return False - result = self.run_bd("update", issue_id, "--status", "in_progress") - return "error" not in result - - def release_issue(self, issue_id: str): - if self.mail.enabled: - self.mail.release_issue(issue_id) - - -def agent_claim_worker(agent_name: str, workspace: str, issue_id: str, - mail_url: str, result_queue: Queue): - """Worker that tries to claim a single issue.""" - try: - agent = TestAgent(workspace, agent_name, mail_url) - success = agent.claim_issue(issue_id) - result_queue.put({"agent": agent_name, "issue": issue_id, "success": success}) - except Exception as e: - result_queue.put({"agent": agent_name, "issue": issue_id, "success": False, "error": str(e)}) - - -def test_fairness_n_agents_m_issues(): - """Test that N agents competing for M issues results in exactly M claims.""" - print("\n" + "="*70) - print("TEST 1: Fairness - 10 agents, 5 issues") - print("="*70) - - workspace = tempfile.mkdtemp(prefix="bd-test-fairness-") - server = MockAgentMailServer() - - try: - subprocess.run(["bd", "init", "--quiet", "--prefix", "test"], - cwd=workspace, check=True, capture_output=True) - - port = server.start() - mail_url = f"http://127.0.0.1:{port}" - - # Create 5 issues - agent = TestAgent(workspace, "setup", mail_url) - issues = [agent.create_issue(f"Issue {i+1}") for i in range(5)] - - # Spawn 10 agents trying to claim all 5 issues - result_queue = Queue() - processes = [] - - for agent_num in range(10): - for issue_id in issues: - p = Process(target=agent_claim_worker, - args=(f"agent-{agent_num}", workspace, issue_id, mail_url, result_queue)) - processes.append(p) - - # Start all at once - for p in processes: - p.start() - - for p in processes: - p.join(timeout=10) - - # Collect results - results = [] - while not result_queue.empty(): - results.append(result_queue.get()) - - # Count successful claims per issue - claims_per_issue = {} - for r in results: - if r["success"]: - issue = r["issue"] - claims_per_issue[issue] = claims_per_issue.get(issue, 0) + 1 - - print(f" • Total attempts: {len(results)}") - print(f" • Successful claims: {sum(claims_per_issue.values())}") - print(f" • Claims per issue: {claims_per_issue}") - - # Verify exactly 1 claim per issue - for issue_id in issues: - claims = claims_per_issue.get(issue_id, 0) - assert claims == 1, f"Issue {issue_id} claimed {claims} times (expected 1)" - - print("āœ… PASS: Each issue claimed exactly once") - return True - - finally: - server.stop() - shutil.rmtree(workspace, ignore_errors=True) - - -def test_notification_end_to_end(): - """Test notifications from agent1 to agent2.""" - print("\n" + "="*70) - print("TEST 2: Notification End-to-End") - print("="*70) - - workspace = tempfile.mkdtemp(prefix="bd-test-notify-") - server = MockAgentMailServer() - - try: - subprocess.run(["bd", "init", "--quiet", "--prefix", "test"], - cwd=workspace, check=True, capture_output=True) - - port = server.start() - mail_url = f"http://127.0.0.1:{port}" - - # Create two agents - agent1 = TestAgent(workspace, "agent1", mail_url) - agent2 = TestAgent(workspace, "agent2", mail_url) - - # Register agent2's inbox - server.notifications["agent2"] = [] - - # Agent1 sends notification - sent = agent1.mail.notify("task_completed", { - "issue_id": "bd-123", - "status": "done", - "to_agent": "agent2" - }) - - assert sent, "Should send notification" - - # Agent2 checks inbox - messages = agent2.mail.check_inbox() - - print(f" • Agent1 sent notification") - print(f" • Agent2 received {len(messages)} message(s)") - - assert len(messages) == 1, f"Expected 1 message, got {len(messages)}" - assert messages[0]["from"] == "agent1" - assert messages[0]["event"] == "task_completed" - assert messages[0]["data"]["issue_id"] == "bd-123" - - # Second check should be empty (messages consumed) - messages2 = agent2.mail.check_inbox() - assert len(messages2) == 0, "Inbox should be empty after read" - - print("āœ… PASS: Notification delivered correctly") - return True - - finally: - server.stop() - shutil.rmtree(workspace, ignore_errors=True) - - -def test_reservation_handoff(): - """Test immediate claim after release (handoff scenario).""" - print("\n" + "="*70) - print("TEST 3: Reservation Handoff") - print("="*70) - - workspace = tempfile.mkdtemp(prefix="bd-test-handoff-") - server = MockAgentMailServer() - - try: - subprocess.run(["bd", "init", "--quiet", "--prefix", "test"], - cwd=workspace, check=True, capture_output=True) - - port = server.start() - mail_url = f"http://127.0.0.1:{port}" - - agent1 = TestAgent(workspace, "agent1", mail_url) - agent2 = TestAgent(workspace, "agent2", mail_url) - - # Agent1 creates and claims issue - issue_id = agent1.create_issue("Handoff test") - claimed1 = agent1.claim_issue(issue_id) - assert claimed1, "Agent1 should claim issue" - - # Agent2 tries to claim (should fail - reserved) - claimed2_before = agent2.claim_issue(issue_id) - assert not claimed2_before, "Agent2 should be blocked" - - # Agent1 releases - agent1.release_issue(issue_id) - - # Agent2 immediately claims (handoff) - claimed2_after = agent2.claim_issue(issue_id) - assert claimed2_after, "Agent2 should claim after release" - - # Verify reservation ownership - reservations = agent2.mail.get_reservations() - assert len(reservations) == 1 - assert reservations[0]["agent_name"] == "agent2" - - print("āœ… PASS: Clean handoff from agent1 to agent2") - return True - - finally: - server.stop() - shutil.rmtree(workspace, ignore_errors=True) - - -def test_idempotent_operations(): - """Test double reserve and double release by same agent.""" - print("\n" + "="*70) - print("TEST 4: Idempotent Operations") - print("="*70) - - workspace = tempfile.mkdtemp(prefix="bd-test-idem-") - server = MockAgentMailServer() - - try: - subprocess.run(["bd", "init", "--quiet", "--prefix", "test"], - cwd=workspace, check=True, capture_output=True) - - port = server.start() - mail_url = f"http://127.0.0.1:{port}" - - agent = TestAgent(workspace, "agent1", mail_url) - issue_id = agent.create_issue("Idempotency test") - - # Reserve twice (idempotent) - reserve1 = agent.mail.reserve_issue(issue_id) - reserve2 = agent.mail.reserve_issue(issue_id) - - assert reserve1, "First reserve should succeed" - assert reserve2, "Second reserve should be idempotent (same agent)" - - # Verify only one reservation - reservations = agent.mail.get_reservations() - assert len(reservations) == 1, f"Should have 1 reservation, got {len(reservations)}" - - # Release twice (idempotent) - release1 = agent.mail.release_issue(issue_id) - release2 = agent.mail.release_issue(issue_id) - - assert release1, "First release should succeed" - assert release2, "Second release should be idempotent (no error)" - - # Verify no reservations - reservations_after = agent.mail.get_reservations() - assert len(reservations_after) == 0, "Should have 0 reservations after release" - - print("āœ… PASS: Double reserve and release are idempotent") - return True - - finally: - server.stop() - shutil.rmtree(workspace, ignore_errors=True) - - -def main(): - """Run coordination tests.""" - print("🧪 Multi-Agent Coordination Test Suite") - print("Fast tests for critical coordination scenarios") - - try: - subprocess.run(["bd", "--version"], capture_output=True, check=True) - except (subprocess.CalledProcessError, FileNotFoundError): - print("āŒ ERROR: bd command not found") - sys.exit(1) - - tests = [ - ("Fairness (10 agents, 5 issues)", test_fairness_n_agents_m_issues), - ("Notification end-to-end", test_notification_end_to_end), - ("Reservation handoff", test_reservation_handoff), - ("Idempotent operations", test_idempotent_operations), - ] - - passed = 0 - failed = 0 - start_time = time.time() - - for name, test_func in tests: - try: - if test_func(): - passed += 1 - except AssertionError as e: - print(f"\nāŒ FAIL: {name}") - print(f" {e}") - failed += 1 - except Exception as e: - print(f"\nšŸ’„ ERROR in {name}: {e}") - import traceback - traceback.print_exc() - failed += 1 - - elapsed = time.time() - start_time - - print("\n" + "="*70) - print("SUMMARY") - print("="*70) - print(f"āœ… Passed: {passed}/{len(tests)}") - print(f"āŒ Failed: {failed}/{len(tests)}") - print(f"ā±ļø Total time: {elapsed:.1f}s") - - if failed == 0: - print("\nšŸŽ‰ All coordination tests passed!") - sys.exit(0) - else: - print(f"\nāš ļø {failed} test(s) failed") - sys.exit(1) - - -if __name__ == "__main__": - main() diff --git a/tests/integration/test_reservation_ttl.py b/tests/integration/test_reservation_ttl.py deleted file mode 100755 index d1de9759..00000000 --- a/tests/integration/test_reservation_ttl.py +++ /dev/null @@ -1,635 +0,0 @@ -#!/usr/bin/env python3 -""" -Reservation TTL and Expiration Test Suite - -Tests verify time-based reservation behavior: -- Short TTL reservations (30s) -- Reservation blocking verification -- Auto-release after expiration -- Renewal/heartbeat mechanisms - -Performance notes: -- Uses 30s TTL for expiration tests (fast enough for CI) -- Uses mock HTTP server with minimal overhead -- Each test ~30-60s (waiting for expiration) -""" - -import json -import subprocess -import tempfile -import shutil -import os -import sys -import time -import logging -from pathlib import Path -from http.server import HTTPServer, BaseHTTPRequestHandler -from threading import Thread, Lock -from typing import Optional, Dict, Any, List -import socket -from datetime import datetime, timedelta - -# Add lib directory for beads_mail_adapter -lib_path = Path(__file__).parent.parent.parent / "lib" -sys.path.insert(0, str(lib_path)) - -from beads_mail_adapter import AgentMailAdapter - -# Configure logging -logging.basicConfig( - level=logging.WARNING, - format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' -) -logger = logging.getLogger(__name__) - -# Test configuration -TEST_TIMEOUT = 2 # HTTP timeout -SHORT_TTL = 30 # Short TTL for expiration tests (30 seconds) - - -class Reservation: - """Represents a file reservation with TTL.""" - - def __init__(self, file_path: str, agent_name: str, ttl: int): - self.file_path = file_path - self.agent_name = agent_name - self.expires_at = datetime.now() + timedelta(seconds=ttl) - self.created_at = datetime.now() - - def is_expired(self) -> bool: - """Check if reservation has expired.""" - return datetime.now() >= self.expires_at - - def renew(self, ttl: int) -> None: - """Renew reservation with new TTL.""" - self.expires_at = datetime.now() + timedelta(seconds=ttl) - - def to_dict(self) -> Dict[str, Any]: - """Convert to dictionary for JSON serialization.""" - return { - "file_path": self.file_path, - "agent_name": self.agent_name, - "expires_at": self.expires_at.isoformat(), - "created_at": self.created_at.isoformat() - } - - -class MockAgentMailServer: - """Mock Agent Mail server with TTL-based reservation management.""" - - def __init__(self, port: int = 0): - self.port = port - self.server: Optional[HTTPServer] = None - self.thread: Optional[Thread] = None - self.reservations: Dict[str, Reservation] = {} # file_path -> Reservation - self.lock = Lock() # Thread-safe access to reservations - self.request_count = 0 - - def start(self) -> int: - """Start the mock server. Returns actual port number.""" - handler_class = self._create_handler() - - # Find available port if port=0 - if self.port == 0: - with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: - s.bind(('', 0)) - s.listen(1) - self.port = s.getsockname()[1] - - self.server = HTTPServer(('127.0.0.1', self.port), handler_class) - self.thread = Thread(target=self.server.serve_forever, daemon=True) - self.thread.start() - - # Wait for server to be ready - time.sleep(0.1) - - logger.info(f"Mock Agent Mail server started on port {self.port}") - return self.port - - def stop(self): - """Stop the mock server.""" - if self.server: - self.server.shutdown() - self.server.server_close() - logger.info(f"Mock Agent Mail server stopped") - - def _cleanup_expired(self) -> None: - """Remove expired reservations.""" - with self.lock: - expired = [path for path, res in self.reservations.items() if res.is_expired()] - for path in expired: - del self.reservations[path] - logger.debug(f"Auto-released expired reservation: {path}") - - def _create_handler(self): - """Create request handler class with access to server state.""" - parent = self - - class MockHandler(BaseHTTPRequestHandler): - def log_message(self, format, *args): - """Suppress default logging.""" - pass - - def do_GET(self): - parent.request_count += 1 - parent._cleanup_expired() # Clean up expired reservations - - # Health check - if self.path == "/api/health": - response = {"status": "ok"} - self.send_response(200) - self.send_header('Content-Type', 'application/json') - self.end_headers() - self.wfile.write(json.dumps(response).encode()) - - # Get all reservations - elif self.path == "/api/reservations": - with parent.lock: - reservations = [res.to_dict() for res in parent.reservations.values()] - - self.send_response(200) - self.send_header('Content-Type', 'application/json') - self.end_headers() - self.wfile.write(json.dumps({"reservations": reservations}).encode()) - - else: - self.send_response(404) - self.end_headers() - - def do_POST(self): - parent.request_count += 1 - parent._cleanup_expired() # Clean up expired reservations - - # Read request body - content_length = int(self.headers.get('Content-Length', 0)) - body = self.rfile.read(content_length) if content_length > 0 else b'{}' - - try: - data = json.loads(body.decode('utf-8')) - except json.JSONDecodeError: - self.send_response(400) - self.send_header('Content-Type', 'application/json') - self.end_headers() - self.wfile.write(json.dumps({"error": "Invalid JSON"}).encode()) - return - - # Create/renew reservation - if self.path == "/api/reservations": - file_path = data.get("file_path") - agent_name = data.get("agent_name") - ttl = data.get("ttl", 3600) - - if not file_path or not agent_name: - self.send_response(400) - self.send_header('Content-Type', 'application/json') - self.end_headers() - self.wfile.write(json.dumps({"error": "Missing file_path or agent_name"}).encode()) - return - - with parent.lock: - # Check if already reserved by another agent - if file_path in parent.reservations: - existing = parent.reservations[file_path] - if existing.agent_name != agent_name: - # Conflict: already reserved by another agent - self.send_response(409) - self.send_header('Content-Type', 'application/json') - self.end_headers() - error_msg = f"File already reserved by {existing.agent_name}" - self.wfile.write(json.dumps({"error": error_msg}).encode()) - return - else: - # Renewal: same agent re-reserving (heartbeat) - existing.renew(ttl) - logger.debug(f"Renewed reservation: {file_path} by {agent_name}") - else: - # New reservation - parent.reservations[file_path] = Reservation(file_path, agent_name, ttl) - logger.debug(f"Created reservation: {file_path} by {agent_name} (TTL={ttl}s)") - - self.send_response(201) - self.send_header('Content-Type', 'application/json') - self.end_headers() - self.wfile.write(json.dumps({"status": "reserved"}).encode()) - - else: - self.send_response(404) - self.end_headers() - - def do_DELETE(self): - parent.request_count += 1 - parent._cleanup_expired() # Clean up expired reservations - - # Release reservation: /api/reservations/{agent}/{issue_id} - # Extract file_path from URL (last component is issue_id) - parts = self.path.split('/') - if len(parts) >= 5 and parts[1] == "api" and parts[2] == "reservations": - agent_name = parts[3] - issue_id = parts[4] - file_path = f".beads/issues/{issue_id}" - - with parent.lock: - if file_path in parent.reservations: - res = parent.reservations[file_path] - if res.agent_name == agent_name: - del parent.reservations[file_path] - logger.debug(f"Released reservation: {file_path}") - - self.send_response(204) - self.end_headers() - else: - self.send_response(404) - self.end_headers() - - return MockHandler - - -class TestAgent: - """Test agent that performs bd operations with reservation support.""" - - def __init__(self, workspace: str, agent_name: str = "test-agent", - mail_url: Optional[str] = None): - self.workspace = workspace - self.agent_name = agent_name - self.mail_url = mail_url - - # Initialize adapter if URL provided - if mail_url: - self.mail = AgentMailAdapter( - url=mail_url, - agent_name=agent_name, - timeout=TEST_TIMEOUT - ) - else: - self.mail = None - - def run_bd(self, *args) -> dict: - """Run bd command and return JSON output.""" - cmd = ["bd", "--no-daemon"] + list(args) + ["--json"] - result = subprocess.run( - cmd, - cwd=self.workspace, - capture_output=True, - text=True - ) - - if result.returncode != 0: - return {"error": result.stderr} - - if result.stdout.strip(): - try: - return json.loads(result.stdout) - except json.JSONDecodeError: - return {"error": "Invalid JSON", "output": result.stdout} - return {} - - def create_issue(self, title: str, priority: int = 1) -> Optional[str]: - """Create an issue and return its ID.""" - result = self.run_bd("create", title, "-p", str(priority)) - if "error" in result: - logger.error(f"Failed to create issue: {result['error']}") - return None - return result.get("id") - - def claim_issue(self, issue_id: str, ttl: int = 3600) -> bool: - """Attempt to claim an issue with optional reservation.""" - # Try to reserve if Agent Mail is enabled - if self.mail and self.mail.enabled: - reserved = self.mail.reserve_issue(issue_id, ttl=ttl) - if not reserved: - logger.warning(f"Failed to reserve {issue_id}") - return False - - # Update status - result = self.run_bd("update", issue_id, "--status", "in_progress") - - if "error" in result: - logger.error(f"Failed to claim {issue_id}: {result['error']}") - if self.mail and self.mail.enabled: - self.mail.release_issue(issue_id) - return False - - return True - - def renew_reservation(self, issue_id: str, ttl: int = 3600) -> bool: - """Renew reservation (heartbeat).""" - if self.mail and self.mail.enabled: - # Re-reserving with same agent acts as renewal - return self.mail.reserve_issue(issue_id, ttl=ttl) - return True - - -def test_short_ttl_reservation(): - """Test reservation with short TTL (30s).""" - print("\n" + "="*70) - print("TEST 1: Short TTL Reservation (30s)") - print("="*70) - - workspace = tempfile.mkdtemp(prefix="bd-test-ttl-") - server = MockAgentMailServer() - - try: - # Initialize workspace - subprocess.run( - ["bd", "init", "--quiet", "--prefix", "test"], - cwd=workspace, - check=True, - capture_output=True - ) - - # Start server - port = server.start() - mail_url = f"http://127.0.0.1:{port}" - - # Create agent - agent = TestAgent(workspace, "test-agent", mail_url=mail_url) - - # Create and claim issue with short TTL - issue_id = agent.create_issue("Test short TTL reservation") - assert issue_id is not None, "Should create issue" - - start_time = time.time() - claimed = agent.claim_issue(issue_id, ttl=SHORT_TTL) - assert claimed, f"Should claim issue with {SHORT_TTL}s TTL" - - # Verify reservation exists - reservations = agent.mail.get_reservations() - assert len(reservations) == 1, f"Should have 1 reservation, got {len(reservations)}" - assert reservations[0]["agent_name"] == "test-agent", "Reservation should be owned by test-agent" - - # Check TTL info - res = reservations[0] - expires_at = datetime.fromisoformat(res["expires_at"]) - created_at = datetime.fromisoformat(res["created_at"]) - actual_ttl = (expires_at - created_at).total_seconds() - - print(f"āœ… PASS: Created reservation with {SHORT_TTL}s TTL") - print(f" • Issue: {issue_id}") - print(f" • Actual TTL: {actual_ttl:.1f}s") - print(f" • Expires at: {expires_at.strftime('%H:%M:%S')}") - return True - - finally: - server.stop() - shutil.rmtree(workspace, ignore_errors=True) - - -def test_reservation_blocking(): - """Test that reservation blocks other agents from claiming.""" - print("\n" + "="*70) - print("TEST 2: Reservation Blocking Verification") - print("="*70) - - workspace = tempfile.mkdtemp(prefix="bd-test-block-") - server = MockAgentMailServer() - - try: - # Initialize workspace - subprocess.run( - ["bd", "init", "--quiet", "--prefix", "test"], - cwd=workspace, - check=True, - capture_output=True - ) - - # Start server - port = server.start() - mail_url = f"http://127.0.0.1:{port}" - - # Create two agents - agent1 = TestAgent(workspace, "agent1", mail_url=mail_url) - agent2 = TestAgent(workspace, "agent2", mail_url=mail_url) - - # Agent 1 creates and claims issue - issue_id = agent1.create_issue("Test reservation blocking") - assert issue_id is not None, "Agent 1 should create issue" - - claimed1 = agent1.claim_issue(issue_id, ttl=SHORT_TTL) - assert claimed1, "Agent 1 should claim issue" - - # Agent 2 attempts to claim same issue (should fail) - claimed2 = agent2.claim_issue(issue_id, ttl=SHORT_TTL) - assert not claimed2, "Agent 2 should NOT be able to claim (blocked by reservation)" - - # Verify only one reservation exists - reservations = agent1.mail.get_reservations() - assert len(reservations) == 1, f"Should have 1 reservation, got {len(reservations)}" - assert reservations[0]["agent_name"] == "agent1", "Reservation should be owned by agent1" - - print("āœ… PASS: Reservation successfully blocked other agent") - print(f" • Agent 1 claimed: {issue_id}") - print(f" • Agent 2 blocked by reservation") - return True - - finally: - server.stop() - shutil.rmtree(workspace, ignore_errors=True) - - -def test_auto_release_after_expiration(): - """Test that reservation auto-releases after TTL expires.""" - print("\n" + "="*70) - print("TEST 3: Auto-Release After Expiration") - print("="*70) - print(f" (This test waits {SHORT_TTL}s for expiration)") - - workspace = tempfile.mkdtemp(prefix="bd-test-expire-") - server = MockAgentMailServer() - - try: - # Initialize workspace - subprocess.run( - ["bd", "init", "--quiet", "--prefix", "test"], - cwd=workspace, - check=True, - capture_output=True - ) - - # Start server - port = server.start() - mail_url = f"http://127.0.0.1:{port}" - - # Create two agents - agent1 = TestAgent(workspace, "agent1", mail_url=mail_url) - agent2 = TestAgent(workspace, "agent2", mail_url=mail_url) - - # Agent 1 creates and claims issue with short TTL - issue_id = agent1.create_issue("Test auto-release") - assert issue_id is not None, "Agent 1 should create issue" - - start_time = time.time() - claimed1 = agent1.claim_issue(issue_id, ttl=SHORT_TTL) - assert claimed1, "Agent 1 should claim issue" - - # Verify reservation exists - reservations = agent1.mail.get_reservations() - assert len(reservations) == 1, "Should have 1 active reservation" - - # Agent 2 attempts to claim (should fail - still reserved) - claimed2_before = agent2.claim_issue(issue_id, ttl=SHORT_TTL) - assert not claimed2_before, "Agent 2 should be blocked before expiration" - - print(f" • Waiting {SHORT_TTL}s for reservation to expire...") - - # Wait for TTL to expire (add 2s buffer for clock skew) - time.sleep(SHORT_TTL + 2) - - elapsed = time.time() - start_time - - # Verify reservation auto-released (next request cleans up expired) - reservations_after = agent2.mail.get_reservations() # Triggers cleanup - assert len(reservations_after) == 0, f"Reservation should have expired, got {len(reservations_after)}" - - # Agent 2 should now be able to claim - claimed2_after = agent2.claim_issue(issue_id, ttl=SHORT_TTL) - assert claimed2_after, "Agent 2 should claim issue after expiration" - - # Verify new reservation by agent2 - final_reservations = agent2.mail.get_reservations() - assert len(final_reservations) == 1, "Should have 1 reservation after agent2 claims" - assert final_reservations[0]["agent_name"] == "agent2", "Reservation should be owned by agent2" - - print(f"āœ… PASS: Reservation auto-released after {elapsed:.1f}s") - print(f" • Agent 1 reservation expired") - print(f" • Agent 2 successfully claimed after expiration") - return True - - finally: - server.stop() - shutil.rmtree(workspace, ignore_errors=True) - - -def test_renewal_heartbeat(): - """Test reservation renewal (heartbeat mechanism).""" - print("\n" + "="*70) - print("TEST 4: Renewal/Heartbeat Mechanism") - print("="*70) - print(f" (This test waits {SHORT_TTL // 2}s to test renewal)") - - workspace = tempfile.mkdtemp(prefix="bd-test-renew-") - server = MockAgentMailServer() - - try: - # Initialize workspace - subprocess.run( - ["bd", "init", "--quiet", "--prefix", "test"], - cwd=workspace, - check=True, - capture_output=True - ) - - # Start server - port = server.start() - mail_url = f"http://127.0.0.1:{port}" - - # Create agent - agent = TestAgent(workspace, "test-agent", mail_url=mail_url) - - # Create and claim issue with short TTL - issue_id = agent.create_issue("Test renewal/heartbeat") - assert issue_id is not None, "Should create issue" - - claimed = agent.claim_issue(issue_id, ttl=SHORT_TTL) - assert claimed, f"Should claim issue with {SHORT_TTL}s TTL" - - # Get initial expiration time - reservations = agent.mail.get_reservations() - assert len(reservations) == 1, "Should have 1 reservation" - initial_expires = datetime.fromisoformat(reservations[0]["expires_at"]) - - print(f" • Initial expiration: {initial_expires.strftime('%H:%M:%S')}") - print(f" • Waiting {SHORT_TTL // 2}s before renewal...") - - # Wait halfway through TTL - time.sleep(SHORT_TTL // 2) - - # Renew reservation (heartbeat) - renewed = agent.renew_reservation(issue_id, ttl=SHORT_TTL) - assert renewed, "Should renew reservation" - - # Get new expiration time - reservations_after = agent.mail.get_reservations() - assert len(reservations_after) == 1, "Should still have 1 reservation" - renewed_expires = datetime.fromisoformat(reservations_after[0]["expires_at"]) - - # Verify expiration was extended - extension = (renewed_expires - initial_expires).total_seconds() - - print(f" • Renewed expiration: {renewed_expires.strftime('%H:%M:%S')}") - print(f" • Extension: {extension:.1f}s") - - # Extension should be approximately TTL/2 (since we renewed halfway) - # Allow 5s tolerance for clock skew and processing time - expected_extension = SHORT_TTL // 2 - assert abs(extension - expected_extension) < 5, \ - f"Extension should be ~{expected_extension}s, got {extension:.1f}s" - - print(f"āœ… PASS: Reservation renewed successfully") - print(f" • Heartbeat extended expiration by {extension:.1f}s") - return True - - finally: - server.stop() - shutil.rmtree(workspace, ignore_errors=True) - - -def main(): - """Run all TTL/expiration tests.""" - print("🧪 Reservation TTL and Expiration Test Suite") - print(f"Testing time-based reservation behavior (SHORT_TTL={SHORT_TTL}s)") - - # Check if bd is available - try: - subprocess.run(["bd", "--version"], capture_output=True, check=True) - except (subprocess.CalledProcessError, FileNotFoundError): - print("āŒ ERROR: bd command not found") - print(" Install: go install github.com/steveyegge/beads/cmd/bd@latest") - sys.exit(1) - - # Run tests - tests = [ - ("Short TTL reservation", test_short_ttl_reservation), - ("Reservation blocking", test_reservation_blocking), - ("Auto-release after expiration", test_auto_release_after_expiration), - ("Renewal/heartbeat mechanism", test_renewal_heartbeat), - ] - - passed = 0 - failed = 0 - start_time = time.time() - - for name, test_func in tests: - try: - if test_func(): - passed += 1 - except AssertionError as e: - print(f"\nāŒ FAIL: {name}") - print(f" {e}") - failed += 1 - except Exception as e: - print(f"\nšŸ’„ ERROR in {name}: {e}") - import traceback - traceback.print_exc() - failed += 1 - - elapsed = time.time() - start_time - - # Summary - print("\n" + "="*70) - print("SUMMARY") - print("="*70) - print(f"āœ… Passed: {passed}/{len(tests)}") - print(f"āŒ Failed: {failed}/{len(tests)}") - print(f"ā±ļø Total time: {elapsed:.1f}s") - - if failed == 0: - print("\nšŸŽ‰ All TTL/expiration tests passed!") - print(" Reservation expiration and renewal work correctly") - sys.exit(0) - else: - print(f"\nāš ļø {failed} test(s) failed") - sys.exit(1) - - -if __name__ == "__main__": - main()