Files
gastown/internal/cmd/rig_dock.go
nux 3f920048cb feat(rig): add dock/undock commands for Level 2 rig control (gt-9gm9n)
Implement gt rig dock <rig> and gt rig undock <rig> 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 <noreply@anthropic.com>
2026-01-06 20:43:27 -08:00

254 lines
7.0 KiB
Go

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 <rig>",
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 <rig>",
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
}