diff --git a/internal/cmd/handoff.go b/internal/cmd/handoff.go index e4cecc39..bfcdaa71 100644 --- a/internal/cmd/handoff.go +++ b/internal/cmd/handoff.go @@ -33,13 +33,17 @@ Any molecule on the hook will be auto-continued by the new session.`, } var ( - handoffWatch bool - handoffDryRun bool + handoffWatch bool + handoffDryRun bool + handoffSubject string + handoffMessage string ) func init() { handoffCmd.Flags().BoolVarP(&handoffWatch, "watch", "w", true, "Switch to new session (for remote handoff)") handoffCmd.Flags().BoolVarP(&handoffDryRun, "dry-run", "n", false, "Show what would be done without executing") + handoffCmd.Flags().StringVarP(&handoffSubject, "subject", "s", "", "Subject for handoff mail (optional)") + handoffCmd.Flags().StringVarP(&handoffMessage, "message", "m", "", "Message body for handoff mail (optional)") rootCmd.AddCommand(handoffCmd) } @@ -83,6 +87,16 @@ func runHandoff(cmd *cobra.Command, args []string) error { return handoffRemoteSession(t, targetSession, restartCmd) } + // If subject/message provided, send handoff mail to self first + if handoffSubject != "" || handoffMessage != "" { + if err := sendHandoffMail(handoffSubject, handoffMessage); err != nil { + fmt.Printf("%s Warning: could not send handoff mail: %v\n", style.Dim.Render("⚠"), err) + // Continue anyway - the respawn is more important + } else { + fmt.Printf("%s Sent handoff mail\n", style.Bold.Render("📬")) + } + } + // Handing off ourselves - print feedback then respawn fmt.Printf("%s Handing off %s...\n", style.Bold.Render("🤝"), currentSession) @@ -239,3 +253,24 @@ func getSessionPane(sessionName string) (string, error) { } return lines[0], nil } + +// sendHandoffMail sends a handoff mail to self using gt mail send. +func sendHandoffMail(subject, message string) error { + // Build subject with handoff prefix if not already present + if subject == "" { + subject = "🤝 HANDOFF: Session cycling" + } else if !strings.Contains(subject, "HANDOFF") { + subject = "🤝 HANDOFF: " + subject + } + + // Default message if not provided + if message == "" { + message = "Context cycling. Check bd ready for pending work." + } + + // Use gt mail send to self (--self flag sends to current agent identity) + cmd := exec.Command("gt", "mail", "send", "--self", "-s", subject, "-m", message) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + return cmd.Run() +} diff --git a/internal/cmd/mail.go b/internal/cmd/mail.go index b7e3dbfa..6249605d 100644 --- a/internal/cmd/mail.go +++ b/internal/cmd/mail.go @@ -25,6 +25,7 @@ var ( mailType string mailReplyTo string mailNotify bool + mailSendSelf bool mailInboxJSON bool mailReadJSON bool mailInboxUnread bool @@ -109,8 +110,9 @@ Examples: gt mail send gastown/ -s "All hands" -m "Swarm starting" --notify gt mail send gastown/Toast -s "Task" -m "Fix bug" --type task --priority 1 gt mail send gastown/Toast -s "Urgent" -m "Help!" --urgent - gt mail send mayor/ -s "Re: Status" -m "Done" --reply-to msg-abc123`, - Args: cobra.ExactArgs(1), + gt mail send mayor/ -s "Re: Status" -m "Done" --reply-to msg-abc123 + gt mail send --self -s "Handoff" -m "Context for next session"`, + Args: cobra.MaximumNArgs(1), RunE: runMailSend, } @@ -233,6 +235,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(&mailSendSelf, "self", false, "Send to self (auto-detect from cwd)") _ = mailSendCmd.MarkFlagRequired("subject") // Inbox flags @@ -273,7 +276,28 @@ func init() { } func runMailSend(cmd *cobra.Command, args []string) error { - to := args[0] + var to string + + if mailSendSelf { + // Auto-detect identity from cwd + cwd, err := os.Getwd() + if err != nil { + return fmt.Errorf("getting current directory: %w", err) + } + townRoot, err := workspace.FindFromCwd() + if err != nil || townRoot == "" { + return fmt.Errorf("not in a Gas Town workspace") + } + ctx := detectRole(cwd, townRoot) + to = buildAgentIdentity(ctx) + if to == "" { + return fmt.Errorf("cannot determine identity from current directory (role: %s)", ctx.Role) + } + } else if len(args) > 0 { + to = args[0] + } else { + return fmt.Errorf("address required (or use --self)") + } // All mail uses town beads (two-level architecture) workDir, err := findMailWorkDir()