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>
334 lines
8.6 KiB
Go
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
|
|
}
|