Add gt broadcast command to nudge all workers
New command that sends a message to all active workers (polecats + crew). Supports filtering by rig with --rig flag, and can include infrastructure agents (mayor, deacon, witness, refinery) with --all flag. Usage: gt broadcast "Check your mail" gt broadcast --rig gastown "New work available" gt broadcast --all "System maintenance in 5 minutes" gt broadcast --dry-run "Test" 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
149
internal/cmd/broadcast.go
Normal file
149
internal/cmd/broadcast.go
Normal file
@@ -0,0 +1,149 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/steveyegge/gastown/internal/style"
|
||||
"github.com/steveyegge/gastown/internal/tmux"
|
||||
)
|
||||
|
||||
var (
|
||||
broadcastRig string
|
||||
broadcastAll bool
|
||||
broadcastDryRun bool
|
||||
)
|
||||
|
||||
func init() {
|
||||
broadcastCmd.Flags().StringVar(&broadcastRig, "rig", "", "Only broadcast to workers in this rig")
|
||||
broadcastCmd.Flags().BoolVar(&broadcastAll, "all", false, "Include all agents (mayor, witness, etc.), not just workers")
|
||||
broadcastCmd.Flags().BoolVar(&broadcastDryRun, "dry-run", false, "Show what would be sent without sending")
|
||||
rootCmd.AddCommand(broadcastCmd)
|
||||
}
|
||||
|
||||
var broadcastCmd = &cobra.Command{
|
||||
Use: "broadcast <message>",
|
||||
Short: "Send a nudge message to all workers",
|
||||
Long: `Broadcasts a message to all active workers (polecats and crew).
|
||||
|
||||
By default, only workers (polecats and crew) receive the message.
|
||||
Use --all to include infrastructure agents (mayor, deacon, witness, refinery).
|
||||
|
||||
The message is sent as a nudge to each worker's Claude Code session.
|
||||
|
||||
Examples:
|
||||
gt broadcast "Check your mail"
|
||||
gt broadcast --rig gastown "New priority work available"
|
||||
gt broadcast --all "System maintenance in 5 minutes"
|
||||
gt broadcast --dry-run "Test message"`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: runBroadcast,
|
||||
}
|
||||
|
||||
func runBroadcast(cmd *cobra.Command, args []string) error {
|
||||
message := args[0]
|
||||
|
||||
if message == "" {
|
||||
return fmt.Errorf("message cannot be empty")
|
||||
}
|
||||
|
||||
// Get all agent sessions (including polecats)
|
||||
agents, err := getAgentSessions(true)
|
||||
if err != nil {
|
||||
return fmt.Errorf("listing sessions: %w", err)
|
||||
}
|
||||
|
||||
// Filter to target agents
|
||||
var targets []*AgentSession
|
||||
for _, agent := range agents {
|
||||
// Filter by rig if specified
|
||||
if broadcastRig != "" && agent.Rig != broadcastRig {
|
||||
continue
|
||||
}
|
||||
|
||||
// Unless --all, only include workers (crew + polecats)
|
||||
if !broadcastAll {
|
||||
if agent.Type != AgentCrew && agent.Type != AgentPolecat {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
targets = append(targets, agent)
|
||||
}
|
||||
|
||||
if len(targets) == 0 {
|
||||
fmt.Println("No workers running to broadcast to.")
|
||||
if broadcastRig != "" {
|
||||
fmt.Printf(" (filtered by rig: %s)\n", broadcastRig)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Dry run - just show what would be sent
|
||||
if broadcastDryRun {
|
||||
fmt.Printf("Would broadcast to %d agent(s):\n\n", len(targets))
|
||||
for _, agent := range targets {
|
||||
fmt.Printf(" %s %s\n", AgentTypeIcons[agent.Type], formatAgentName(agent))
|
||||
}
|
||||
fmt.Printf("\nMessage: %s\n", message)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Send nudges
|
||||
t := tmux.NewTmux()
|
||||
var succeeded, failed int
|
||||
var failures []string
|
||||
|
||||
fmt.Printf("Broadcasting to %d agent(s)...\n\n", len(targets))
|
||||
|
||||
for i, agent := range targets {
|
||||
agentName := formatAgentName(agent)
|
||||
|
||||
if err := t.NudgeSession(agent.Name, message); err != nil {
|
||||
failed++
|
||||
failures = append(failures, fmt.Sprintf("%s: %v", agentName, err))
|
||||
fmt.Printf(" %s %s %s\n", style.ErrorPrefix, AgentTypeIcons[agent.Type], agentName)
|
||||
} else {
|
||||
succeeded++
|
||||
fmt.Printf(" %s %s %s\n", style.SuccessPrefix, AgentTypeIcons[agent.Type], agentName)
|
||||
}
|
||||
|
||||
// Small delay between nudges to avoid overwhelming tmux
|
||||
if i < len(targets)-1 {
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
if failed > 0 {
|
||||
fmt.Printf("%s Broadcast complete: %d succeeded, %d failed\n",
|
||||
style.WarningPrefix, succeeded, failed)
|
||||
for _, f := range failures {
|
||||
fmt.Printf(" %s\n", style.Dim.Render(f))
|
||||
}
|
||||
return fmt.Errorf("%d nudge(s) failed", failed)
|
||||
}
|
||||
|
||||
fmt.Printf("%s Broadcast complete: %d agent(s) nudged\n", style.SuccessPrefix, succeeded)
|
||||
return nil
|
||||
}
|
||||
|
||||
// formatAgentName returns a display name for an agent.
|
||||
func formatAgentName(agent *AgentSession) string {
|
||||
switch agent.Type {
|
||||
case AgentMayor:
|
||||
return "mayor"
|
||||
case AgentDeacon:
|
||||
return "deacon"
|
||||
case AgentWitness:
|
||||
return fmt.Sprintf("%s/witness", agent.Rig)
|
||||
case AgentRefinery:
|
||||
return fmt.Sprintf("%s/refinery", agent.Rig)
|
||||
case AgentCrew:
|
||||
return fmt.Sprintf("%s/crew/%s", agent.Rig, agent.AgentName)
|
||||
case AgentPolecat:
|
||||
return fmt.Sprintf("%s/%s", agent.Rig, agent.AgentName)
|
||||
}
|
||||
return agent.Name
|
||||
}
|
||||
Reference in New Issue
Block a user