The handoff mail bead was created with un-normalized assignee
(e.g., gastown/crew/holden) but mailbox queries use normalized identity
(gastown/holden), causing self-mail to be invisible to the inbox.
Export addressToIdentity as AddressToIdentity and call it in
sendHandoffMail() to normalize the agent identity before storing,
matching the normalization performed in Router.sendToSingle().
Fix handoff mail delivery (hq-snp8)
When messages are sent to a channel, subscribers now receive a copy
in their inbox with [channel:name] prefix in the subject.
Closes: gt-3rldf6
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
detectTownRoot() was only checking for mayor/town.json, but some
workspaces only have the mayor/ directory without town.json.
This caused mail routing to fail silently - messages showed
success but werent persisted because townRoot was empty.
Now uses workspace.Find() which supports both primary marker
(mayor/town.json) and secondary marker (mayor/ directory).
Fixes: gt-6v7z89
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add CreatedAt timestamp to CreateGroupBead() in beads_group.go
- Add CreatedAt timestamp to CreateChannelBead() in beads_channel.go
- Check channel status before sending in router.go sendToChannel()
- Reject sends to closed channels with appropriate error message
Closes: gt-yibjdm, gt-bv2f97
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The router was missing support for beads-native channel addresses.
When mail_send.go resolved an address to RecipientChannel, it set
msg.To to "channel:<name>" but router.Send() had no handler for this
prefix, causing channel messages to fail silently.
Added:
- isChannelAddress() and parseChannelName() helper functions
- sendToChannel() method that creates messages with proper channel:
labels for channel queries
- Channel validation before sending
- Retention enforcement after message creation
Also updated docs/beads-native-messaging.md with more comprehensive
documentation of the beads-native messaging system.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Simplified notification delivery to always use NudgeSession, since all
sessions are Claude Code (or similar AI sessions), never plain terminals.
This removes unnecessary complexity and the IsClaudeRunning check.
Extracted duplicate bd command execution pattern from mailbox.go and
router.go into a new helper function in bd.go. This reduces code
duplication and provides consistent error handling via the bdError type.
Changes:
- Added internal/mail/bd.go with runBdCommand helper and bdError type
- Refactored 5 functions in mailbox.go to use runBdCommand
- Refactored 5 functions in router.go to use runBdCommand
- Net reduction of 55 lines of code
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Reverts the session naming changes from PR #70. Multi-town support
on a single machine is not a real use case - rigs provide project
isolation, and true isolation should use containers/VMs.
Changes:
- MayorSessionName() and DeaconSessionName() no longer take townName parameter
- ParseSessionName() handles simple gt-mayor and gt-deacon formats
- Removed Town field from AgentIdentity and AgentSession structs
- Updated all callers and tests
Generated with Claude Code
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Session names `gt-mayor` and `gt-deacon` were hardcoded, causing tmux
session name collisions when running multiple towns simultaneously.
Changed to `gt-{town}-mayor` and `gt-{town}-deacon` format (e.g.,
`gt-ai-mayor`) to allow concurrent multi-town operation.
Key changes:
- session.MayorSessionName() and DeaconSessionName() now take townName param
- Added workspace.GetTownName() helper to load town name from config
- Updated all callers in cmd/, daemon/, doctor/, mail/, rig/, templates/
- Updated tests with new session name format
- Bead IDs remain unchanged (already scoped by .beads/ directory)
Fixes#60🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Implements announce channel delivery in router.go:
- Add isAnnounceAddress() and parseAnnounceName() helpers
- Add ErrUnknownAnnounce error variable
- Add expandAnnounce() to load AnnounceConfig from messaging.json
- Add sendToAnnounce() for bulletin board delivery (single copy, no claiming)
- Add pruneAnnounce() for retention-based message cleanup
- Integrate announce routing in Send()
Announce channels store ONE copy of each message (unlike lists which fan-out).
Messages persist until retention limit is reached, with oldest messages
pruned automatically when limit is exceeded.
Also includes address helpers (gt-pn2fq dependency).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add announce address detection to internal/mail/router.go following
the same pattern as isListAddress/parseListName and isQueueAddress/
parseQueueName.
Added:
- isAnnounceAddress(address string) bool - returns true for 'announce:' prefix
- parseAnnounceName(address string) string - extracts channel name
- ErrUnknownAnnounce error variable
(gt-pn2fq)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Implements queue message delivery in internal/mail/router.go:
- Validates queue exists via expandQueue()
- Creates single message (no fan-out unlike lists)
- Stores in town-level beads with queue metadata label
- Uses queue:name as assignee for inbox filtering
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add queue expansion to internal/mail/router.go following the expandList() pattern:
- Add ErrUnknownQueue error for unknown queue names
- Add expandQueue() method to look up QueueConfig from messaging.json
- Add TestExpandQueue and TestExpandQueueNoTownRoot tests
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add queue address detection following the same pattern as list address
detection. Includes tests in router_test.go.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Implements @group address resolution for mail routing:
- @rig/<rigname>: All agents in a rig
- @town: All town-level agents (mayor, deacon)
- @witnesses, @dogs, @refineries: Role-based groups
- @crew/<rig>, @polecats/<rig>: Role+rig scoped groups
- @overseer: Human operator (uses overseer.json)
Resolution uses `bd list --type=agent` queries with
description filtering. Fan-out at send time creates
individual messages for each resolved recipient.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Implement static mailing list expansion for the mail system:
- Add list:name address syntax (e.g., list:oncall)
- Load lists from ~/gt/config/messaging.json
- Fan-out delivery: each list member gets their own message copy
- Clear error handling for unknown list names
- Add tests for list detection, parsing, and expansion
- Update gt mail send help text with list:name documentation
- Show recipients in output when sending to a list
Example:
gt mail send list:oncall -s "Alert" -m "System down"
# Expands to: mayor/, gastown/witness
# Creates 2 message copies
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Adds first-class support for the human overseer in Gas Town mail:
- New OverseerConfig in internal/config/overseer.go with identity
detection (git config, gh cli, environment)
- Overseer detected/saved on town install (mayor/overseer.json)
- Simplified detectSender(): GT_ROLE set = agent, else = overseer
- New overseer address alongside mayor/ and deacon/
- Added --cc flag to mail send for CC recipients
- Inbox now includes CC'd messages via label query
- gt status shows overseer identity and unread mail count
- New gt whoami command shows current mail identity
Generated with Claude Code
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add CreatedBy field to Issue struct (matches beads GH#748)
- Add Actor field to CreateOptions, pass --actor to bd create
- Add ActorString() method to RoleInfo for identity formatting
- Update all beads.Create() callers to pass Actor
- Update direct bd create exec calls with --actor:
- mail/router.go: uses sender identity
- patrol_helpers.go: uses role name
- doctor/patrol_check.go: uses "gt-doctor"
- rig/manager.go: uses "gt-rig-init"
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
All 156 instances of _ = error suppression in non-test code now have
explanatory comments documenting why the error is intentionally ignored.
Categories of intentional suppressions:
- non-fatal: session works without these - tmux environment setup
- non-fatal: theming failure does not affect operation - visual styling
- best-effort cleanup - defer cleanup on failure paths
- best-effort notification - mail/notifications that should not block
- best-effort interrupt - graceful shutdown attempts
- crypto/rand.Read only fails on broken system - random ID generation
- output errors non-actionable - fmt.Fprint to io.Writer
This addresses the silent failure and debugging concerns raised in the
issue by making the intentionality explicit in the code.
Generated with Claude Code https://claude.com/claude-code
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Removed dual-routing architecture that used separate .beads-wisp/ directory.
Now uses single .beads/ with --wisp flag passed to bd create.
Changes:
- router.go: Remove resolveWispDir(), simplify shouldBeWisp()
- mailbox.go: Remove wispDir field and dual-source query logic
- types.go: Rename Ephemeral to Wisp, remove MessageSource
- mail.go: Rename --ephemeral to --wisp flag
- spawn.go: Use Wisp field for lifecycle messages
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add dual-inbox architecture where ephemeral messages go to
.beads-wisp/.beads/ instead of .beads/. Lifecycle messages
(POLECAT_STARTED, NUDGE, etc.) auto-detect as ephemeral.
Changes:
- Add Ephemeral and Source fields to mail.Message
- Add --ephemeral flag to gt mail send
- Router auto-detects lifecycle message patterns
- Mailbox merges messages from both persistent and wisp storage
- Inbox displays (ephemeral) indicator for wisp messages
- Delete/archive works for both message types
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The mail router was incorrectly routing rig-level addresses
(e.g., gastown/crew/max) to {rig}/.beads instead of town beads.
This caused mail to be invisible when agents checked their inbox.
Two-level beads architecture:
- ALL mail uses town beads ({townRoot}/.beads)
- Rig-level beads are for project issues only
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The merge at 96c773f lost changes from 5791752 (beads-sync branch).
Re-implementing:
- router.go: Use bd create --type=message with labels
- mailbox.go: Use bd list/show/close instead of bd mail commands
- types.go: Add Pinned field to Message struct
Original work was in commits 5791752 and 4c060f4 on beads-sync.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Replace `bd mail send/inbox/read/ack` commands with `bd create/list/show/close`.
This separates the orchestration layer (gt) from the data plane (beads).
Changes:
- router.go: Use `bd create --type=message` instead of `bd mail send`
- mailbox.go: Use `bd list --type=message` and `bd show` for inbox/read
- types.go: Parse metadata from labels (from:, thread:, reply-to:)
- mail.go: Fix findBeadsWorkDir to prefer rig-level beads, fix crew address format
Messages are now stored as beads issues with type=message. Metadata (sender,
thread, reply-to) is stored in labels for retrieval.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Fix ~50 errcheck warnings across the codebase:
- Add explicit `_ =` for intentionally ignored error returns (cleanup,
best-effort operations, etc.)
- Use `defer func() { _ = ... }()` pattern for defer statements
- Handle tmux SetEnvironment, KillSession, SendKeysRaw returns
- Handle mail router.Send returns
- Handle os.RemoveAll, os.Rename in cleanup paths
- Handle rand.Read returns for ID generation
- Handle fmt.Fprint* returns when writing to io.Writer
- Fix for-select with single case to use for-range
- Handle cobra MarkFlagRequired returns
All tests pass. Code compiles without errors.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Replace tmux display-message (subtle status bar notification) with
send-keys to echo a visible banner to the terminal. This ensures
mail notifications are actually seen by agents.
Closes gt-vnp9
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Adds:
- gt mail send now triggers tmux notification for recipients
- Merge execution with config and retry logic
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add MessageType enum (task, scavenge, notification, reply)
- Expand Priority from 2 to 4 levels (low, normal, high, urgent)
- Add ThreadID and ReplyTo fields to Message struct
- Add --type and --reply-to flags to 'gt mail send'
- Add 'gt mail thread <id>' command to view conversation threads
- Update inbox/read display to show type and threading info
- Auto-generate thread IDs for new messages
- Reply messages inherit thread from original
Closes: gt-hgk
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add DisplayMessage/DisplayMessageDefault to tmux package for non-disruptive
status line notifications
- Change mail send to always notify recipients (not just high priority)
- Use display-message instead of send-keys to avoid disrupting agent input
- Support notifications for mayor, polecat, and refinery sessions
Closes gt-7lt
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The bd CLI renamed 'message' subcommand to 'mail'. Updated gt mail
to call the correct bd subcommand, and fixed body to use -m flag
instead of positional argument.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The beads command for messaging is 'bd message', not 'bd mail'.
Fixed 4 locations:
- mailbox.go: inbox, read, ack commands
- router.go: send command (also fixed arg order to match bd API)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Mail commands (send/inbox/read/delete) now wrap bd mail CLI
- Address translation: mayor/ → mayor, rig/polecat → rig-polecat
- Beads stores messages as type=message issues
- Legacy JSONL mode retained for crew workers (local mail)
- Refinery notifications use new mail interface
- Swarm landing notifications use new mail interface
Closes gt-u1j.6, gt-u1j.12
🤖 Generated with Claude Code
Co-Authored-By: Claude <noreply@anthropic.com>
- internal/mail: Message types with priority support
- internal/mail: Mailbox JSONL operations (list, get, append, delete)
- internal/mail: Router for address resolution and delivery
- gt mail send: Send messages to agents
- gt mail inbox: List messages (--unread, --json)
- gt mail read: Read and mark messages as read
- Address formats: mayor/, rig/, rig/polecat, rig/refinery
- High priority messages trigger tmux notification
- Auto-detect sender from GT_RIG/GT_POLECAT env vars
Closes gt-u1j.6, gt-u1j.12
Generated with Claude Code
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>