feat(crew): Add gt crew stop command for stopping crew sessions
Implements gt crew stop with the following features: - Stop individual crew members: gt crew stop dave - Stop multiple: gt crew stop beads/emma beads/grip - Stop all: gt crew stop --all - Filter by rig: gt crew stop --all --rig beads - Dry-run mode: gt crew stop --all --dry-run - Force mode (skip output capture): gt crew stop dave --force Uses same semantics as gt shutdown: captures output before stopping, logs kill events to town log, provides clear status reporting. (gt-q2am4) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -36,6 +36,7 @@ Unlike polecats which are witness-managed and transient, crew workers are:
|
|||||||
|
|
||||||
Commands:
|
Commands:
|
||||||
gt crew start <name> Start a crew workspace (creates if needed)
|
gt crew start <name> Start a crew workspace (creates if needed)
|
||||||
|
gt crew stop <name> Stop crew workspace session(s)
|
||||||
gt crew add <name> Create a new crew workspace
|
gt crew add <name> Create a new crew workspace
|
||||||
gt crew list List crew workspaces with status
|
gt crew list List crew workspaces with status
|
||||||
gt crew at <name> Attach to crew workspace session
|
gt crew at <name> Attach to crew workspace session
|
||||||
@@ -265,6 +266,42 @@ Examples:
|
|||||||
RunE: runCrewStart,
|
RunE: runCrewStart,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var crewStopCmd = &cobra.Command{
|
||||||
|
Use: "stop [name...]",
|
||||||
|
Short: "Stop crew workspace session(s)",
|
||||||
|
Long: `Stop one or more running crew workspace sessions.
|
||||||
|
|
||||||
|
Kills the tmux session(s) for the specified crew member(s). Use --all to
|
||||||
|
stop all running crew sessions across all rigs.
|
||||||
|
|
||||||
|
The name can include the rig in slash format (e.g., beads/emma).
|
||||||
|
If not specified, the rig is inferred from the current directory.
|
||||||
|
|
||||||
|
Output is captured before stopping for debugging purposes (use --force
|
||||||
|
to skip capture for faster shutdown).
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
gt crew stop dave # Stop dave's session
|
||||||
|
gt crew stop beads/emma beads/grip # Stop multiple from specific rig
|
||||||
|
gt crew stop --all # Stop all running crew sessions
|
||||||
|
gt crew stop --all --rig beads # Stop all crew in beads rig
|
||||||
|
gt crew stop --all --dry-run # Preview what would be stopped
|
||||||
|
gt crew stop dave --force # Stop without capturing output`,
|
||||||
|
Args: func(cmd *cobra.Command, args []string) error {
|
||||||
|
if crewAll {
|
||||||
|
if len(args) > 0 {
|
||||||
|
return fmt.Errorf("cannot specify both --all and a name")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if len(args) < 1 {
|
||||||
|
return fmt.Errorf("requires at least 1 argument (or --all)")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
RunE: runCrewStop,
|
||||||
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
// Add flags
|
// Add flags
|
||||||
crewAddCmd.Flags().StringVar(&crewRig, "rig", "", "Rig to create crew workspace in")
|
crewAddCmd.Flags().StringVar(&crewRig, "rig", "", "Rig to create crew workspace in")
|
||||||
@@ -299,6 +336,11 @@ func init() {
|
|||||||
crewStartCmd.Flags().BoolVar(&crewAll, "all", false, "Start all crew members in the rig")
|
crewStartCmd.Flags().BoolVar(&crewAll, "all", false, "Start all crew members in the rig")
|
||||||
crewStartCmd.Flags().StringVar(&crewAccount, "account", "", "Claude Code account handle to use")
|
crewStartCmd.Flags().StringVar(&crewAccount, "account", "", "Claude Code account handle to use")
|
||||||
|
|
||||||
|
crewStopCmd.Flags().StringVar(&crewRig, "rig", "", "Rig to use (filter when using --all)")
|
||||||
|
crewStopCmd.Flags().BoolVar(&crewAll, "all", false, "Stop all running crew sessions")
|
||||||
|
crewStopCmd.Flags().BoolVar(&crewDryRun, "dry-run", false, "Show what would be stopped without stopping")
|
||||||
|
crewStopCmd.Flags().BoolVar(&crewForce, "force", false, "Skip output capture for faster shutdown")
|
||||||
|
|
||||||
// Add subcommands
|
// Add subcommands
|
||||||
crewCmd.AddCommand(crewAddCmd)
|
crewCmd.AddCommand(crewAddCmd)
|
||||||
crewCmd.AddCommand(crewListCmd)
|
crewCmd.AddCommand(crewListCmd)
|
||||||
@@ -317,6 +359,7 @@ func init() {
|
|||||||
crewCmd.AddCommand(crewNextCmd)
|
crewCmd.AddCommand(crewNextCmd)
|
||||||
crewCmd.AddCommand(crewPrevCmd)
|
crewCmd.AddCommand(crewPrevCmd)
|
||||||
crewCmd.AddCommand(crewStartCmd)
|
crewCmd.AddCommand(crewStartCmd)
|
||||||
|
crewCmd.AddCommand(crewStopCmd)
|
||||||
|
|
||||||
rootCmd.AddCommand(crewCmd)
|
rootCmd.AddCommand(crewCmd)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import (
|
|||||||
"github.com/steveyegge/gastown/internal/session"
|
"github.com/steveyegge/gastown/internal/session"
|
||||||
"github.com/steveyegge/gastown/internal/style"
|
"github.com/steveyegge/gastown/internal/style"
|
||||||
"github.com/steveyegge/gastown/internal/tmux"
|
"github.com/steveyegge/gastown/internal/tmux"
|
||||||
|
"github.com/steveyegge/gastown/internal/townlog"
|
||||||
"github.com/steveyegge/gastown/internal/workspace"
|
"github.com/steveyegge/gastown/internal/workspace"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -559,3 +560,179 @@ func restartCrewSession(rigName, crewName, clonePath string) error {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// runCrewStop stops one or more crew workers.
|
||||||
|
// Supports: "name", "rig/name" formats, or --all to stop all.
|
||||||
|
func runCrewStop(cmd *cobra.Command, args []string) error {
|
||||||
|
// Handle --all flag
|
||||||
|
if crewAll {
|
||||||
|
return runCrewStopAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
var lastErr error
|
||||||
|
t := tmux.NewTmux()
|
||||||
|
|
||||||
|
for _, arg := range args {
|
||||||
|
name := arg
|
||||||
|
rigOverride := crewRig
|
||||||
|
|
||||||
|
// Parse rig/name format (e.g., "beads/emma" -> rig=beads, name=emma)
|
||||||
|
if rig, crewName, ok := parseRigSlashName(name); ok {
|
||||||
|
if rigOverride == "" {
|
||||||
|
rigOverride = rig
|
||||||
|
}
|
||||||
|
name = crewName
|
||||||
|
}
|
||||||
|
|
||||||
|
_, r, err := getCrewManager(rigOverride)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Error stopping %s: %v\n", arg, err)
|
||||||
|
lastErr = err
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
sessionID := crewSessionName(r.Name, name)
|
||||||
|
|
||||||
|
// Check if session exists
|
||||||
|
hasSession, _ := t.HasSession(sessionID)
|
||||||
|
if !hasSession {
|
||||||
|
fmt.Printf("No session found for %s/%s\n", r.Name, name)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Capture output before stopping (best effort)
|
||||||
|
var output string
|
||||||
|
if !crewForce {
|
||||||
|
output, _ = t.CapturePane(sessionID, 50)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kill the session
|
||||||
|
if err := t.KillSession(sessionID); err != nil {
|
||||||
|
fmt.Printf(" %s [%s] %s: %s\n",
|
||||||
|
style.ErrorPrefix,
|
||||||
|
r.Name, name,
|
||||||
|
style.Dim.Render(err.Error()))
|
||||||
|
lastErr = err
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf(" %s [%s] %s: stopped\n",
|
||||||
|
style.SuccessPrefix,
|
||||||
|
r.Name, name)
|
||||||
|
|
||||||
|
// Log kill event to town log
|
||||||
|
townRoot, _ := workspace.Find(r.Path)
|
||||||
|
if townRoot != "" {
|
||||||
|
agent := fmt.Sprintf("%s/crew/%s", r.Name, name)
|
||||||
|
logger := townlog.NewLogger(townRoot)
|
||||||
|
logger.Log(townlog.EventKill, agent, "gt crew stop")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log captured output (truncated)
|
||||||
|
if len(output) > 200 {
|
||||||
|
output = output[len(output)-200:]
|
||||||
|
}
|
||||||
|
if output != "" {
|
||||||
|
fmt.Printf(" %s\n", style.Dim.Render("(output captured)"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return lastErr
|
||||||
|
}
|
||||||
|
|
||||||
|
// runCrewStopAll stops all running crew sessions.
|
||||||
|
// If crewRig is set, only stops crew in that rig.
|
||||||
|
func runCrewStopAll() error {
|
||||||
|
// Get all agent sessions (including polecats to find crew)
|
||||||
|
agents, err := getAgentSessions(true)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("listing sessions: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter to crew agents only
|
||||||
|
var targets []*AgentSession
|
||||||
|
for _, agent := range agents {
|
||||||
|
if agent.Type != AgentCrew {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Filter by rig if specified
|
||||||
|
if crewRig != "" && agent.Rig != crewRig {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
targets = append(targets, agent)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(targets) == 0 {
|
||||||
|
fmt.Println("No running crew sessions to stop.")
|
||||||
|
if crewRig != "" {
|
||||||
|
fmt.Printf(" (filtered by rig: %s)\n", crewRig)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dry run - just show what would be stopped
|
||||||
|
if crewDryRun {
|
||||||
|
fmt.Printf("Would stop %d crew session(s):\n\n", len(targets))
|
||||||
|
for _, agent := range targets {
|
||||||
|
fmt.Printf(" %s %s/crew/%s\n", AgentTypeIcons[AgentCrew], agent.Rig, agent.AgentName)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("%s Stopping %d crew session(s)...\n\n",
|
||||||
|
style.Bold.Render("🛑"), len(targets))
|
||||||
|
|
||||||
|
t := tmux.NewTmux()
|
||||||
|
var succeeded, failed int
|
||||||
|
var failures []string
|
||||||
|
|
||||||
|
for _, agent := range targets {
|
||||||
|
agentName := fmt.Sprintf("%s/crew/%s", agent.Rig, agent.AgentName)
|
||||||
|
sessionID := agent.Name // agent.Name IS the tmux session name
|
||||||
|
|
||||||
|
// Capture output before stopping (best effort)
|
||||||
|
var output string
|
||||||
|
if !crewForce {
|
||||||
|
output, _ = t.CapturePane(sessionID, 50)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kill the session
|
||||||
|
if err := t.KillSession(sessionID); err != nil {
|
||||||
|
failed++
|
||||||
|
failures = append(failures, fmt.Sprintf("%s: %v", agentName, err))
|
||||||
|
fmt.Printf(" %s %s\n", style.ErrorPrefix, agentName)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
succeeded++
|
||||||
|
fmt.Printf(" %s %s\n", style.SuccessPrefix, agentName)
|
||||||
|
|
||||||
|
// Log kill event to town log
|
||||||
|
townRoot, _ := workspace.FindFromCwd()
|
||||||
|
if townRoot != "" {
|
||||||
|
logger := townlog.NewLogger(townRoot)
|
||||||
|
logger.Log(townlog.EventKill, agentName, "gt crew stop --all")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log captured output (truncated)
|
||||||
|
if len(output) > 200 {
|
||||||
|
output = output[len(output)-200:]
|
||||||
|
}
|
||||||
|
if output != "" {
|
||||||
|
fmt.Printf(" %s\n", style.Dim.Render("(output captured)"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println()
|
||||||
|
if failed > 0 {
|
||||||
|
fmt.Printf("%s Stop 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 stop(s) failed", failed)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("%s Stop complete: %d crew session(s) stopped\n", style.SuccessPrefix, succeeded)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user