package cmd import ( "encoding/json" "fmt" "os" "os/exec" "github.com/spf13/cobra" "github.com/steveyegge/gastown/internal/mail" "github.com/steveyegge/gastown/internal/style" "github.com/steveyegge/gastown/internal/workspace" ) // Gate command provides gt wrappers for gate operations. // Most gate commands are in beads (bd gate ...), but gt provides // integration with the Gas Town mail system for wake notifications. var gateCmd = &cobra.Command{ Use: "gate", GroupID: GroupWork, Short: "Gate coordination commands", Long: `Gate commands for async coordination. Most gate commands are in beads: bd gate create - Create a gate (timer, gh:run, human, mail) bd gate show - Show gate details bd gate list - List open gates bd gate close - Close a gate bd gate approve - Approve a human gate bd gate eval - Evaluate and close elapsed gates The gt gate command provides Gas Town integration: gt gate wake - Send wake mail to gate waiters after close`, } var gateWakeCmd = &cobra.Command{ Use: "wake ", Short: "Send wake mail to gate waiters", Long: `Send wake mail to all waiters on a gate. This command should be called after a gate closes to notify waiting agents. Typically called by Deacon after 'bd gate eval' or after manual gate close. The wake mail includes: - Gate ID and close reason - Instructions to run 'gt resume' Examples: # After manual gate close bd gate close gt-xxx --reason "Approved" gt gate wake gt-xxx # In Deacon patrol after gate eval for gate in $(bd gate eval --json | jq -r '.closed[]'); do gt gate wake $gate done`, Args: cobra.ExactArgs(1), RunE: runGateWake, } var ( gateWakeJSON bool gateWakeDryRun bool ) func init() { gateWakeCmd.Flags().BoolVar(&gateWakeJSON, "json", false, "Output as JSON") gateWakeCmd.Flags().BoolVarP(&gateWakeDryRun, "dry-run", "n", false, "Show what would be done") gateCmd.AddCommand(gateWakeCmd) rootCmd.AddCommand(gateCmd) } // GateWakeResult represents the result of sending wake mail. type GateWakeResult struct { GateID string `json:"gate_id"` CloseReason string `json:"close_reason"` Waiters []string `json:"waiters"` Notified []string `json:"notified"` Failed []string `json:"failed,omitempty"` } func runGateWake(cmd *cobra.Command, args []string) error { gateID := args[0] // Get gate info gateCheck := exec.Command("bd", "gate", "show", gateID, "--json") gateOutput, err := gateCheck.Output() if err != nil { return fmt.Errorf("gate '%s' not found or not accessible", gateID) } var gateInfo struct { ID string `json:"id"` Status string `json:"status"` CloseReason string `json:"close_reason"` Waiters []string `json:"waiters"` } if err := json.Unmarshal(gateOutput, &gateInfo); err != nil { return fmt.Errorf("parsing gate info: %w", err) } if gateInfo.Status != "closed" { return fmt.Errorf("gate '%s' is not closed (status: %s) - wake mail only sent for closed gates", gateID, gateInfo.Status) } if len(gateInfo.Waiters) == 0 { if gateWakeJSON { result := GateWakeResult{ GateID: gateID, CloseReason: gateInfo.CloseReason, Waiters: []string{}, Notified: []string{}, } return outputGateWakeResult(result) } fmt.Printf("%s Gate %s has no waiters to notify\n", style.Dim.Render("○"), gateID) return nil } if gateWakeDryRun { fmt.Printf("Would send wake mail for gate %s to:\n", gateID) for _, w := range gateInfo.Waiters { fmt.Printf(" - %s\n", w) } return nil } // Find town root for mail routing townRoot, err := workspace.FindFromCwd() if err != nil { return fmt.Errorf("finding town root: %w", err) } router := mail.NewRouter(townRoot) result := GateWakeResult{ GateID: gateID, CloseReason: gateInfo.CloseReason, Waiters: gateInfo.Waiters, Notified: []string{}, Failed: []string{}, } subject := fmt.Sprintf("🚦 GATE CLEARED: %s", gateID) body := fmt.Sprintf("Gate %s has closed.\n\nReason: %s\n\nRun 'gt resume' to continue your parked work.", gateID, gateInfo.CloseReason) for _, waiter := range gateInfo.Waiters { msg := &mail.Message{ From: "deacon/", To: waiter, Subject: subject, Body: body, Type: mail.TypeNotification, Priority: mail.PriorityHigh, Wisp: true, } if err := router.Send(msg); err != nil { result.Failed = append(result.Failed, waiter) } else { result.Notified = append(result.Notified, waiter) } } if gateWakeJSON { return outputGateWakeResult(result) } fmt.Printf("%s Sent wake mail for gate %s\n", style.Bold.Render("🚦"), gateID) if len(result.Notified) > 0 { fmt.Printf(" Notified: %v\n", result.Notified) } if len(result.Failed) > 0 { fmt.Printf(" Failed: %v\n", result.Failed) } return nil } func outputGateWakeResult(result GateWakeResult) error { enc := json.NewEncoder(os.Stdout) enc.SetIndent("", " ") return enc.Encode(result) }