package cmd import ( "fmt" "os/exec" "github.com/spf13/cobra" "github.com/steveyegge/gastown/internal/beads" "github.com/steveyegge/gastown/internal/refinery" "github.com/steveyegge/gastown/internal/style" "github.com/steveyegge/gastown/internal/tmux" "github.com/steveyegge/gastown/internal/witness" ) // RigDockedLabel is the label set on rig identity beads when docked. const RigDockedLabel = "status:docked" var rigDockCmd = &cobra.Command{ Use: "dock ", Short: "Dock a rig (global, persistent shutdown)", Long: `Dock a rig to persistently disable it across all clones. Docking a rig: - Stops the witness if running - Stops the refinery if running - Sets status:docked label on the rig identity bead - Syncs via git so all clones see the docked status This is a Level 2 (global/persistent) operation: - Affects all clones of this rig (via git sync) - Persists until explicitly undocked - The daemon respects this status and won't auto-restart agents Use 'gt rig undock' to resume normal operation. Examples: gt rig dock gastown gt rig dock beads`, Args: cobra.ExactArgs(1), RunE: runRigDock, } var rigUndockCmd = &cobra.Command{ Use: "undock ", Short: "Undock a rig (remove global docked status)", Long: `Undock a rig to remove the persistent docked status. Undocking a rig: - Removes the status:docked label from the rig identity bead - Syncs via git so all clones see the undocked status - Allows the daemon to auto-restart agents - Does NOT automatically start agents (use 'gt rig start' for that) Examples: gt rig undock gastown gt rig undock beads`, Args: cobra.ExactArgs(1), RunE: runRigUndock, } func init() { rigCmd.AddCommand(rigDockCmd) rigCmd.AddCommand(rigUndockCmd) } func runRigDock(cmd *cobra.Command, args []string) error { rigName := args[0] // Get rig _, r, err := getRig(rigName) if err != nil { return err } // Get rig prefix for bead ID prefix := "gt" // default if r.Config != nil && r.Config.Prefix != "" { prefix = r.Config.Prefix } // Find the rig identity bead rigBeadID := beads.RigBeadIDWithPrefix(prefix, rigName) bd := beads.New(r.BeadsPath()) // Check if rig bead exists, create if not rigBead, err := bd.Show(rigBeadID) if err != nil { // Rig identity bead doesn't exist (legacy rig) - create it fmt.Printf(" Creating rig identity bead %s...\n", rigBeadID) rigBead, err = bd.CreateRigBead(rigBeadID, rigName, &beads.RigFields{ Repo: r.GitURL, Prefix: prefix, State: "active", }) if err != nil { return fmt.Errorf("creating rig identity bead: %w", err) } } // Check if already docked for _, label := range rigBead.Labels { if label == RigDockedLabel { fmt.Printf("%s Rig %s is already docked\n", style.Dim.Render("•"), rigName) return nil } } fmt.Printf("Docking rig %s...\n", style.Bold.Render(rigName)) var stoppedAgents []string t := tmux.NewTmux() // Stop witness if running witnessSession := fmt.Sprintf("gt-%s-witness", rigName) witnessRunning, _ := t.HasSession(witnessSession) if witnessRunning { fmt.Printf(" Stopping witness...\n") witMgr := witness.NewManager(r) if err := witMgr.Stop(); err != nil { fmt.Printf(" %s Failed to stop witness: %v\n", style.Warning.Render("!"), err) } else { stoppedAgents = append(stoppedAgents, "Witness stopped") } } // Stop refinery if running refinerySession := fmt.Sprintf("gt-%s-refinery", rigName) refineryRunning, _ := t.HasSession(refinerySession) if refineryRunning { fmt.Printf(" Stopping refinery...\n") refMgr := refinery.NewManager(r) if err := refMgr.Stop(); err != nil { fmt.Printf(" %s Failed to stop refinery: %v\n", style.Warning.Render("!"), err) } else { stoppedAgents = append(stoppedAgents, "Refinery stopped") } } // Set docked label on rig identity bead if err := bd.Update(rigBeadID, beads.UpdateOptions{ AddLabels: []string{RigDockedLabel}, }); err != nil { return fmt.Errorf("setting docked label: %w", err) } // Sync beads to propagate to other clones fmt.Printf(" Syncing beads...\n") syncCmd := exec.Command("bd", "sync") syncCmd.Dir = r.BeadsPath() if output, err := syncCmd.CombinedOutput(); err != nil { fmt.Printf(" %s bd sync warning: %v\n%s", style.Warning.Render("!"), err, string(output)) } // Output fmt.Printf("%s Rig %s docked (global)\n", style.Success.Render("✓"), rigName) fmt.Printf(" Label added: %s\n", RigDockedLabel) for _, msg := range stoppedAgents { fmt.Printf(" %s\n", msg) } fmt.Printf(" Run '%s' to propagate to other clones\n", style.Dim.Render("bd sync")) return nil } func runRigUndock(cmd *cobra.Command, args []string) error { rigName := args[0] // Get rig and town root _, r, err := getRig(rigName) if err != nil { return err } // Get rig prefix for bead ID prefix := "gt" // default if r.Config != nil && r.Config.Prefix != "" { prefix = r.Config.Prefix } // Find the rig identity bead rigBeadID := beads.RigBeadIDWithPrefix(prefix, rigName) bd := beads.New(r.BeadsPath()) // Check if rig bead exists, create if not rigBead, err := bd.Show(rigBeadID) if err != nil { // Rig identity bead doesn't exist (legacy rig) - can't be docked fmt.Printf("%s Rig %s has no identity bead and is not docked\n", style.Dim.Render("•"), rigName) return nil } // Check if actually docked isDocked := false for _, label := range rigBead.Labels { if label == RigDockedLabel { isDocked = true break } } if !isDocked { fmt.Printf("%s Rig %s is not docked\n", style.Dim.Render("•"), rigName) return nil } // Remove docked label from rig identity bead if err := bd.Update(rigBeadID, beads.UpdateOptions{ RemoveLabels: []string{RigDockedLabel}, }); err != nil { return fmt.Errorf("removing docked label: %w", err) } // Sync beads to propagate to other clones fmt.Printf(" Syncing beads...\n") syncCmd := exec.Command("bd", "sync") syncCmd.Dir = r.BeadsPath() if output, err := syncCmd.CombinedOutput(); err != nil { fmt.Printf(" %s bd sync warning: %v\n%s", style.Warning.Render("!"), err, string(output)) } fmt.Printf("%s Rig %s undocked\n", style.Success.Render("✓"), rigName) fmt.Printf(" Label removed: %s\n", RigDockedLabel) fmt.Printf(" Daemon can now auto-restart agents\n") fmt.Printf(" Use '%s' to start agents immediately\n", style.Dim.Render("gt rig start "+rigName)) return nil } // IsRigDocked checks if a rig is docked by checking for the status:docked label // on the rig identity bead. This function is exported for use by the daemon. func IsRigDocked(townRoot, rigName, prefix string) bool { // Construct the rig beads path rigPath := townRoot + "/" + rigName beadsPath := rigPath + "/mayor/rig" if _, err := exec.Command("test", "-d", beadsPath).CombinedOutput(); err != nil { beadsPath = rigPath } bd := beads.New(beadsPath) rigBeadID := beads.RigBeadIDWithPrefix(prefix, rigName) rigBead, err := bd.Show(rigBeadID) if err != nil { return false } for _, label := range rigBead.Labels { if label == RigDockedLabel { return true } } return false }