feat: mail uses wisps for ephemeral orchestration messages (gt-lg66)

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>
This commit is contained in:
Steve Yegge
2025-12-24 20:03:37 -08:00
parent 9d1ebfc54d
commit 200a09a02d
5 changed files with 166 additions and 19 deletions

View File

@@ -22,6 +22,7 @@ var (
mailPriority int
mailUrgent bool
mailPinned bool
mailEphemeral bool
mailType string
mailReplyTo string
mailNotify bool
@@ -235,6 +236,7 @@ func init() {
mailSendCmd.Flags().StringVar(&mailReplyTo, "reply-to", "", "Message ID this is replying to")
mailSendCmd.Flags().BoolVarP(&mailNotify, "notify", "n", false, "Send tmux notification to recipient")
mailSendCmd.Flags().BoolVar(&mailPinned, "pinned", false, "Pin message (for handoff context that persists)")
mailSendCmd.Flags().BoolVar(&mailEphemeral, "ephemeral", false, "Send as ephemeral wisp (auto-cleanup on patrol squash)")
mailSendCmd.Flags().BoolVar(&mailSendSelf, "self", false, "Send to self (auto-detect from cwd)")
_ = mailSendCmd.MarkFlagRequired("subject")
@@ -332,6 +334,9 @@ func runMailSend(cmd *cobra.Command, args []string) error {
// Set pinned flag
msg.Pinned = mailPinned
// Set ephemeral flag
msg.Ephemeral = mailEphemeral
// Handle reply-to: auto-set type to reply and look up thread
if mailReplyTo != "" {
msg.ReplyTo = mailReplyTo
@@ -434,8 +439,12 @@ func runMailInbox(cmd *cobra.Command, args []string) error {
if msg.Priority == mail.PriorityHigh || msg.Priority == mail.PriorityUrgent {
priorityMarker = " " + style.Bold.Render("!")
}
ephemeralMarker := ""
if msg.Ephemeral || msg.Source == mail.SourceWisp {
ephemeralMarker = " " + style.Dim.Render("(ephemeral)")
}
fmt.Printf(" %s %s%s%s\n", readMarker, msg.Subject, typeMarker, priorityMarker)
fmt.Printf(" %s %s%s%s%s\n", readMarker, msg.Subject, typeMarker, priorityMarker, ephemeralMarker)
fmt.Printf(" %s from %s\n",
style.Dim.Render(msg.ID),
msg.From)

View File

@@ -395,13 +395,14 @@ func runSpawn(cmd *cobra.Command, args []string) error {
sender := detectSender()
sessionName := sessMgr.SessionName(polecatName)
// Notify Witness with POLECAT_STARTED message
// Notify Witness with POLECAT_STARTED message (ephemeral - lifecycle ping)
witnessAddr := fmt.Sprintf("%s/witness", rigName)
witnessNotification := &mail.Message{
To: witnessAddr,
From: sender,
Subject: fmt.Sprintf("POLECAT_STARTED %s", polecatName),
Body: fmt.Sprintf("Issue: %s\nSession: %s", assignmentID, sessionName),
To: witnessAddr,
From: sender,
Subject: fmt.Sprintf("POLECAT_STARTED %s", polecatName),
Body: fmt.Sprintf("Issue: %s\nSession: %s", assignmentID, sessionName),
Ephemeral: true,
}
if err := townRouter.Send(witnessNotification); err != nil {
@@ -410,13 +411,14 @@ func runSpawn(cmd *cobra.Command, args []string) error {
fmt.Printf(" %s\n", style.Dim.Render("Witness notified of polecat start"))
}
// Notify Deacon with POLECAT_STARTED message (includes full rig/polecat address)
// Notify Deacon with POLECAT_STARTED message (ephemeral - lifecycle ping)
deaconAddr := "deacon/"
deaconNotification := &mail.Message{
To: deaconAddr,
From: sender,
Subject: fmt.Sprintf("POLECAT_STARTED %s/%s", rigName, polecatName),
Body: fmt.Sprintf("Issue: %s\nSession: %s", assignmentID, sessionName),
To: deaconAddr,
From: sender,
Subject: fmt.Sprintf("POLECAT_STARTED %s/%s", rigName, polecatName),
Body: fmt.Sprintf("Issue: %s\nSession: %s", assignmentID, sessionName),
Ephemeral: true,
}
if err := townRouter.Send(deaconNotification); err != nil {