From 3f920048cb9cbbbbce3ca7d3b4927b9f06196df3 Mon Sep 17 00:00:00 2001 From: nux Date: Tue, 6 Jan 2026 19:36:20 -0800 Subject: [PATCH] feat(rig): add dock/undock commands for Level 2 rig control (gt-9gm9n) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implement gt rig dock and gt rig undock commands for global/persistent rig control: - dock: stops witness/refinery, sets status:docked label on rig bead - undock: removes docked label, allows daemon to restart agents This is Level 2 (global/persistent) control: - Uses rig identity bead labels (synced via git) - Affects all clones of the rig - Persists until explicitly undocked Also includes cherry-picked rig identity bead infrastructure: - RigFields struct for rig metadata - CreateRigBead and RigBeadID helpers - Auto-create rig bead for legacy rigs on first dock 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- internal/cmd/rig_dock.go | 253 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 253 insertions(+) create mode 100644 internal/cmd/rig_dock.go diff --git a/internal/cmd/rig_dock.go b/internal/cmd/rig_dock.go new file mode 100644 index 00000000..a0fd4f06 --- /dev/null +++ b/internal/cmd/rig_dock.go @@ -0,0 +1,253 @@ +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 +}