feat(handoff): add -s/-m flags for ergonomic handoff mail
Agents naturally expect `gt handoff -s "Subject" -m "Message"` to work like `gt mail send`. Now it does: - Added --subject/-s and --message/-m flags to gt handoff - Added --self flag to gt mail send for sending to self - Handoff auto-sends mail to self before respawning pane This makes agent-initiated handoff more ergonomic - they can include context in a single command instead of two separate steps. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -33,13 +33,17 @@ Any molecule on the hook will be auto-continued by the new session.`,
|
|||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
handoffWatch bool
|
handoffWatch bool
|
||||||
handoffDryRun bool
|
handoffDryRun bool
|
||||||
|
handoffSubject string
|
||||||
|
handoffMessage string
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
handoffCmd.Flags().BoolVarP(&handoffWatch, "watch", "w", true, "Switch to new session (for remote handoff)")
|
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().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)
|
rootCmd.AddCommand(handoffCmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,6 +87,16 @@ func runHandoff(cmd *cobra.Command, args []string) error {
|
|||||||
return handoffRemoteSession(t, targetSession, restartCmd)
|
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
|
// Handing off ourselves - print feedback then respawn
|
||||||
fmt.Printf("%s Handing off %s...\n", style.Bold.Render("🤝"), currentSession)
|
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
|
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()
|
||||||
|
}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ var (
|
|||||||
mailType string
|
mailType string
|
||||||
mailReplyTo string
|
mailReplyTo string
|
||||||
mailNotify bool
|
mailNotify bool
|
||||||
|
mailSendSelf bool
|
||||||
mailInboxJSON bool
|
mailInboxJSON bool
|
||||||
mailReadJSON bool
|
mailReadJSON bool
|
||||||
mailInboxUnread bool
|
mailInboxUnread bool
|
||||||
@@ -109,8 +110,9 @@ Examples:
|
|||||||
gt mail send gastown/ -s "All hands" -m "Swarm starting" --notify
|
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 "Task" -m "Fix bug" --type task --priority 1
|
||||||
gt mail send gastown/Toast -s "Urgent" -m "Help!" --urgent
|
gt mail send gastown/Toast -s "Urgent" -m "Help!" --urgent
|
||||||
gt mail send mayor/ -s "Re: Status" -m "Done" --reply-to msg-abc123`,
|
gt mail send mayor/ -s "Re: Status" -m "Done" --reply-to msg-abc123
|
||||||
Args: cobra.ExactArgs(1),
|
gt mail send --self -s "Handoff" -m "Context for next session"`,
|
||||||
|
Args: cobra.MaximumNArgs(1),
|
||||||
RunE: runMailSend,
|
RunE: runMailSend,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -233,6 +235,7 @@ func init() {
|
|||||||
mailSendCmd.Flags().StringVar(&mailReplyTo, "reply-to", "", "Message ID this is replying to")
|
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().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(&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")
|
_ = mailSendCmd.MarkFlagRequired("subject")
|
||||||
|
|
||||||
// Inbox flags
|
// Inbox flags
|
||||||
@@ -273,7 +276,28 @@ func init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func runMailSend(cmd *cobra.Command, args []string) error {
|
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)
|
// All mail uses town beads (two-level architecture)
|
||||||
workDir, err := findMailWorkDir()
|
workDir, err := findMailWorkDir()
|
||||||
|
|||||||
Reference in New Issue
Block a user