Files
gastown/internal/cmd/rig_park.go
max c860112cf6 feat(rig): support parking multiple rigs in single call
- gt rig park now accepts variadic args (fixes #375)
- gt rig unpark updated for consistency
- Errors collected and reported at end

Also fixes test self-interruption bug where sling tests sent real
tmux nudges containing "Work slung: gt-wisp-xyz", causing agents
running tests to interrupt themselves. Added GT_TEST_NO_NUDGE env
var to skip nudge during tests.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 17:44:37 -08:00

184 lines
4.9 KiB
Go

package cmd
import (
"fmt"
"github.com/spf13/cobra"
"github.com/steveyegge/gastown/internal/refinery"
"github.com/steveyegge/gastown/internal/style"
"github.com/steveyegge/gastown/internal/tmux"
"github.com/steveyegge/gastown/internal/wisp"
"github.com/steveyegge/gastown/internal/witness"
)
// RigStatusKey is the wisp config key for rig operational status.
const RigStatusKey = "status"
// RigStatusParked is the value indicating a rig is parked.
const RigStatusParked = "parked"
var rigParkCmd = &cobra.Command{
Use: "park <rig>...",
Short: "Park one or more rigs (stops agents, daemon won't auto-restart)",
Long: `Park rigs to temporarily disable them.
Parking a rig:
- Stops the witness if running
- Stops the refinery if running
- Sets status=parked in the wisp layer (local/ephemeral)
- The daemon respects this status and won't auto-restart agents
This is a Level 1 (local/ephemeral) operation:
- Only affects this town
- Disappears on wisp cleanup
- Use 'gt rig unpark' to resume normal operation
Examples:
gt rig park gastown
gt rig park beads gastown mayor`,
Args: cobra.MinimumNArgs(1),
RunE: runRigPark,
}
var rigUnparkCmd = &cobra.Command{
Use: "unpark <rig>...",
Short: "Unpark one or more rigs (allow daemon to auto-restart agents)",
Long: `Unpark rigs to resume normal operation.
Unparking a rig:
- Removes the parked status from the wisp layer
- Allows the daemon to auto-restart agents
- Does NOT automatically start agents (use 'gt rig start' for that)
Examples:
gt rig unpark gastown
gt rig unpark beads gastown mayor`,
Args: cobra.MinimumNArgs(1),
RunE: runRigUnpark,
}
func init() {
rigCmd.AddCommand(rigParkCmd)
rigCmd.AddCommand(rigUnparkCmd)
}
func runRigPark(cmd *cobra.Command, args []string) error {
var errs []error
for _, rigName := range args {
if err := parkOneRig(rigName); err != nil {
errs = append(errs, fmt.Errorf("%s: %w", rigName, err))
}
}
if len(errs) > 0 {
for _, err := range errs {
fmt.Printf("%s %v\n", style.Error.Render("✗"), err)
}
return fmt.Errorf("failed to park %d rig(s)", len(errs))
}
return nil
}
func parkOneRig(rigName string) error {
// Get rig and town root
townRoot, r, err := getRig(rigName)
if err != nil {
return err
}
fmt.Printf("Parking 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 parked status in wisp layer
wispCfg := wisp.NewConfig(townRoot, rigName)
if err := wispCfg.Set(RigStatusKey, RigStatusParked); err != nil {
return fmt.Errorf("setting parked status: %w", err)
}
// Output
fmt.Printf("%s Rig %s parked (local only)\n", style.Success.Render("✓"), rigName)
for _, msg := range stoppedAgents {
fmt.Printf(" %s\n", msg)
}
fmt.Printf(" Daemon will not auto-restart\n")
return nil
}
func runRigUnpark(cmd *cobra.Command, args []string) error {
var errs []error
for _, rigName := range args {
if err := unparkOneRig(rigName); err != nil {
errs = append(errs, fmt.Errorf("%s: %w", rigName, err))
}
}
if len(errs) > 0 {
for _, err := range errs {
fmt.Printf("%s %v\n", style.Error.Render("✗"), err)
}
return fmt.Errorf("failed to unpark %d rig(s)", len(errs))
}
return nil
}
func unparkOneRig(rigName string) error {
// Get rig and town root
townRoot, _, err := getRig(rigName)
if err != nil {
return err
}
// Remove parked status from wisp layer
wispCfg := wisp.NewConfig(townRoot, rigName)
if err := wispCfg.Unset(RigStatusKey); err != nil {
return fmt.Errorf("clearing parked status: %w", err)
}
fmt.Printf("%s Rig %s unparked\n", style.Success.Render("✓"), rigName)
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
}
// IsRigParked checks if a rig is parked in the wisp layer.
// This function is exported for use by the daemon.
func IsRigParked(townRoot, rigName string) bool {
wispCfg := wisp.NewConfig(townRoot, rigName)
return wispCfg.GetString(RigStatusKey) == RigStatusParked
}