fix(beads): use hq- prefix for group/channel beads (town-level entities)

Groups and channels are town-level entities that span rigs, so they
should use the hq- prefix rather than gt- (rig-level).

Changes:
- GroupBeadID: gt-group- → hq-group-
- ChannelBeadID: gt-channel- → hq-channel-
- Add --force flag to bypass prefix validation (town beads may have
  mixed prefixes from test runs)
- Update tests and documentation

Also adds docs/beads-native-messaging.md documenting:
- New bead types (gt:group, gt:queue, gt:channel)
- CLI commands (gt mail group, gt mail channel)
- Address resolution logic
- Usage examples

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
gastown/crew/max
2026-01-14 22:19:00 -08:00
committed by Steve Yegge
parent e30e46a87a
commit cbbf566f06
4 changed files with 239 additions and 10 deletions

View File

@@ -0,0 +1,226 @@
# Beads-Native Messaging
This document describes the beads-native messaging system, which extends Gas Town's mail system with first-class support for groups, queues, and channels backed by beads (Git-native storage).
## Overview
Beads-native messaging introduces three new bead types that integrate with the existing mail system:
- **Groups** (`gt:group`) - Named distribution lists for multi-recipient delivery
- **Queues** (`gt:queue`) - Work queues where workers claim items
- **Channels** (`gt:channel`) - Pub/sub broadcast streams with retention policies
All three are stored as beads, providing Git-native storage, audit trails, and replication.
## Bead Types
### Groups (`gt:group`)
Groups are named collections of addresses used for mail distribution. Members can be:
- Direct agent addresses (`gastown/crew/max`)
- Wildcard patterns (`*/witness`, `gastown/*`)
- Nested group names (groups can contain other groups)
**ID Format:** `hq-group-<name>` (e.g., `hq-group-ops-team`)
**Fields:**
- `name` - Unique group name
- `members` - Comma-separated list of addresses/patterns
- `created_by` - Who created the group
- `created_at` - ISO 8601 timestamp
**Source:** `internal/beads/beads_group.go`
### Queues (`gt:queue`)
Queues are work queues where multiple workers can claim items. Messages sent to a queue are delivered once to a claiming worker.
**ID Format:** `gt-q-<name>` or `hq-q-<name>` (town-level)
**Fields:**
- `name` - Queue name
- `status` - `active`, `paused`, or `closed`
- `max_concurrency` - Maximum concurrent workers (0 = unlimited)
- `processing_order` - `fifo` or `priority`
- `available_count`, `processing_count`, `completed_count`, `failed_count` - Queue statistics
**Source:** `internal/beads/beads_queue.go`
### Channels (`gt:channel`)
Channels are pub/sub broadcast streams. Messages sent to a channel are retained according to the channel's retention policy and can be viewed by any subscriber.
**ID Format:** `hq-channel-<name>` (e.g., `hq-channel-alerts`)
**Fields:**
- `name` - Unique channel name
- `subscribers` - Comma-separated list of subscribed addresses
- `status` - `active` or `closed`
- `retention_count` - Number of messages to retain (0 = unlimited)
- `retention_hours` - Hours to retain messages (0 = forever)
- `created_by` - Who created the channel
- `created_at` - ISO 8601 timestamp
**Retention Enforcement:**
- On-write cleanup: When a message is posted, old messages are pruned if over the limit
- Patrol cleanup: Deacon patrol runs periodic cleanup with 10% buffer to avoid thrashing
**Source:** `internal/beads/beads_channel.go`
## CLI Commands
### Group Commands
```bash
# List all groups
gt mail group list [--json]
# Show group details
gt mail group show <name> [--json]
# Create a group with members
gt mail group create <name> [members...]
gt mail group create ops-team gastown/witness gastown/crew/max
gt mail group create ops-team --member gastown/witness --member gastown/crew/max
# Add member to existing group
gt mail group add <name> <member>
# Remove member from group
gt mail group remove <name> <member>
# Delete a group
gt mail group delete <name>
```
**Source:** `internal/cmd/mail_group.go`
### Channel Commands
```bash
# List all channels
gt mail channel [--json]
gt mail channel list [--json]
# View channel messages
gt mail channel <name> [--json]
gt mail channel show <name> [--json]
# Create a channel with retention policy
gt mail channel create <name> [--retain-count=N] [--retain-hours=N]
gt mail channel create alerts --retain-count=100
# Delete a channel
gt mail channel delete <name>
```
**Source:** `internal/cmd/mail_channel.go`
### Sending Messages
The `gt mail send` command supports all address types through the address resolver:
```bash
# Send to agent (direct)
gt mail send gastown/crew/max -s "Hello" -m "Message body"
# Send to group (expands to all members)
gt mail send ops-team -s "Alert" -m "Important message"
gt mail send group:ops-team -s "Alert" -m "Explicit group syntax"
# Send to queue (delivered to one claiming worker)
gt mail send queue:build-queue -s "Job" -m "Build request"
# Send to channel (broadcast, retained)
gt mail send channel:alerts -s "Alert" -m "System alert"
# Send to pattern (wildcards)
gt mail send "*/witness" -s "Witness alert" -m "All witnesses"
```
**Source:** `internal/cmd/mail_send.go`
## Address Resolution
The address resolver (`internal/mail/resolve.go`) determines how to route messages based on the address format:
### Resolution Order
1. **Explicit prefix** - If address starts with `group:`, `queue:`, or `channel:`, use that type directly
2. **Contains `/`** - Treat as agent address or pattern (direct delivery)
3. **Starts with `@`** - Check for beads group, then fall back to built-in patterns
4. **Name lookup** - Search in order: group → queue → channel
### Address Formats
| Format | Type | Example |
|--------|------|---------|
| `group:<name>` | Group | `group:ops-team` |
| `queue:<name>` | Queue | `queue:build-queue` |
| `channel:<name>` | Channel | `channel:alerts` |
| `<town>/<role>/<name>` | Agent | `gastown/crew/max` |
| `<town>/<role>` | Agent | `gastown/witness` |
| `*/<role>` | Pattern | `*/witness` (all witnesses) |
| `@<name>` | Group/Pattern | `@ops-team` |
### Conflict Handling
If a name matches multiple types (e.g., both a group and a channel named "alerts"), the resolver returns an error requiring an explicit prefix:
```
ambiguous address "alerts": matches multiple types. Use explicit prefix: group:alerts, channel:alerts
```
## Key Files
| File | Description |
|------|-------------|
| `internal/beads/beads_group.go` | Group bead CRUD operations |
| `internal/beads/beads_queue.go` | Queue bead CRUD operations |
| `internal/beads/beads_channel.go` | Channel bead CRUD + retention |
| `internal/mail/resolve.go` | Address resolution logic |
| `internal/cmd/mail_group.go` | Group CLI commands |
| `internal/cmd/mail_channel.go` | Channel CLI commands |
| `internal/cmd/mail_send.go` | Send command with resolver |
## Examples
### Create a Team Distribution Group
```bash
# Create group
gt mail group create dev-team gastown/crew/max gastown/crew/dennis
# Add another member
gt mail group add dev-team gastown/crew/george
# Send to entire team
gt mail send dev-team -s "Standup" -m "Daily standup in 5 minutes"
```
### Set Up a Build Alert Channel
```bash
# Create channel with retention
gt mail channel create build-alerts --retain-count=50
# Send build notifications
gt mail send channel:build-alerts -s "Build #123 passed" -m "All tests green"
# View channel history
gt mail channel build-alerts
```
### Nested Groups
```bash
# Create base groups
gt mail group create witnesses gastown/witness ranchero/witness
gt mail group create crew gastown/crew/max gastown/crew/dennis
# Create umbrella group that includes other groups
gt mail group create all-agents witnesses crew deacon/
# Send to everyone
gt mail send all-agents -s "Town meeting" -m "All hands meeting at noon"
```

