Files
gastown/internal/cmd/witness.go
mayor 31a32c084b Unify agent startup with GUPP propulsion nudge
Witness and Refinery startup was duplicated across cmd/witness.go, cmd/up.go,
cmd/rig.go, and daemon.go. Worse, not all code paths sent the propulsion nudge
(GUPP - Gas Town Universal Propulsion Principle). Now unified in Manager.Start()
which handles everything including nudges.

Changes:
- witness/manager.go: Full rewrite with session creation, env vars, theming,
  WaitForClaudeReady, startup nudge, and propulsion nudge (GUPP)
- refinery/manager.go: Add propulsion nudge sequence after Claude startup
- cmd/witness.go: Simplify to just call mgr.Start(), remove ensureWitnessSession
- cmd/rig.go: Use witness.Manager.Start() instead of inline session creation
- cmd/start.go: Use witness.Manager.Start()
- cmd/up.go: Use witness.Manager.Start(), remove ensureWitness(),
  add EnsureSettingsForRole in ensureSession()
- daemon.go: Use witness.Manager.Start() and refinery.Manager.Start() for
  unified startup with proper nudges

This ensures all agent startup paths (gt witness start, gt rig boot, gt up,
daemon restarts) consistently apply GUPP propulsion nudges.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-06 01:51:25 -08:00

334 lines
8.6 KiB
Go

