refactor: remove bd mail commands - mail moves to Gas Town

Mail is orchestration, not data plane. The bd mail commands were just
sugar over bd create/list/show with type=message. Gas Town (gt) now
owns mail routing and delivery.

Removed:
- cmd/bd/mail.go - all mail subcommands (send, inbox, read, ack, reply)
- cmd/bd/mail_test.go - mail command tests
- docs/messaging.md - dedicated mail documentation
- EventMessage hook - no longer triggered

Kept (data plane):
- type=message as valid issue type
- Sender, Ephemeral fields on issues
- replies_to dependency type for threading

Coordination: gt-9xg implements native mail in Gas Town.

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Steve Yegge
2025-12-20 17:56:46 -08:00
parent 79191bf7d1
commit 4fa0866fcf
8 changed files with 31 additions and 1447 deletions

View File

@@ -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-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-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-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-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-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-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-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-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-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-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-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"} {"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-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-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-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-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-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-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-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-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-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-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-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"} {"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"}

View File

@@ -19,7 +19,7 @@ the overseer, not as part of a swarm.
**Your mail address:** `beads/dave` **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 ## Gas Town Architecture
@@ -38,7 +38,7 @@ Town (/Users/stevey/gt)
## Key Commands ## Key Commands
### Finding Work ### 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 ready` - Available issues
- `bd list --status=in_progress` - Your active work - `bd list --status=in_progress` - Your active work
@@ -49,8 +49,8 @@ Town (/Users/stevey/gt)
- `bd sync` - Sync beads changes - `bd sync` - Sync beads changes
### Communication ### Communication
- `bd mail send mayor -s "Subject" -m "Message"` - To Mayor - `gt mail send mayor/ -s "Subject" -m "Message"` - To Mayor
- `bd mail send beads-dave -s "Subject" -m "Message"` - To yourself (handoff) - `gt mail send beads/dave -s "Subject" -m "Message"` - To yourself (handoff)
## Beads Database ## 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? 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 <recipient> -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 ### Deletion Tracking
When issues are deleted (via `bd delete` or `bd cleanup`), they are recorded in `.beads/deletions.jsonl`. This manifest: When issues are deleted (via `bd delete` or `bd cleanup`), they are recorded in `.beads/deletions.jsonl`. This manifest:

View File

@@ -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 <recipient> -s <subject> -m <body>",
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 <id>",
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 <id> [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 <id> -m <body>",
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
}

View File

@@ -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)
}
}
}

View File

@@ -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. Creates message threads, similar to email or chat conversations.
**Created by:** **Created by:**
- `bd mail reply <id>` command - `gt mail reply <id>` command (Gas Town handles messaging)
- `bd dep add <new-id> <original-id> --type replies_to` (manual linking)
**Use cases:** **Use cases:**
- Agent-to-agent message threads - Agent-to-agent message threads
@@ -19,19 +20,19 @@ Creates message threads, similar to email or chat conversations.
**Example:** **Example:**
```bash ```bash
# Original message # Original message (via Gas Town)
bd mail send worker-1 -s "Review needed" -m "Please review bd-xyz" gt mail send gastown/worker -s "Review needed" -m "Please review gt-xyz"
# Creates: bd-a1b2 # Creates: gt-a1b2
# Reply (automatically sets replies_to: bd-a1b2) # Reply (automatically sets replies_to)
bd mail reply bd-a1b2 -m "Done! Approved with minor comments." gt mail reply gt-a1b2 -m "Done! Approved with minor comments."
# Creates: bd-c3d4 with replies_to: bd-a1b2 # Creates: gt-c3d4 with replies_to: gt-a1b2
``` ```
**Viewing threads:** **Viewing threads:**
```bash ```bash
bd show bd-a1b2 --thread bd show gt-a1b2 --thread
``` ```
### relates_to - Loose Associations ### relates_to - Loose Associations
@@ -255,12 +256,12 @@ bd supersede bd-rfc2 --with bd-rfc3
### Message Threading ### Message Threading
Build conversation chains: Build conversation chains (via Gas Town):
```bash ```bash
bd mail send dev -s "Question" -m "How does X work?" gt mail send gastown/dev -s "Question" -m "How does X work?"
bd mail reply bd-q1 -m "X works by..." gt mail reply gt-q1 -m "X works by..."
bd mail reply bd-q1.reply -m "Thanks!" gt mail reply gt-q1.reply -m "Thanks!"
``` ```
## Best Practices ## Best Practices

View File

@@ -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 <recipient> -s <subject> -m <body>
```
**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 <sender>` - Filter by sender
- `--priority <n>` - 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 <id>
```
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 <id> [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 <id> -m <body>
```
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

View File

@@ -12,18 +12,16 @@ import (
// Event types // Event types
const ( const (
EventCreate = "create" EventCreate = "create"
EventUpdate = "update" EventUpdate = "update"
EventClose = "close" EventClose = "close"
EventMessage = "message"
) )
// Hook file names // Hook file names
const ( const (
HookOnCreate = "on_create" HookOnCreate = "on_create"
HookOnUpdate = "on_update" HookOnUpdate = "on_update"
HookOnClose = "on_close" HookOnClose = "on_close"
HookOnMessage = "on_message"
) )
// Runner handles hook execution // Runner handles hook execution
@@ -120,8 +118,6 @@ func eventToHook(event string) string {
return HookOnUpdate return HookOnUpdate
case EventClose: case EventClose:
return HookOnClose return HookOnClose
case EventMessage:
return HookOnMessage
default: default:
return "" return ""
} }

View File

@@ -44,7 +44,6 @@ func TestEventToHook(t *testing.T) {
{EventCreate, HookOnCreate}, {EventCreate, HookOnCreate},
{EventUpdate, HookOnUpdate}, {EventUpdate, HookOnUpdate},
{EventClose, HookOnClose}, {EventClose, HookOnClose},
{EventMessage, HookOnMessage},
{"unknown", ""}, {"unknown", ""},
{"", ""}, {"", ""},
} }
@@ -182,7 +181,7 @@ echo "$1 $2" > ` + outputFile
func TestRunSync_ReceivesJSON(t *testing.T) { func TestRunSync_ReceivesJSON(t *testing.T) {
tmpDir := t.TempDir() tmpDir := t.TempDir()
hookPath := filepath.Join(tmpDir, HookOnMessage) hookPath := filepath.Join(tmpDir, HookOnCreate)
outputFile := filepath.Join(tmpDir, "stdin.txt") outputFile := filepath.Join(tmpDir, "stdin.txt")
// Create a hook that captures stdin // Create a hook that captures stdin
@@ -194,13 +193,12 @@ cat > ` + outputFile
runner := NewRunner(tmpDir) runner := NewRunner(tmpDir)
issue := &types.Issue{ issue := &types.Issue{
ID: "bd-msg", ID: "bd-test",
Title: "Test Message", Title: "Test Issue",
Sender: "alice",
Assignee: "bob", Assignee: "bob",
} }
err := runner.RunSync(EventMessage, issue) err := runner.RunSync(EventCreate, issue)
if err != nil { if err != nil {
t.Errorf("RunSync returned error: %v", err) t.Errorf("RunSync returned error: %v", err)
} }
@@ -380,7 +378,6 @@ func TestAllHookEvents(t *testing.T) {
{EventCreate, HookOnCreate}, {EventCreate, HookOnCreate},
{EventUpdate, HookOnUpdate}, {EventUpdate, HookOnUpdate},
{EventClose, HookOnClose}, {EventClose, HookOnClose},
{EventMessage, HookOnMessage},
} }
for _, e := range events { for _, e := range events {