View File

@@ -127,15 +127,15 @@ func ParseChannelFields(description string) *ChannelFields {
return fields return fields
} }
// ChannelBeadID returns the channel bead ID for a given channel name. // ChannelBeadID returns the bead ID for a channel name.
// Format: hq-channel-<name> for town-level channels (default). // Format: hq-channel-<name> (town-level, channels span rigs)
// Town-level channels are stored in the shared beads database.
func ChannelBeadID(name string) string { func ChannelBeadID(name string) string {
return "hq-channel-" + name return "hq-channel-" + name
} }
// CreateChannelBead creates a channel bead for pub/sub messaging. // CreateChannelBead creates a channel bead for pub/sub messaging.
// The ID format is: hq-channel-<name> (e.g., hq-channel-alerts) for town-level. // The ID format is: hq-channel-<name> (e.g., hq-channel-alerts)
// Channels are town-level entities (hq- prefix) because they span rigs.
// The created_by field is populated from BD_ACTOR env var for provenance tracking. // The created_by field is populated from BD_ACTOR env var for provenance tracking.
func (b *Beads) CreateChannelBead(name string, subscribers []string, createdBy string) (*Issue, error) { func (b *Beads) CreateChannelBead(name string, subscribers []string, createdBy string) (*Issue, error) {
id := ChannelBeadID(name) id := ChannelBeadID(name)
@@ -156,6 +156,7 @@ func (b *Beads) CreateChannelBead(name string, subscribers []string, createdBy s
"--description=" + description, "--description=" + description,
"--type=task", // Channels use task type with gt:channel label "--type=task", // Channels use task type with gt:channel label
"--labels=gt:channel", "--labels=gt:channel",
"--force", // Override prefix check (town beads may have mixed prefixes)
} }
// Default actor from BD_ACTOR env var for provenance tracking // Default actor from BD_ACTOR env var for provenance tracking

View File

@@ -97,13 +97,14 @@ func ParseGroupFields(description string) *GroupFields {
} }
// GroupBeadID returns the bead ID for a group name. // GroupBeadID returns the bead ID for a group name.
// Format: gt-group-<name> // Format: hq-group-<name> (town-level, groups span rigs)
func GroupBeadID(name string) string { func GroupBeadID(name string) string {
return "gt-group-" + name return "hq-group-" + name
} }
// CreateGroupBead creates a group bead for mail distribution. // CreateGroupBead creates a group bead for mail distribution.
// The ID format is: gt-group-<name> (e.g., gt-group-ops-team) // The ID format is: hq-group-<name> (e.g., hq-group-ops-team)
// Groups are town-level entities (hq- prefix) because they span rigs.
// The created_by field is populated from BD_ACTOR env var for provenance tracking. // The created_by field is populated from BD_ACTOR env var for provenance tracking.
func (b *Beads) CreateGroupBead(name string, members []string, createdBy string) (*Issue, error) { func (b *Beads) CreateGroupBead(name string, members []string, createdBy string) (*Issue, error) {
id := GroupBeadID(name) id := GroupBeadID(name)
@@ -123,6 +124,7 @@ func (b *Beads) CreateGroupBead(name string, members []string, createdBy string)
"--description=" + description, "--description=" + description,
"--type=task", // Groups use task type with gt:group label "--type=task", // Groups use task type with gt:group label
"--labels=gt:group", "--labels=gt:group",
"--force", // Override prefix check (town beads may have mixed prefixes)
} }
// Default actor from BD_ACTOR env var for provenance tracking // Default actor from BD_ACTOR env var for provenance tracking

View File

@@ -163,9 +163,9 @@ func TestGroupBeadID(t *testing.T) {
name string name string
want string want string
}{ }{
{"ops-team", "gt-group-ops-team"}, {"ops-team", "hq-group-ops-team"},
{"all", "gt-group-all"}, {"all", "hq-group-all"},
{"crew-leads", "gt-group-crew-leads"}, {"crew-leads", "hq-group-crew-leads"},
} }
for _, tt := range tests { for _, tt := range tests {