package cmd
import (
"encoding/json"
"fmt"
"os"
"os/exec"
"github.com/spf13/cobra"
"github.com/steveyegge/gastown/internal/rig"
"github.com/steveyegge/gastown/internal/style"
"github.com/steveyegge/gastown/internal/tmux"
"github.com/steveyegge/gastown/internal/witness"
"github.com/steveyegge/gastown/internal/workspace"
)
// Witness command flags
var (
witnessForeground bool
witnessStatusJSON bool
)
var witnessCmd = &cobra.Command{
Use: "witness",
GroupID: GroupAgents,
Short: "Manage the polecat monitoring agent",
RunE: requireSubcommand,
Long: `Manage the Witness monitoring agent for a rig.
The Witness monitors polecats for stuck/idle state, nudges polecats
that seem blocked, and reports status to the mayor.`,
}
var witnessStartCmd = &cobra.Command{
Use: "start <rig>",
Aliases: []string{"spawn"},
Short: "Start the witness",
Long: `Start the Witness for a rig.
Launches the monitoring agent which watches polecats for stuck or idle
states and takes action to keep work flowing.
Examples:
gt witness start greenplace
gt witness start greenplace --foreground`,
Args: cobra.ExactArgs(1),
RunE: runWitnessStart,
}
var witnessStopCmd = &cobra.Command{
Use: "stop <rig>",
Short: "Stop the witness",
Long: `Stop a running Witness.
Gracefully stops the witness monitoring agent.`,
Args: cobra.ExactArgs(1),
RunE: runWitnessStop,
}
var witnessStatusCmd = &cobra.Command{
Use: "status <rig>",
Short: "Show witness status",
Long: `Show the status of a rig's Witness.
Displays running state, monitored polecats, and statistics.`,
Args: cobra.ExactArgs(1),
RunE: runWitnessStatus,
}
var witnessAttachCmd = &cobra.Command{
Use: "attach [rig]",
Aliases: []string{"at"},
Short: "Attach to witness session",
Long: `Attach to the Witness tmux session for a rig.
Attaches the current terminal to the witness's tmux session.
Detach with Ctrl-B D.
If the witness is not running, this will start it first.
If rig is not specified, infers it from the current directory.
Examples:
gt witness attach greenplace
gt witness attach # infer rig from cwd`,
Args: cobra.MaximumNArgs(1),
RunE: runWitnessAttach,
}
var witnessRestartCmd = &cobra.Command{
Use: "restart <rig>",
Short: "Restart the witness",
Long: `Restart the Witness for a rig.
Stops the current session (if running) and starts a fresh one.
Examples:
gt witness restart greenplace`,
Args: cobra.ExactArgs(1),
RunE: runWitnessRestart,
}
func init() {
// Start flags
witnessStartCmd.Flags().BoolVar(&witnessForeground, "foreground", false, "Run in foreground (default: background)")
// Status flags
witnessStatusCmd.Flags().BoolVar(&witnessStatusJSON, "json", false, "Output as JSON")
// Add subcommands
witnessCmd.AddCommand(witnessStartCmd)
witnessCmd.AddCommand(witnessStopCmd)
witnessCmd.AddCommand(witnessRestartCmd)
witnessCmd.AddCommand(witnessStatusCmd)
witnessCmd.AddCommand(witnessAttachCmd)
rootCmd.AddCommand(witnessCmd)
}
// getWitnessManager creates a witness manager for a rig.
func getWitnessManager(rigName string) (*witness.Manager, *rig.Rig, error) {
_, r, err := getRig(rigName)
if err != nil {
return nil, nil, err
}
mgr := witness.NewManager(r)
return mgr, r, nil
}
func runWitnessStart(cmd *cobra.Command, args []string) error {
rigName := args[0]
mgr, _, err := getWitnessManager(rigName)
if err != nil {
return err
}
fmt.Printf("Starting witness for %s...\n", rigName)
if err := mgr.Start(witnessForeground); err != nil {
if err == witness.ErrAlreadyRunning {
fmt.Printf("%s Witness is already running\n", style.Dim.Render("⚠"))
fmt.Printf(" %s\n", style.Dim.Render("Use 'gt witness attach' to connect"))
return nil
}
return fmt.Errorf("starting witness: %w", err)
}
if witnessForeground {
fmt.Printf("%s Note: Foreground mode no longer runs patrol loop\n", style.Dim.Render("⚠"))
fmt.Printf(" %s\n", style.Dim.Render("Patrol logic is now handled by mol-witness-patrol molecule"))
return nil
}
fmt.Printf("%s Witness started for %s\n", style.Bold.Render("✓"), rigName)
fmt.Printf(" %s\n", style.Dim.Render("Use 'gt witness attach' to connect"))
fmt.Printf(" %s\n", style.Dim.Render("Use 'gt witness status' to check progress"))
return nil
}
func runWitnessStop(cmd *cobra.Command, args []string) error {
rigName := args[0]
mgr, _, err := getWitnessManager(rigName)
if err != nil {
return err
}
// Kill tmux session if it exists
t := tmux.NewTmux()
sessionName := witnessSessionName(rigName)
running, _ := t.HasSession(sessionName)
if running {
if err := t.KillSession(sessionName); err != nil {
style.PrintWarning("failed to kill session: %v", err)
}
}
// Update state file
if err := mgr.Stop(); err != nil {
if err == witness.ErrNotRunning && !running {
fmt.Printf("%s Witness is not running\n", style.Dim.Render("⚠"))
return nil
}
// Even if manager.Stop fails, if we killed the session it's stopped
if !running {
return fmt.Errorf("stopping witness: %w", err)
}
}
fmt.Printf("%s Witness stopped for %s\n", style.Bold.Render("✓"), rigName)
return nil
}
func runWitnessStatus(cmd *cobra.Command, args []string) error {
rigName := args[0]
mgr, _, err := getWitnessManager(rigName)
if err != nil {
return err
}
w, err := mgr.Status()
if err != nil {
return fmt.Errorf("getting status: %w", err)
}
// Check actual tmux session state (more reliable than state file)
t := tmux.NewTmux()
sessionName := witnessSessionName(rigName)
sessionRunning, _ := t.HasSession(sessionName)
// Reconcile state: tmux session is the source of truth for background mode
if sessionRunning && w.State != witness.StateRunning {
w.State = witness.StateRunning
} else if !sessionRunning && w.State == witness.StateRunning {
w.State = witness.StateStopped
}
// JSON output
if witnessStatusJSON {
enc := json.NewEncoder(os.Stdout)
enc.SetIndent("", " ")
return enc.Encode(w)
}
// Human-readable output
fmt.Printf("%s Witness: %s\n\n", style.Bold.Render(AgentTypeIcons[AgentWitness]), rigName)
stateStr := string(w.State)
switch w.State {
case witness.StateRunning:
stateStr = style.Bold.Render("● running")
case witness.StateStopped:
stateStr = style.Dim.Render("○ stopped")
case witness.StatePaused:
stateStr = style.Dim.Render("⏸ paused")
}
fmt.Printf(" State: %s\n", stateStr)
if sessionRunning {
fmt.Printf(" Session: %s\n", sessionName)
}
if w.StartedAt != nil {
fmt.Printf(" Started: %s\n", w.StartedAt.Format("2006-01-02 15:04:05"))
}
// Show monitored polecats
fmt.Printf("\n %s\n", style.Bold.Render("Monitored Polecats:"))
if len(w.MonitoredPolecats) == 0 {
fmt.Printf(" %s\n", style.Dim.Render("(none)"))
} else {
for _, p := range w.MonitoredPolecats {
fmt.Printf(" • %s\n", p)
}
}
return nil
}
// witnessSessionName returns the tmux session name for a rig's witness.
func witnessSessionName(rigName string) string {
return fmt.Sprintf("gt-%s-witness", rigName)
}
func runWitnessAttach(cmd *cobra.Command, args []string) error {
rigName := ""
if len(args) > 0 {
rigName = args[0]
}
// Infer rig from cwd if not provided
if rigName == "" {
townRoot, err := workspace.FindFromCwdOrError()
if err != nil {
return fmt.Errorf("not in a Gas Town workspace: %w", err)
}
rigName, err = inferRigFromCwd(townRoot)
if err != nil {
return fmt.Errorf("could not determine rig: %w\nUsage: gt witness attach <rig>", err)
}
}
// Verify rig exists and get manager
mgr, _, err := getWitnessManager(rigName)
if err != nil {
return err
}
sessionName := witnessSessionName(rigName)
// Ensure session exists (creates if needed)
if err := mgr.Start(false); err != nil && err != witness.ErrAlreadyRunning {
return err
} else if err == nil {
fmt.Printf("Started witness session for %s\n", rigName)
}
// Attach to the session
tmuxPath, err := exec.LookPath("tmux")
if err != nil {
return fmt.Errorf("tmux not found: %w", err)
}
attachCmd := exec.Command(tmuxPath, "attach-session", "-t", sessionName)
attachCmd.Stdin = os.Stdin
attachCmd.Stdout = os.Stdout
attachCmd.Stderr = os.Stderr
return attachCmd.Run()
}
func runWitnessRestart(cmd *cobra.Command, args []string) error {
rigName := args[0]
mgr, _, err := getWitnessManager(rigName)
if err != nil {
return err
}
fmt.Printf("Restarting witness for %s...\n", rigName)
// Stop existing session (non-fatal: may not be running)
_ = mgr.Stop()
// Start fresh
if err := mgr.Start(false); err != nil {
return fmt.Errorf("starting witness: %w", err)
}
fmt.Printf("%s Witness restarted for %s\n", style.Bold.Render("✓"), rigName)
fmt.Printf(" %s\n", style.Dim.Render("Use 'gt witness attach' to connect"))
return nil
}