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>
This commit is contained in:
253
internal/cmd/rig_dock.go
Normal file
253
internal/cmd/rig_dock.go
Normal file
@@ -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 <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
|
||||
}
|
||||
Reference in New Issue
Block a user