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:
@@ -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"}
|
||||||
|
|||||||
49
CLAUDE.md
49
CLAUDE.md
@@ -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:
|
||||||
|
|||||||
727
cmd/bd/mail.go
727
cmd/bd/mail.go
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -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 ""
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user