diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index ae0b29d5..3a98c26c 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -32,6 +32,7 @@ {"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","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-4lm3","title":"Correction: Pinned field already in v0.31.0","description":"Quick correction - the Pinned field is already in the current bd v0.31.0:\n\n```go\n// In beads internal/types/types.go\nPinned bool `json:\"pinned,omitempty\"`\n```\n\nSo you just need to:\n1. Add `Pinned bool `json:\"pinned,omitempty\"`` to BeadsMessage in types.go\n2. Sort pinned messages first in listBeads() after fetching\n\nNo migration needed - the field is already there.\n\n-- Mayor","status":"open","priority":2,"issue_type":"message","assignee":"gastown/crew/max","created_at":"2025-12-20T17:52:27.321458-08:00","updated_at":"2025-12-20T17:52:27.321458-08:00","labels":["from:beads-crew-dave","thread:thread-4dd70157dbc1"]} {"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","metadata":"{}"}]} {"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"} @@ -69,6 +70,7 @@ {"id":"bd-8pyn","title":"Version Bump: 0.30.7","description":"Release checklist for version 0.30.7. This molecule ensures all release steps are completed properly.","status":"closed","priority":1,"issue_type":"epic","created_at":"2025-12-19T22:56:48.648694-08:00","updated_at":"2025-12-20T00:25:59.529183-08:00","closed_at":"2025-12-20T00:25:59.529183-08:00","close_reason":"Version 0.30.7 released successfully - all steps completed"} {"id":"bd-8v2","title":"Add {{version}} to versionChanges in info.go","description":"Add new entry at TOP of versionChanges in cmd/bd/info.go with release notes from CHANGELOG.md. Must do before bump-version.sh --commit.","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-18T22:43:00.482846-08:00","updated_at":"2025-12-18T22:45:21.465817-08:00","closed_at":"2025-12-18T22:45:21.465817-08:00","dependencies":[{"issue_id":"bd-8v2","depends_on_id":"bd-qqc","type":"parent-child","created_at":"2025-12-18T22:43:16.496649-08:00","created_by":"daemon"},{"issue_id":"bd-8v2","depends_on_id":"bd-kyo","type":"blocks","created_at":"2025-12-18T22:43:20.69619-08:00","created_by":"daemon"}]} {"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-95k8","title":"Pinned field available in beads v0.37.0","description":"Hey max,\n\nHeads up on your mail overhaul work:\n\n1. **Pinned field is available** - beads v0.37.0 (released by dave earlier) includes the pinned field on issues. You'll want to add this to BeadsMessage in types.go.\n\n2. **Database migration** - Check if existing .beads databases need migration to support the pinned field. Run `bd doctor` to see if it flags anything.\n\n3. **Sorting task** - Once you have the pinned field, gt-ngu1 (pinned beads first in mail inbox) needs implementing. Since messages now come from `bd list --type=message`, you'll need to either:\n - Sort in listBeads() after fetching, or\n - Ensure bd list returns pinned items first (may already do this?)\n\nCheck what version of bd you're building against.\n\n-- Mayor","status":"open","priority":2,"issue_type":"message","assignee":"gastown/crew/max","created_at":"2025-12-20T17:51:57.315956-08:00","updated_at":"2025-12-20T17:51:57.315956-08:00","labels":["from:beads-crew-dave","thread:thread-71ac20c7e432"]} {"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","metadata":"{}"}]} {"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"} @@ -139,6 +141,7 @@ {"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-f3ll","title":"Merge: bd-ot0w","description":"branch: polecat/dementus\ntarget: main\nsource_issue: bd-ot0w\nrig: beads","status":"open","priority":2,"issue_type":"merge-request","created_at":"2025-12-19T23:20:33.495772-08:00","updated_at":"2025-12-19T23:20:33.495772-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","dependencies":[{"issue_id":"bd-f5cc","depends_on_id":"bd-x36g","type":"supersedes","created_at":"2025-12-18T13:45:31.137191-08:00","created_by":"migration"}],"deleted_at":"2025-12-17T16:11:17.070763-08:00","deleted_by":"batch delete","delete_reason":"batch delete","original_type":"message"} +{"id":"bd-fa2h","title":"🤝 HANDOFF: v0.31.0 released, molecules discussion","description":"Session completed 0.31.0 release and had important molecules discussion.\n\n## Completed\n- v0.31.0 released (deferred status, audit trail, directory labels, etc.)\n- Fixed lint issues, hook version markers, codesigning\n- All CI green, artifacts verified\n\n## Filed Issues\n- bd-usro: Rename template instantiate → bd mol bond\n- bd-y8bj: Auto-detect identity for bd mail (P1 bug)\n- gt-975: Molecule execution support for polecats/crew\n- gt-976: Crew lifecycle support in Deacon\n\n## Key Insight\nMolecules are the future - TodoWrite is ephemeral, molecules are persistent institutional memory on the world chain. I tried to use TodoWrite for version bump and missed steps (codesigning, MCP verification). Molecules would have caught this.\n\n## Next Steps\n- bd mol bond implementation is priority\n- Max has gt-976 for crew lifecycle (enables automated refresh mid-molecule)\n\nCheck bd ready and gt-975/976 status.","status":"open","priority":2,"issue_type":"message","assignee":"beads/crew/dave","created_at":"2025-12-20T17:23:09.889562-08:00","updated_at":"2025-12-20T17:23:09.889562-08:00","sender":"Steve Yegge","ephemeral":true} {"id":"bd-fgw3","title":"Update local installation","description":"Run install script or brew upgrade to get new version locally: curl -fsSL .../install.sh | bash","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-19T22:56:05.052016-08:00","updated_at":"2025-12-20T00:25:52.805029-08:00","closed_at":"2025-12-20T00:25:52.805029-08:00","close_reason":"Local bd updated to 0.30.7","dependencies":[{"issue_id":"bd-fgw3","depends_on_id":"bd-6s61","type":"parent-child","created_at":"2025-12-19T22:56:15.248427-08:00","created_by":"daemon"},{"issue_id":"bd-fgw3","depends_on_id":"bd-si4g","type":"blocks","created_at":"2025-12-19T22:56:23.497325-08:00","created_by":"daemon"}]} {"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"} @@ -290,6 +293,7 @@ {"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-y4vz","title":"Work on beads-eub: Consolidated context tool for MCP serv...","description":"Work on beads-eub: Consolidated context tool for MCP server (GH#636). Merge set_context, where_am_i, init into single 'context' tool. When done, submit MR (not PR) to integration branch for Refinery.","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-19T22:56:58.527144-08:00","updated_at":"2025-12-20T00:26:12.607357-08:00","closed_at":"2025-12-19T23:31:11.906952-08:00"} +{"id":"bd-y8bj","title":"Auto-detect identity from directory context for bd mail","description":"Currently bd mail inbox defaults to git user name, requiring --identity flag with exact format.\n\n## Problem\n- Mail sent to `gastown/crew/max`\n- Max runs `bd mail inbox` → defaults to 'Steve Yegge' (git user)\n- Max must know to use `--identity 'gastown/crew/max'` with exact slashes\n\n## Proposed Fix\nAuto-detect identity from directory context when in a Gas Town workspace:\n- In `/Users/stevey/gt/gastown/crew/max`, infer identity = `gastown/crew/max`\n- Pattern: `\u003ctown\u003e/\u003crig\u003e/\u003crole\u003e/\u003cname\u003e` → `\u003crig\u003e/\u003crole\u003e/\u003cname\u003e`\n\n## Additional Improvements\n1. Support GT_IDENTITY env var (set by gt crew at / session spawning)\n2. Support identity in .beads/config.yaml\n3. Normalize format: accept both slashes and dashes as equivalent\n\n## Context\nDiscovered during crew-to-crew work assignment. Max couldn't see mail despite correct nudge because identity defaulted wrong.","status":"open","priority":1,"issue_type":"bug","created_at":"2025-12-20T17:22:53.938586-08:00","updated_at":"2025-12-20T17:22:53.938586-08:00"} {"id":"bd-y8tn","title":"Test Molecule","description":"A test molecule","status":"closed","priority":2,"issue_type":"molecule","created_at":"2025-12-19T18:30:24.491279-08:00","updated_at":"2025-12-19T18:31:12.49898-08:00","closed_at":"2025-12-19T18:31:12.49898-08:00","close_reason":"test molecule - deleting"} {"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-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"} diff --git a/CLAUDE.md b/CLAUDE.md index df878195..74a12177 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -19,7 +19,7 @@ the overseer, not as part of a swarm. **Your mail address:** `beads/dave` -Check your mail with: `bd mail inbox --identity beads-dave` +Check your mail with: `gt mail inbox` (mail is handled by Gas Town, not beads) ## Gas Town Architecture @@ -38,7 +38,7 @@ Town (/Users/stevey/gt) ## Key Commands ### Finding Work -- `bd mail inbox --identity beads-dave` - Check your inbox +- `gt mail inbox` - Check your inbox (mail is in Gas Town) - `bd ready` - Available issues - `bd list --status=in_progress` - Your active work @@ -49,8 +49,8 @@ Town (/Users/stevey/gt) - `bd sync` - Sync beads changes ### Communication -- `bd mail send mayor -s "Subject" -m "Message"` - To Mayor -- `bd mail send beads-dave -s "Subject" -m "Message"` - To yourself (handoff) +- `gt mail send mayor/ -s "Subject" -m "Message"` - To Mayor +- `gt mail send beads/dave -s "Subject" -m "Message"` - To yourself (handoff) ## Beads Database @@ -361,47 +361,6 @@ bd create "Add feature" -t feature --json # What feature? Why needed? bd create "Refactor code" -t task --json # What code? Why refactor? ``` -### 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. - -**Setup:** - -```bash -# Set your identity (add to environment or .beads/config.json) -export BEADS_IDENTITY="worker-1" -``` - -**Commands:** - -```bash -# Send a message -bd mail send -s "Subject" -m "Body" -bd mail send worker-2 -s "Handoff" -m "Your turn on bd-xyz" --urgent - -# Check your inbox -bd mail inbox - -# Read a specific message -bd mail read bd-a1b2 - -# Acknowledge (mark as read/close) -bd mail ack bd-a1b2 - -# Reply to a message (creates thread) -bd mail reply bd-a1b2 -m "Thanks, on it!" -``` - -**Use cases:** -- Task handoffs between agents -- Status updates to coordinator -- Blocking questions requiring response -- Priority signaling with `--urgent` flag - -**Cleanup:** Messages are ephemeral. Run `bd cleanup --ephemeral --force` to delete closed messages. - -See [docs/messaging.md](docs/messaging.md) for full documentation. - ### Deletion Tracking When issues are deleted (via `bd delete` or `bd cleanup`), they are recorded in `.beads/deletions.jsonl`. This manifest: diff --git a/cmd/bd/mail.go b/cmd/bd/mail.go deleted file mode 100644 index 418d074f..00000000 --- a/cmd/bd/mail.go +++ /dev/null @@ -1,727 +0,0 @@ -package main - -import ( - "encoding/json" - "fmt" - "os" - "strings" - "time" - - "github.com/spf13/cobra" - "github.com/steveyegge/beads/internal/config" - "github.com/steveyegge/beads/internal/hooks" - "github.com/steveyegge/beads/internal/rpc" - "github.com/steveyegge/beads/internal/types" -) - -var mailCmd = &cobra.Command{ - Use: "mail", - Short: "Send and receive messages via beads", - Long: `Send and receive messages between agents using beads storage. - -Messages are stored as issues with type=message, enabling git-native -inter-agent communication without external services. - -Examples: - bd mail send worker-1 -s "Task complete" -m "Finished bd-xyz" - bd mail inbox - bd mail read bd-abc123 - bd mail ack bd-abc123`, -} - -var mailSendCmd = &cobra.Command{ - Use: "send -s -m ", - Short: "Send a message to another agent", - Long: `Send a message to another agent via beads. - -Creates an issue with type=message, sender=your identity, assignee=recipient. -The --urgent flag sets priority=0. Use --priority for explicit priority control. - -Priority levels: 0=critical/urgent, 1=high, 2=normal (default), 3=low, 4=backlog - -Examples: - bd mail send worker-1 -s "Task complete" -m "Finished bd-xyz" - bd mail send worker-1 -s "Help needed" -m "Blocked on auth" --urgent - bd mail send worker-1 -s "Quick note" -m "FYI" --priority 3 - bd mail send worker-1 -s "Task" -m "Do this" --type task --priority 1 - bd mail send worker-1 -s "Re: Hello" -m "Hi back" --reply-to bd-abc123 - bd mail send worker-1 -s "Quick note" -m "FYI" --identity refinery`, - Args: cobra.ExactArgs(1), - RunE: runMailSend, -} - -var mailInboxCmd = &cobra.Command{ - Use: "inbox", - Short: "List messages addressed to you", - Long: `List open messages where assignee matches your identity. - -Messages are sorted by priority (urgent first), then by date (newest first). - -Examples: - bd mail inbox - bd mail inbox --from worker-1 - bd mail inbox --priority 0`, - RunE: runMailInbox, -} - -var mailReadCmd = &cobra.Command{ - Use: "read ", - Short: "Read a specific message", - Long: `Display the full content of a message. - -Does NOT mark the message as read - use 'bd mail ack' for that. - -Example: - bd mail read bd-abc123`, - Args: cobra.ExactArgs(1), - RunE: runMailRead, -} - -var mailAckCmd = &cobra.Command{ - Use: "ack [id2...]", - Short: "Acknowledge (close) messages", - Long: `Mark messages as read by closing them. - -Can acknowledge multiple messages at once. - -Examples: - bd mail ack bd-abc123 - bd mail ack bd-abc123 bd-def456 bd-ghi789`, - Args: cobra.MinimumNArgs(1), - RunE: runMailAck, -} - -var mailReplyCmd = &cobra.Command{ - Use: "reply -m ", - Short: "Reply to a message", - Long: `Reply to an existing message, creating a conversation thread. - -Creates a new message with replies_to set to the original message, -and sends it to the original sender. - -Examples: - bd mail reply bd-abc123 -m "Thanks for the update!" - bd mail reply bd-abc123 -m "Done" --urgent`, - Args: cobra.ExactArgs(1), - RunE: runMailReply, -} - -// Mail command flags -var ( - mailSubject string - mailBody string - mailUrgent bool - mailIdentity string - mailFrom string - mailPriorityFlag int - // New flags for GGT compatibility (gt-8j8e) - mailSendPriority int // Numeric priority for mail send (0-4) - mailMsgType string // Message type (task, scavenge, notification, reply) - mailThreadID string // Thread ID for conversation grouping - mailReplyTo string // Message ID being replied to -) - -func init() { - rootCmd.AddCommand(mailCmd) - mailCmd.AddCommand(mailSendCmd) - mailCmd.AddCommand(mailInboxCmd) - mailCmd.AddCommand(mailReadCmd) - mailCmd.AddCommand(mailAckCmd) - mailCmd.AddCommand(mailReplyCmd) - - // Send command flags - mailSendCmd.Flags().StringVarP(&mailSubject, "subject", "s", "", "Message subject (required)") - mailSendCmd.Flags().StringVarP(&mailBody, "body", "m", "", "Message body (required)") - mailSendCmd.Flags().BoolVar(&mailUrgent, "urgent", false, "Set priority=0 (urgent)") - mailSendCmd.Flags().StringVar(&mailIdentity, "identity", "", "Override sender identity") - // GGT compatibility flags (gt-8j8e) - mailSendCmd.Flags().IntVar(&mailSendPriority, "priority", -1, "Message priority (0-4, where 0=urgent)") - mailSendCmd.Flags().StringVar(&mailMsgType, "type", "", "Message type (task, scavenge, notification, reply)") - mailSendCmd.Flags().StringVar(&mailThreadID, "thread-id", "", "Thread ID for conversation grouping") - mailSendCmd.Flags().StringVar(&mailReplyTo, "reply-to", "", "Message ID this is replying to") - _ = mailSendCmd.MarkFlagRequired("subject") - _ = mailSendCmd.MarkFlagRequired("body") - - // Inbox command flags - mailInboxCmd.Flags().StringVar(&mailFrom, "from", "", "Filter by sender") - mailInboxCmd.Flags().IntVar(&mailPriorityFlag, "priority", -1, "Filter by priority (0-4)") - mailInboxCmd.Flags().StringVar(&mailIdentity, "identity", "", "Override recipient identity") - - // Read command flags - mailReadCmd.Flags().StringVar(&mailIdentity, "identity", "", "Override identity for access check") - - // Ack command flags - mailAckCmd.Flags().StringVar(&mailIdentity, "identity", "", "Override identity") - - // Reply command flags - mailReplyCmd.Flags().StringVarP(&mailBody, "body", "m", "", "Reply body (required)") - mailReplyCmd.Flags().BoolVar(&mailUrgent, "urgent", false, "Set priority=0 (urgent)") - mailReplyCmd.Flags().StringVar(&mailIdentity, "identity", "", "Override sender identity") - _ = mailReplyCmd.MarkFlagRequired("body") -} - -func runMailSend(cmd *cobra.Command, args []string) error { - CheckReadonly("mail send") - - recipient := args[0] - sender := config.GetIdentity(mailIdentity) - - // Determine priority (gt-8j8e: --priority takes precedence over --urgent) - priority := 2 // default: normal - if cmd.Flags().Changed("priority") && mailSendPriority >= 0 && mailSendPriority <= 4 { - priority = mailSendPriority - } else if mailUrgent { - priority = 0 - } - - // Build labels for GGT metadata (gt-8j8e) - var labels []string - if mailMsgType != "" { - labels = append(labels, "msg-type:"+mailMsgType) - } - if mailThreadID != "" { - labels = append(labels, "thread:"+mailThreadID) - } - - // If daemon is running, use RPC - if daemonClient != nil { - createArgs := &rpc.CreateArgs{ - Title: mailSubject, - Description: mailBody, - IssueType: string(types.TypeMessage), - Priority: priority, - Assignee: recipient, - Sender: sender, - Ephemeral: true, // Messages can be bulk-deleted - Labels: labels, - RepliesTo: mailReplyTo, // Thread link (gt-8j8e) - } - - resp, err := daemonClient.Create(createArgs) - if err != nil { - return fmt.Errorf("failed to send message: %w", err) - } - - // Parse response to get issue ID - var issue types.Issue - if err := json.Unmarshal(resp.Data, &issue); err != nil { - return fmt.Errorf("parsing response: %w", err) - } - - // Run message hook (bd-kwro.8) - if hookRunner != nil { - hookRunner.Run(hooks.EventMessage, &issue) - } - - if jsonOutput { - result := map[string]interface{}{ - "id": issue.ID, - "to": recipient, - "from": sender, - "subject": mailSubject, - "priority": priority, - "timestamp": issue.CreatedAt, - } - encoder := json.NewEncoder(os.Stdout) - encoder.SetIndent("", " ") - return encoder.Encode(result) - } - - fmt.Printf("Message sent: %s\n", issue.ID) - fmt.Printf(" To: %s\n", recipient) - fmt.Printf(" Subject: %s\n", mailSubject) - if priority <= 1 { - fmt.Printf(" Priority: P%d\n", priority) - } - return nil - } - - // Direct mode - now := time.Now() - issue := &types.Issue{ - Title: mailSubject, - Description: mailBody, - Status: types.StatusOpen, - Priority: priority, - IssueType: types.TypeMessage, - Assignee: recipient, - Sender: sender, - Ephemeral: true, // Messages can be bulk-deleted - Labels: labels, - CreatedAt: now, - UpdatedAt: now, - } - - if err := store.CreateIssue(rootCtx, issue, actor); err != nil { - return fmt.Errorf("failed to send message: %w", err) - } - - // Add reply-to dependency if specified (gt-8j8e) - if mailReplyTo != "" { - dep := &types.Dependency{ - IssueID: issue.ID, - DependsOnID: mailReplyTo, - Type: types.DepRepliesTo, - CreatedAt: now, - CreatedBy: actor, - } - if err := store.AddDependency(rootCtx, dep, actor); err != nil { - // Log but don't fail - the message was still sent - fmt.Fprintf(os.Stderr, "Warning: failed to create reply-to link: %v\n", err) - } - } - - // Trigger auto-flush - if flushManager != nil { - flushManager.MarkDirty(false) - } - - // Run message hook (bd-kwro.8) - if hookRunner != nil { - hookRunner.Run(hooks.EventMessage, issue) - } - - if jsonOutput { - result := map[string]interface{}{ - "id": issue.ID, - "to": recipient, - "from": sender, - "subject": mailSubject, - "priority": priority, - "timestamp": issue.CreatedAt, - } - encoder := json.NewEncoder(os.Stdout) - encoder.SetIndent("", " ") - return encoder.Encode(result) - } - - fmt.Printf("Message sent: %s\n", issue.ID) - fmt.Printf(" To: %s\n", recipient) - fmt.Printf(" Subject: %s\n", mailSubject) - if priority <= 1 { - fmt.Printf(" Priority: P%d\n", priority) - } - - return nil -} - -func runMailInbox(cmd *cobra.Command, args []string) error { - identity := config.GetIdentity(mailIdentity) - - // Query for open messages assigned to this identity - messageType := types.TypeMessage - openStatus := types.StatusOpen - filter := types.IssueFilter{ - IssueType: &messageType, - Status: &openStatus, - Assignee: &identity, - } - - var issues []*types.Issue - var err error - - if daemonClient != nil { - // Daemon mode - use RPC list - resp, rpcErr := daemonClient.List(&rpc.ListArgs{ - Status: string(openStatus), - IssueType: string(messageType), - Assignee: identity, - }) - if rpcErr != nil { - return fmt.Errorf("failed to fetch inbox: %w", rpcErr) - } - if err := json.Unmarshal(resp.Data, &issues); err != nil { - return fmt.Errorf("parsing response: %w", err) - } - } else { - // Direct mode - issues, err = store.SearchIssues(rootCtx, "", filter) - if err != nil { - return fmt.Errorf("failed to fetch inbox: %w", err) - } - } - - // Filter by sender if specified - var filtered []*types.Issue - for _, issue := range issues { - if mailFrom != "" && issue.Sender != mailFrom { - continue - } - // Filter by priority if specified - if cmd.Flags().Changed("priority") && mailPriorityFlag >= 0 && issue.Priority != mailPriorityFlag { - continue - } - filtered = append(filtered, issue) - } - - // Sort by priority (ascending), then by date (descending) - // Priority 0 is highest priority - for i := 0; i < len(filtered)-1; i++ { - for j := i + 1; j < len(filtered); j++ { - swap := false - if filtered[i].Priority > filtered[j].Priority { - swap = true - } else if filtered[i].Priority == filtered[j].Priority { - if filtered[i].CreatedAt.Before(filtered[j].CreatedAt) { - swap = true - } - } - if swap { - filtered[i], filtered[j] = filtered[j], filtered[i] - } - } - } - - if jsonOutput { - encoder := json.NewEncoder(os.Stdout) - encoder.SetIndent("", " ") - return encoder.Encode(filtered) - } - - if len(filtered) == 0 { - fmt.Printf("No messages for %s\n", identity) - return nil - } - - fmt.Printf("Inbox for %s (%d messages):\n\n", identity, len(filtered)) - for _, msg := range filtered { - // 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)) - } - - // Priority indicator - priorityStr := "" - if msg.Priority == 0 { - priorityStr = " [URGENT]" - } else if msg.Priority == 1 { - priorityStr = " [HIGH]" - } - - fmt.Printf(" %s: %s%s\n", msg.ID, msg.Title, priorityStr) - fmt.Printf(" From: %s (%s)\n", msg.Sender, timeStr) - // NOTE: Thread info now in dependencies (Decision 004) - fmt.Println() - } - - return nil -} - -func runMailRead(cmd *cobra.Command, args []string) error { - messageID := args[0] - - var issue *types.Issue - - if daemonClient != nil { - // Daemon mode - use RPC show - resp, err := daemonClient.Show(&rpc.ShowArgs{ID: messageID}) - if err != nil { - return fmt.Errorf("failed to read message: %w", err) - } - if err := json.Unmarshal(resp.Data, &issue); err != nil { - return fmt.Errorf("parsing response: %w", err) - } - } else { - // Direct mode - var err error - issue, err = store.GetIssue(rootCtx, messageID) - if err != nil { - return fmt.Errorf("failed to read message: %w", err) - } - } - - if issue == nil { - return fmt.Errorf("message not found: %s", messageID) - } - - if issue.IssueType != types.TypeMessage { - return fmt.Errorf("%s is not a message (type: %s)", messageID, issue.IssueType) - } - - if jsonOutput { - encoder := json.NewEncoder(os.Stdout) - encoder.SetIndent("", " ") - return encoder.Encode(issue) - } - - // Display message - fmt.Println(strings.Repeat("─", 66)) - fmt.Printf("ID: %s\n", issue.ID) - fmt.Printf("From: %s\n", issue.Sender) - fmt.Printf("To: %s\n", issue.Assignee) - fmt.Printf("Subject: %s\n", issue.Title) - fmt.Printf("Time: %s\n", issue.CreatedAt.Format("2006-01-02 15:04:05")) - if issue.Priority <= 1 { - fmt.Printf("Priority: P%d\n", issue.Priority) - } - // NOTE: Thread info (RepliesTo) now in dependencies (Decision 004) - fmt.Printf("Status: %s\n", issue.Status) - fmt.Println(strings.Repeat("─", 66)) - fmt.Println() - fmt.Println(issue.Description) - fmt.Println() - - return nil -} - -func runMailAck(cmd *cobra.Command, args []string) error { - CheckReadonly("mail ack") - - var acked []string - var errors []string - - for _, messageID := range args { - var issue *types.Issue - - if daemonClient != nil { - // Daemon mode - use RPC - resp, err := daemonClient.Show(&rpc.ShowArgs{ID: messageID}) - if err != nil { - errors = append(errors, fmt.Sprintf("%s: %v", messageID, err)) - continue - } - if err := json.Unmarshal(resp.Data, &issue); err != nil { - errors = append(errors, fmt.Sprintf("%s: parse error: %v", messageID, err)) - continue - } - } else { - // Direct mode - var err error - issue, err = store.GetIssue(rootCtx, messageID) - if err != nil { - errors = append(errors, fmt.Sprintf("%s: %v", messageID, err)) - continue - } - } - - if issue == nil { - errors = append(errors, fmt.Sprintf("%s: not found", messageID)) - continue - } - - if issue.IssueType != types.TypeMessage { - errors = append(errors, fmt.Sprintf("%s: not a message (type: %s)", messageID, issue.IssueType)) - continue - } - - if issue.Status == types.StatusClosed { - errors = append(errors, fmt.Sprintf("%s: already acknowledged", messageID)) - continue - } - - // Close the message - if daemonClient != nil { - // Daemon mode - use RPC close - _, err := daemonClient.CloseIssue(&rpc.CloseArgs{ID: messageID}) - if err != nil { - errors = append(errors, fmt.Sprintf("%s: %v", messageID, err)) - continue - } - // Fire close hook for GGT notifications (daemon mode) - if hookRunner != nil { - hookRunner.Run(hooks.EventClose, issue) - } - } else { - // Direct mode - use CloseIssue for proper close handling - if err := store.CloseIssue(rootCtx, messageID, "acknowledged", actor); err != nil { - errors = append(errors, fmt.Sprintf("%s: %v", messageID, err)) - continue - } - // Fire close hook for GGT notifications (direct mode) - if hookRunner != nil { - hookRunner.Run(hooks.EventClose, issue) - } - } - - acked = append(acked, messageID) - } - - // Trigger auto-flush if any messages were acked (direct mode only) - if len(acked) > 0 && flushManager != nil { - flushManager.MarkDirty(false) - } - - if jsonOutput { - result := map[string]interface{}{ - "acknowledged": acked, - "errors": errors, - } - encoder := json.NewEncoder(os.Stdout) - encoder.SetIndent("", " ") - return encoder.Encode(result) - } - - for _, id := range acked { - fmt.Printf("Acknowledged: %s\n", id) - } - for _, errMsg := range errors { - fmt.Fprintf(os.Stderr, "Error: %s\n", errMsg) - } - - if len(errors) > 0 && len(acked) == 0 { - return fmt.Errorf("failed to acknowledge any messages") - } - - return nil -} - -func runMailReply(cmd *cobra.Command, args []string) error { - CheckReadonly("mail reply") - - messageID := args[0] - sender := config.GetIdentity(mailIdentity) - - // Get the original message - var originalMsg *types.Issue - - if daemonClient != nil { - resp, err := daemonClient.Show(&rpc.ShowArgs{ID: messageID}) - if err != nil { - return fmt.Errorf("failed to get original message: %w", err) - } - if err := json.Unmarshal(resp.Data, &originalMsg); err != nil { - return fmt.Errorf("parsing response: %w", err) - } - } else { - var err error - originalMsg, err = store.GetIssue(rootCtx, messageID) - if err != nil { - return fmt.Errorf("failed to get original message: %w", err) - } - } - - if originalMsg == nil { - return fmt.Errorf("message not found: %s", messageID) - } - - if originalMsg.IssueType != types.TypeMessage { - return fmt.Errorf("%s is not a message (type: %s)", messageID, originalMsg.IssueType) - } - - // Determine recipient: reply goes to the original sender - recipient := originalMsg.Sender - if recipient == "" { - return fmt.Errorf("original message has no sender, cannot determine reply recipient") - } - - // Build reply subject - subject := originalMsg.Title - if !strings.HasPrefix(strings.ToLower(subject), "re:") { - subject = "Re: " + subject - } - - // Determine priority - priority := 2 // default: normal - if mailUrgent { - priority = 0 - } - - // Create the reply message - now := time.Now() - reply := &types.Issue{ - Title: subject, - Description: mailBody, - Status: types.StatusOpen, - Priority: priority, - IssueType: types.TypeMessage, - Assignee: recipient, - Sender: sender, - Ephemeral: true, - // NOTE: RepliesTo now handled via dependency API (Decision 004) - CreatedAt: now, - UpdatedAt: now, - } - _ = messageID // RepliesTo handled via CreateArgs.RepliesTo -> server creates dependency - - if daemonClient != nil { - // Daemon mode - create reply with all messaging fields - createArgs := &rpc.CreateArgs{ - Title: reply.Title, - Description: reply.Description, - IssueType: string(types.TypeMessage), - Priority: priority, - Assignee: recipient, - Sender: sender, - Ephemeral: true, - RepliesTo: messageID, // Thread link - } - - resp, err := daemonClient.Create(createArgs) - if err != nil { - return fmt.Errorf("failed to send reply: %w", err) - } - - var createdIssue types.Issue - if err := json.Unmarshal(resp.Data, &createdIssue); err != nil { - return fmt.Errorf("parsing response: %w", err) - } - - if jsonOutput { - result := map[string]interface{}{ - "id": createdIssue.ID, - "to": recipient, - "from": sender, - "subject": subject, - "replies_to": messageID, - "priority": priority, - "timestamp": createdIssue.CreatedAt, - } - encoder := json.NewEncoder(os.Stdout) - encoder.SetIndent("", " ") - return encoder.Encode(result) - } - - fmt.Printf("Reply sent: %s\n", createdIssue.ID) - fmt.Printf(" To: %s\n", recipient) - fmt.Printf(" Re: %s\n", messageID) - if mailUrgent { - fmt.Printf(" Priority: URGENT\n") - } - return nil - } - - // Direct mode - if err := store.CreateIssue(rootCtx, reply, actor); err != nil { - return fmt.Errorf("failed to send reply: %w", err) - } - - // Trigger auto-flush - if flushManager != nil { - flushManager.MarkDirty(false) - } - - // Fire message hook for GGT notifications - if hookRunner != nil { - hookRunner.Run(hooks.EventMessage, reply) - } - - if jsonOutput { - result := map[string]interface{}{ - "id": reply.ID, - "to": recipient, - "from": sender, - "subject": subject, - "replies_to": messageID, - "priority": priority, - "timestamp": reply.CreatedAt, - } - encoder := json.NewEncoder(os.Stdout) - encoder.SetIndent("", " ") - return encoder.Encode(result) - } - - fmt.Printf("Reply sent: %s\n", reply.ID) - fmt.Printf(" To: %s\n", recipient) - fmt.Printf(" Re: %s\n", messageID) - if mailUrgent { - fmt.Printf(" Priority: URGENT\n") - } - - return nil -} diff --git a/cmd/bd/mail_test.go b/cmd/bd/mail_test.go deleted file mode 100644 index 1986d44e..00000000 --- a/cmd/bd/mail_test.go +++ /dev/null @@ -1,392 +0,0 @@ -package main - -import ( - "context" - "testing" - "time" - - "github.com/steveyegge/beads/internal/types" -) - -func TestMailSendAndInbox(t *testing.T) { - tmpDir := t.TempDir() - testStore := newTestStore(t, tmpDir+"/.beads/beads.db") - ctx := context.Background() - - // Set up global state - oldStore := store - oldRootCtx := rootCtx - oldActor := actor - store = testStore - rootCtx = ctx - actor = "test-user" - defer func() { - store = oldStore - rootCtx = oldRootCtx - actor = oldActor - }() - - // Create a message (simulating mail send) - now := time.Now() - msg := &types.Issue{ - Title: "Test Subject", - Description: "Test message body", - Status: types.StatusOpen, - Priority: 2, - IssueType: types.TypeMessage, - Assignee: "worker-1", - Sender: "manager", - Ephemeral: true, - CreatedAt: now, - UpdatedAt: now, - } - - if err := testStore.CreateIssue(ctx, msg, actor); err != nil { - t.Fatalf("Failed to create message: %v", err) - } - - // Query inbox for worker-1 - messageType := types.TypeMessage - openStatus := types.StatusOpen - assignee := "worker-1" - filter := types.IssueFilter{ - IssueType: &messageType, - Status: &openStatus, - Assignee: &assignee, - } - - messages, err := testStore.SearchIssues(ctx, "", filter) - if err != nil { - t.Fatalf("SearchIssues failed: %v", err) - } - - if len(messages) != 1 { - t.Fatalf("Expected 1 message, got %d", len(messages)) - } - - if messages[0].Title != "Test Subject" { - t.Errorf("Title = %q, want %q", messages[0].Title, "Test Subject") - } - if messages[0].Sender != "manager" { - t.Errorf("Sender = %q, want %q", messages[0].Sender, "manager") - } - if !messages[0].Ephemeral { - t.Error("Ephemeral should be true") - } -} - -func TestMailInboxEmpty(t *testing.T) { - tmpDir := t.TempDir() - testStore := newTestStore(t, tmpDir+"/.beads/beads.db") - ctx := context.Background() - - // Query inbox for non-existent user - messageType := types.TypeMessage - openStatus := types.StatusOpen - assignee := "nobody" - filter := types.IssueFilter{ - IssueType: &messageType, - Status: &openStatus, - Assignee: &assignee, - } - - messages, err := testStore.SearchIssues(ctx, "", filter) - if err != nil { - t.Fatalf("SearchIssues failed: %v", err) - } - - if len(messages) != 0 { - t.Errorf("Expected 0 messages, got %d", len(messages)) - } -} - -func TestMailAck(t *testing.T) { - tmpDir := t.TempDir() - testStore := newTestStore(t, tmpDir+"/.beads/beads.db") - ctx := context.Background() - - // Create a message - now := time.Now() - msg := &types.Issue{ - Title: "Ack Test", - Description: "Test body", - Status: types.StatusOpen, - Priority: 2, - IssueType: types.TypeMessage, - Assignee: "recipient", - Sender: "sender", - Ephemeral: true, - CreatedAt: now, - UpdatedAt: now, - } - - if err := testStore.CreateIssue(ctx, msg, "test"); err != nil { - t.Fatalf("Failed to create message: %v", err) - } - - // Acknowledge (close) the message - if err := testStore.CloseIssue(ctx, msg.ID, "acknowledged", "test"); err != nil { - t.Fatalf("Failed to close message: %v", err) - } - - // Verify it's closed - updated, err := testStore.GetIssue(ctx, msg.ID) - if err != nil { - t.Fatalf("GetIssue failed: %v", err) - } - - if updated.Status != types.StatusClosed { - t.Errorf("Status = %q, want %q", updated.Status, types.StatusClosed) - } - - // Verify it no longer appears in inbox - messageType := types.TypeMessage - openStatus := types.StatusOpen - assignee := "recipient" - filter := types.IssueFilter{ - IssueType: &messageType, - Status: &openStatus, - Assignee: &assignee, - } - - messages, err := testStore.SearchIssues(ctx, "", filter) - if err != nil { - t.Fatalf("SearchIssues failed: %v", err) - } - - if len(messages) != 0 { - t.Errorf("Expected 0 messages in inbox after ack, got %d", len(messages)) - } -} - -func TestMailReply(t *testing.T) { - tmpDir := t.TempDir() - testStore := newTestStore(t, tmpDir+"/.beads/beads.db") - ctx := context.Background() - - // Create original message - now := time.Now() - original := &types.Issue{ - Title: "Original Subject", - Description: "Original body", - Status: types.StatusOpen, - Priority: 2, - IssueType: types.TypeMessage, - Assignee: "worker", - Sender: "manager", - Ephemeral: true, - CreatedAt: now, - UpdatedAt: now, - } - - if err := testStore.CreateIssue(ctx, original, "test"); err != nil { - t.Fatalf("Failed to create original message: %v", err) - } - - // Create reply (thread link now done via dependencies per Decision 004) - reply := &types.Issue{ - Title: "Re: Original Subject", - Description: "Reply body", - Status: types.StatusOpen, - Priority: 2, - IssueType: types.TypeMessage, - Assignee: "manager", // Reply goes to original sender - Sender: "worker", - Ephemeral: true, - CreatedAt: now.Add(time.Minute), - UpdatedAt: now.Add(time.Minute), - } - - if err := testStore.CreateIssue(ctx, reply, "test"); err != nil { - t.Fatalf("Failed to create reply: %v", err) - } - - // Add replies-to dependency (thread link per Decision 004) - dep := &types.Dependency{ - IssueID: reply.ID, - DependsOnID: original.ID, - Type: types.DepRepliesTo, - } - if err := testStore.AddDependency(ctx, dep, "test"); err != nil { - t.Fatalf("Failed to add replies-to dependency: %v", err) - } - - // Verify reply has correct thread link via dependencies - deps, err := testStore.GetDependenciesWithMetadata(ctx, reply.ID) - if err != nil { - t.Fatalf("GetDependenciesWithMetadata failed: %v", err) - } - - var foundReplyLink bool - for _, d := range deps { - if d.DependencyType == types.DepRepliesTo && d.ID == original.ID { - foundReplyLink = true - break - } - } - if !foundReplyLink { - t.Errorf("Reply missing replies-to link to original message") - } -} - -func TestMailPriority(t *testing.T) { - tmpDir := t.TempDir() - testStore := newTestStore(t, tmpDir+"/.beads/beads.db") - ctx := context.Background() - - // Create messages with different priorities - now := time.Now() - messages := []struct { - title string - priority int - }{ - {"Normal message", 2}, - {"Urgent message", 0}, - {"High priority", 1}, - } - - for i, m := range messages { - msg := &types.Issue{ - Title: m.title, - Description: "Body", - Status: types.StatusOpen, - Priority: m.priority, - IssueType: types.TypeMessage, - Assignee: "inbox", - Sender: "sender", - Ephemeral: true, - CreatedAt: now.Add(time.Duration(i) * time.Minute), - UpdatedAt: now.Add(time.Duration(i) * time.Minute), - } - if err := testStore.CreateIssue(ctx, msg, "test"); err != nil { - t.Fatalf("Failed to create message %d: %v", i, err) - } - } - - // Query all messages - messageType := types.TypeMessage - openStatus := types.StatusOpen - assignee := "inbox" - filter := types.IssueFilter{ - IssueType: &messageType, - Status: &openStatus, - Assignee: &assignee, - } - - results, err := testStore.SearchIssues(ctx, "", filter) - if err != nil { - t.Fatalf("SearchIssues failed: %v", err) - } - - if len(results) != 3 { - t.Fatalf("Expected 3 messages, got %d", len(results)) - } - - // Verify we can filter by priority - urgentPriority := 0 - urgentFilter := types.IssueFilter{ - IssueType: &messageType, - Status: &openStatus, - Assignee: &assignee, - Priority: &urgentPriority, - } - - urgent, err := testStore.SearchIssues(ctx, "", urgentFilter) - if err != nil { - t.Fatalf("SearchIssues failed: %v", err) - } - - if len(urgent) != 1 { - t.Errorf("Expected 1 urgent message, got %d", len(urgent)) - } -} - -func TestMailTypeValidation(t *testing.T) { - tmpDir := t.TempDir() - testStore := newTestStore(t, tmpDir+"/.beads/beads.db") - ctx := context.Background() - - // Create a regular issue (not a message) - now := time.Now() - task := &types.Issue{ - Title: "Regular Task", - Status: types.StatusOpen, - Priority: 2, - IssueType: types.TypeTask, - CreatedAt: now, - UpdatedAt: now, - } - - if err := testStore.CreateIssue(ctx, task, "test"); err != nil { - t.Fatalf("Failed to create task: %v", err) - } - - // Query for messages should not return the task - messageType := types.TypeMessage - filter := types.IssueFilter{ - IssueType: &messageType, - } - - messages, err := testStore.SearchIssues(ctx, "", filter) - if err != nil { - t.Fatalf("SearchIssues failed: %v", err) - } - - for _, m := range messages { - if m.ID == task.ID { - t.Errorf("Task %s should not appear in message query", task.ID) - } - } -} - -func TestMailSenderField(t *testing.T) { - tmpDir := t.TempDir() - testStore := newTestStore(t, tmpDir+"/.beads/beads.db") - ctx := context.Background() - - // Create messages from different senders - now := time.Now() - senders := []string{"alice", "bob", "charlie"} - - for i, sender := range senders { - msg := &types.Issue{ - Title: "Message from " + sender, - Description: "Body", - Status: types.StatusOpen, - Priority: 2, - IssueType: types.TypeMessage, - Assignee: "inbox", - Sender: sender, - Ephemeral: true, - CreatedAt: now.Add(time.Duration(i) * time.Minute), - UpdatedAt: now.Add(time.Duration(i) * time.Minute), - } - if err := testStore.CreateIssue(ctx, msg, "test"); err != nil { - t.Fatalf("Failed to create message from %s: %v", sender, err) - } - } - - // Query all messages and verify sender - messageType := types.TypeMessage - filter := types.IssueFilter{ - IssueType: &messageType, - } - - messages, err := testStore.SearchIssues(ctx, "", filter) - if err != nil { - t.Fatalf("SearchIssues failed: %v", err) - } - - senderSet := make(map[string]bool) - for _, m := range messages { - if m.Sender != "" { - senderSet[m.Sender] = true - } - } - - for _, s := range senders { - if !senderSet[s] { - t.Errorf("Sender %q not found in messages", s) - } - } -} diff --git a/docs/graph-links.md b/docs/graph-links.md index 6f3c6a39..100ae449 100644 --- a/docs/graph-links.md +++ b/docs/graph-links.md @@ -9,7 +9,8 @@ Beads supports several types of links between issues to create a knowledge graph Creates message threads, similar to email or chat conversations. **Created by:** -- `bd mail reply ` command +- `gt mail reply ` command (Gas Town handles messaging) +- `bd dep add --type replies_to` (manual linking) **Use cases:** - Agent-to-agent message threads @@ -19,19 +20,19 @@ Creates message threads, similar to email or chat conversations. **Example:** ```bash -# Original message -bd mail send worker-1 -s "Review needed" -m "Please review bd-xyz" -# Creates: bd-a1b2 +# Original message (via Gas Town) +gt mail send gastown/worker -s "Review needed" -m "Please review gt-xyz" +# Creates: gt-a1b2 -# Reply (automatically sets replies_to: bd-a1b2) -bd mail reply bd-a1b2 -m "Done! Approved with minor comments." -# Creates: bd-c3d4 with replies_to: bd-a1b2 +# Reply (automatically sets replies_to) +gt mail reply gt-a1b2 -m "Done! Approved with minor comments." +# Creates: gt-c3d4 with replies_to: gt-a1b2 ``` **Viewing threads:** ```bash -bd show bd-a1b2 --thread +bd show gt-a1b2 --thread ``` ### relates_to - Loose Associations @@ -255,12 +256,12 @@ bd supersede bd-rfc2 --with bd-rfc3 ### Message Threading -Build conversation chains: +Build conversation chains (via Gas Town): ```bash -bd mail send dev -s "Question" -m "How does X work?" -bd mail reply bd-q1 -m "X works by..." -bd mail reply bd-q1.reply -m "Thanks!" +gt mail send gastown/dev -s "Question" -m "How does X work?" +gt mail reply gt-q1 -m "X works by..." +gt mail reply gt-q1.reply -m "Thanks!" ``` ## Best Practices diff --git a/docs/messaging.md b/docs/messaging.md deleted file mode 100644 index b3428ad0..00000000 --- a/docs/messaging.md +++ /dev/null @@ -1,254 +0,0 @@ -# Beads Messaging System - -Beads provides a built-in messaging system for inter-agent communication. Messages are stored as beads issues with type `message`, enabling git-native communication without external services. - -## Overview - -The messaging system enables: -- **Agent-to-agent communication** - Send messages between workers -- **Thread tracking** - Replies link back to original messages -- **Priority signaling** - Mark messages as urgent (P0) or routine -- **Ephemeral cleanup** - Messages can be bulk-deleted after completion - -## Identity Configuration - -Before using mail commands, configure your identity: - -### Environment Variable - -```bash -export BEADS_IDENTITY="worker-1" -``` - -### Config File - -Add to `.beads/config.json`: - -```json -{ - "identity": "worker-1" -} -``` - -### Priority - -1. `--identity` flag (if provided) -2. `BEADS_IDENTITY` environment variable -3. `.beads/config.json` identity field -4. System username (fallback) - -## Commands - -### Send a Message - -```bash -bd mail send -s -m -``` - -**Options:** -- `-s, --subject` - Message subject (required) -- `-m, --body` - Message body (required) -- `--urgent` - Set priority=0 (urgent) -- `--identity` - Override sender identity - -**Examples:** - -```bash -# Basic message -bd mail send worker-1 -s "Task complete" -m "Finished bd-xyz" - -# Urgent message -bd mail send manager -s "Blocked!" -m "Need credentials for deploy" --urgent - -# With custom identity -bd mail send worker-2 -s "Handoff" -m "Your turn on bd-abc" --identity refinery -``` - -### Check Your Inbox - -```bash -bd mail inbox -``` - -Lists all open messages addressed to your identity. - -**Options:** -- `--from ` - Filter by sender -- `--priority ` - Filter by priority (0-4) - -**Output:** - -``` -Inbox for worker-1 (2 messages): - - bd-a1b2: Task assigned [URGENT] - From: manager (5m ago) - - bd-c3d4: FYI: Design doc updated - From: worker-2 (1h ago) - Re: bd-x7y8 -``` - -### Read a Message - -```bash -bd mail read -``` - -Displays full message content. Does NOT mark as read. - -**Example output:** - -``` -────────────────────────────────────────────────────────────────── -ID: bd-a1b2 -From: manager -To: worker-1 -Subject: Task assigned -Time: 2025-12-16 10:30:45 -Priority: P0 -Status: open -────────────────────────────────────────────────────────────────── - -Please prioritize bd-xyz. It's blocking the release. - -Let me know if you need anything. -``` - -### Acknowledge (Mark as Read) - -```bash -bd mail ack [id2...] -``` - -Closes messages to mark them as acknowledged. - -**Examples:** - -```bash -# Single message -bd mail ack bd-a1b2 - -# Multiple messages -bd mail ack bd-a1b2 bd-c3d4 bd-e5f6 -``` - -### Reply to a Message - -```bash -bd mail reply -m -``` - -Creates a threaded reply to an existing message. - -**Options:** -- `-m, --body` - Reply body (required) -- `--urgent` - Set priority=0 -- `--identity` - Override sender identity - -**Behavior:** -- Sets `replies_to` to original message ID -- Sends to original message's sender -- Prefixes subject with "Re:" if not already present - -**Example:** - -```bash -bd mail reply bd-a1b2 -m "On it! Should be done by EOD." -``` - -## Message Storage - -Messages are stored as issues with these fields: - -| Field | Description | -|-------|-------------| -| `type` | `message` | -| `title` | Subject line | -| `description` | Message body | -| `assignee` | Recipient identity | -| `sender` | Sender identity | -| `priority` | 0 (urgent) to 4 (routine), default 2 | -| `ephemeral` | `true` - can be bulk-deleted | -| `replies_to` | ID of parent message (for threads) | -| `status` | `open` (unread) / `closed` (read) | - -## Cleanup - -Messages are ephemeral by default and can be cleaned up: - -```bash -# Preview ephemeral message cleanup -bd cleanup --ephemeral --dry-run - -# Delete all closed ephemeral messages -bd cleanup --ephemeral --force -``` - -## Hooks - -The messaging system fires hooks when messages are sent: - -**Hook file:** `.beads/hooks/on_message` - -The hook receives: -- **Arg 1:** Issue ID -- **Arg 2:** Event type (`message`) -- **Stdin:** Full issue JSON - -**Example hook:** - -```bash -#!/bin/sh -# .beads/hooks/on_message - -ISSUE_ID="$1" -EVENT="$2" - -# Parse assignee from JSON stdin -ASSIGNEE=$(cat | jq -r '.assignee') - -# Notify recipient (example: send to external system) -curl -X POST "https://example.com/notify" \ - -d "to=$ASSIGNEE&message=$ISSUE_ID" -``` - -Make the hook executable: - -```bash -chmod +x .beads/hooks/on_message -``` - -## JSON Output - -All commands support `--json` for programmatic use: - -```bash -bd mail inbox --json -bd mail read bd-a1b2 --json -bd mail send worker-1 -s "Hi" -m "Test" --json -``` - -## Thread Visualization - -Use `bd show --thread` to view message threads: - -```bash -bd show bd-c3d4 --thread -``` - -This shows the full conversation chain via `replies_to` links. - -## Best Practices - -1. **Use descriptive subjects** - Recipients scan subjects first -2. **Mark urgent sparingly** - P0 should be reserved for blockers -3. **Acknowledge promptly** - Keep inbox clean -4. **Clean up after sprints** - Run `bd cleanup --ephemeral` periodically -5. **Configure identity** - Use `BEADS_IDENTITY` for consistent sender names - -## See Also - -- [Graph Links](graph-links.md) - Other link types (relates_to, duplicates, supersedes) -- [Hooks](EXTENDING.md) - Custom hook scripts -- [Config](CONFIG.md) - Configuration options diff --git a/internal/hooks/hooks.go b/internal/hooks/hooks.go index fc61b22b..240c8456 100644 --- a/internal/hooks/hooks.go +++ b/internal/hooks/hooks.go @@ -12,18 +12,16 @@ import ( // Event types const ( - EventCreate = "create" - EventUpdate = "update" - EventClose = "close" - EventMessage = "message" + EventCreate = "create" + EventUpdate = "update" + EventClose = "close" ) // Hook file names const ( - HookOnCreate = "on_create" - HookOnUpdate = "on_update" - HookOnClose = "on_close" - HookOnMessage = "on_message" + HookOnCreate = "on_create" + HookOnUpdate = "on_update" + HookOnClose = "on_close" ) // Runner handles hook execution @@ -120,8 +118,6 @@ func eventToHook(event string) string { return HookOnUpdate case EventClose: return HookOnClose - case EventMessage: - return HookOnMessage default: return "" } diff --git a/internal/hooks/hooks_test.go b/internal/hooks/hooks_test.go index 53f60b45..db4204e5 100644 --- a/internal/hooks/hooks_test.go +++ b/internal/hooks/hooks_test.go @@ -44,7 +44,6 @@ func TestEventToHook(t *testing.T) { {EventCreate, HookOnCreate}, {EventUpdate, HookOnUpdate}, {EventClose, HookOnClose}, - {EventMessage, HookOnMessage}, {"unknown", ""}, {"", ""}, } @@ -182,7 +181,7 @@ echo "$1 $2" > ` + outputFile func TestRunSync_ReceivesJSON(t *testing.T) { tmpDir := t.TempDir() - hookPath := filepath.Join(tmpDir, HookOnMessage) + hookPath := filepath.Join(tmpDir, HookOnCreate) outputFile := filepath.Join(tmpDir, "stdin.txt") // Create a hook that captures stdin @@ -194,13 +193,12 @@ cat > ` + outputFile runner := NewRunner(tmpDir) issue := &types.Issue{ - ID: "bd-msg", - Title: "Test Message", - Sender: "alice", + ID: "bd-test", + Title: "Test Issue", Assignee: "bob", } - err := runner.RunSync(EventMessage, issue) + err := runner.RunSync(EventCreate, issue) if err != nil { t.Errorf("RunSync returned error: %v", err) } @@ -380,7 +378,6 @@ func TestAllHookEvents(t *testing.T) { {EventCreate, HookOnCreate}, {EventUpdate, HookOnUpdate}, {EventClose, HookOnClose}, - {EventMessage, HookOnMessage}, } for _, e := range events {