package cmd import ( "encoding/json" "fmt" "os" "path/filepath" "github.com/spf13/cobra" "github.com/steveyegge/gastown/internal/beads" "github.com/steveyegge/gastown/internal/config" "github.com/steveyegge/gastown/internal/constants" "github.com/steveyegge/gastown/internal/crew" "github.com/steveyegge/gastown/internal/git" "github.com/steveyegge/gastown/internal/rig" "github.com/steveyegge/gastown/internal/style" "github.com/steveyegge/gastown/internal/tmux" "github.com/steveyegge/gastown/internal/workspace" ) var statusJSON bool var statusCmd = &cobra.Command{ Use: "status", Aliases: []string{"stat"}, GroupID: GroupDiag, Short: "Show overall town status", Long: `Display the current status of the Gas Town workspace. Shows town name, registered rigs, active polecats, and witness status.`, RunE: runStatus, } func init() { statusCmd.Flags().BoolVar(&statusJSON, "json", false, "Output as JSON") rootCmd.AddCommand(statusCmd) } // TownStatus represents the overall status of the workspace. type TownStatus struct { Name string `json:"name"` Location string `json:"location"` Agents []AgentRuntime `json:"agents"` // Global agents (Mayor, Deacon) Rigs []RigStatus `json:"rigs"` Summary StatusSum `json:"summary"` } // AgentRuntime represents the runtime state of an agent. type AgentRuntime struct { Name string `json:"name"` // Display name (e.g., "mayor", "witness") Address string `json:"address"` // Full address (e.g., "gastown/witness") Session string `json:"session"` // tmux session name Role string `json:"role"` // Role type Running bool `json:"running"` // Is tmux session running? HasWork bool `json:"has_work"` // Has pinned work? WorkTitle string `json:"work_title,omitempty"` // Title of pinned work } // RigStatus represents status of a single rig. type RigStatus struct { Name string `json:"name"` Polecats []string `json:"polecats"` PolecatCount int `json:"polecat_count"` Crews []string `json:"crews"` CrewCount int `json:"crew_count"` HasWitness bool `json:"has_witness"` HasRefinery bool `json:"has_refinery"` Hooks []AgentHookInfo `json:"hooks,omitempty"` Agents []AgentRuntime `json:"agents,omitempty"` // Runtime state of all agents in rig } // AgentHookInfo represents an agent's hook (pinned work) status. type AgentHookInfo struct { Agent string `json:"agent"` // Agent address (e.g., "gastown/toast", "gastown/witness") Role string `json:"role"` // Role type (polecat, crew, witness, refinery) HasWork bool `json:"has_work"` // Whether agent has pinned work Molecule string `json:"molecule,omitempty"` // Attached molecule ID Title string `json:"title,omitempty"` // Pinned bead title } // StatusSum provides summary counts. type StatusSum struct { RigCount int `json:"rig_count"` PolecatCount int `json:"polecat_count"` CrewCount int `json:"crew_count"` WitnessCount int `json:"witness_count"` RefineryCount int `json:"refinery_count"` ActiveHooks int `json:"active_hooks"` } func runStatus(cmd *cobra.Command, args []string) error { // Find town root townRoot, err := workspace.FindFromCwdOrError() if err != nil { return fmt.Errorf("not in a Gas Town workspace: %w", err) } // Load town config townConfigPath := constants.MayorTownPath(townRoot) townConfig, err := config.LoadTownConfig(townConfigPath) if err != nil { // Try to continue without config townConfig = &config.TownConfig{Name: filepath.Base(townRoot)} } // Load rigs config rigsConfigPath := constants.MayorRigsPath(townRoot) rigsConfig, err := config.LoadRigsConfig(rigsConfigPath) if err != nil { // Empty config if file doesn't exist rigsConfig = &config.RigsConfig{Rigs: make(map[string]config.RigEntry)} } // Create rig manager g := git.NewGit(townRoot) mgr := rig.NewManager(townRoot, rigsConfig, g) // Create tmux instance for runtime checks t := tmux.NewTmux() // Discover rigs rigs, err := mgr.DiscoverRigs() if err != nil { return fmt.Errorf("discovering rigs: %w", err) } // Build status status := TownStatus{ Name: townConfig.Name, Location: townRoot, Agents: discoverGlobalAgents(t), Rigs: make([]RigStatus, 0, len(rigs)), } for _, r := range rigs { rs := RigStatus{ Name: r.Name, Polecats: r.Polecats, PolecatCount: len(r.Polecats), HasWitness: r.HasWitness, HasRefinery: r.HasRefinery, } // Count crew workers crewGit := git.NewGit(r.Path) crewMgr := crew.NewManager(r, crewGit) if workers, err := crewMgr.List(); err == nil { for _, w := range workers { rs.Crews = append(rs.Crews, w.Name) } rs.CrewCount = len(workers) } // Discover hooks for all agents in this rig rs.Hooks = discoverRigHooks(r, rs.Crews) for _, hook := range rs.Hooks { if hook.HasWork { status.Summary.ActiveHooks++ } } // Discover runtime state for all agents in this rig rs.Agents = discoverRigAgents(t, r, rs.Crews) status.Rigs = append(status.Rigs, rs) // Update summary status.Summary.PolecatCount += len(r.Polecats) status.Summary.CrewCount += rs.CrewCount if r.HasWitness { status.Summary.WitnessCount++ } if r.HasRefinery { status.Summary.RefineryCount++ } } status.Summary.RigCount = len(rigs) // Output if statusJSON { return outputStatusJSON(status) } return outputStatusText(status) } func outputStatusJSON(status TownStatus) error { enc := json.NewEncoder(os.Stdout) enc.SetIndent("", " ") return enc.Encode(status) } func outputStatusText(status TownStatus) error { // Header fmt.Printf("%s %s\n", style.Bold.Render("⚙️ Gas Town:"), status.Name) fmt.Printf(" Location: %s\n\n", style.Dim.Render(status.Location)) // Global Agents (Mayor, Deacon) fmt.Printf("%s\n", style.Bold.Render("Agents")) for _, agent := range status.Agents { statusStr := style.Success.Render("✓ running") if !agent.Running { statusStr = style.Error.Render("✗ stopped") } fmt.Printf(" %-14s %s\n", agent.Name, statusStr) } if len(status.Rigs) == 0 { fmt.Printf("\n%s\n", style.Dim.Render("No rigs registered. Use 'gt rig add' to add one.")) return nil } // Rigs detail with runtime state fmt.Printf("\n%s\n", style.Bold.Render("Rigs")) for _, r := range status.Rigs { fmt.Printf(" %s\n", style.Bold.Render(r.Name)) // Show all agents with their runtime state for _, agent := range r.Agents { statusStr := style.Success.Render("✓ running") if !agent.Running { statusStr = style.Error.Render("✗ stopped") } // Find hook info for this agent hookInfo := "" for _, h := range r.Hooks { if h.Agent == agent.Address && h.HasWork { if h.Molecule != "" { hookInfo = fmt.Sprintf(" → %s", h.Molecule) } else if h.Title != "" { hookInfo = fmt.Sprintf(" → %s", h.Title) } else { hookInfo = " → (work attached)" } break } } // Format agent name based on role displayName := agent.Name if agent.Role == "crew" { displayName = "crew/" + agent.Name } fmt.Printf(" %-14s %s%s\n", displayName, statusStr, hookInfo) } // Show polecats if any (these are already in r.Agents if discovered) if len(r.Polecats) == 0 && len(r.Crews) == 0 && !r.HasWitness && !r.HasRefinery { fmt.Printf(" %s\n", style.Dim.Render("No agents")) } } return nil } // discoverRigHooks finds all hook attachments for agents in a rig. // It scans polecats, crew workers, witness, and refinery for handoff beads. func discoverRigHooks(r *rig.Rig, crews []string) []AgentHookInfo { var hooks []AgentHookInfo // Create beads instance for the rig b := beads.New(r.Path) // Check polecats for _, name := range r.Polecats { hook := getAgentHook(b, name, r.Name+"/"+name, "polecat") hooks = append(hooks, hook) } // Check crew workers for _, name := range crews { hook := getAgentHook(b, name, r.Name+"/crew/"+name, "crew") hooks = append(hooks, hook) } // Check witness if r.HasWitness { hook := getAgentHook(b, "witness", r.Name+"/witness", "witness") hooks = append(hooks, hook) } // Check refinery if r.HasRefinery { hook := getAgentHook(b, "refinery", r.Name+"/refinery", "refinery") hooks = append(hooks, hook) } return hooks } // discoverGlobalAgents checks runtime state for town-level agents (Mayor, Deacon). func discoverGlobalAgents(t *tmux.Tmux) []AgentRuntime { var agents []AgentRuntime // Check Mayor mayorRunning, _ := t.HasSession(MayorSessionName) agents = append(agents, AgentRuntime{ Name: "mayor", Address: "mayor", Session: MayorSessionName, Role: "coordinator", Running: mayorRunning, }) // Check Deacon deaconRunning, _ := t.HasSession(DeaconSessionName) agents = append(agents, AgentRuntime{ Name: "deacon", Address: "deacon", Session: DeaconSessionName, Role: "health-check", Running: deaconRunning, }) return agents } // discoverRigAgents checks runtime state for all agents in a rig. func discoverRigAgents(t *tmux.Tmux, r *rig.Rig, crews []string) []AgentRuntime { var agents []AgentRuntime // Check Witness if r.HasWitness { sessionName := witnessSessionName(r.Name) running, _ := t.HasSession(sessionName) agents = append(agents, AgentRuntime{ Name: "witness", Address: r.Name + "/witness", Session: sessionName, Role: "witness", Running: running, }) } // Check Refinery if r.HasRefinery { sessionName := fmt.Sprintf("gt-%s-refinery", r.Name) running, _ := t.HasSession(sessionName) agents = append(agents, AgentRuntime{ Name: "refinery", Address: r.Name + "/refinery", Session: sessionName, Role: "refinery", Running: running, }) } // Check Polecats for _, name := range r.Polecats { sessionName := fmt.Sprintf("gt-%s-%s", r.Name, name) running, _ := t.HasSession(sessionName) agents = append(agents, AgentRuntime{ Name: name, Address: r.Name + "/" + name, Session: sessionName, Role: "polecat", Running: running, }) } // Check Crew for _, name := range crews { sessionName := crewSessionName(r.Name, name) running, _ := t.HasSession(sessionName) agents = append(agents, AgentRuntime{ Name: name, Address: r.Name + "/crew/" + name, Session: sessionName, Role: "crew", Running: running, }) } return agents } // getAgentHook retrieves hook status for a specific agent. func getAgentHook(b *beads.Beads, role, agentAddress, roleType string) AgentHookInfo { hook := AgentHookInfo{ Agent: agentAddress, Role: roleType, } // Find handoff bead for this role handoff, err := b.FindHandoffBead(role) if err != nil || handoff == nil { return hook } // Check for attachment attachment := beads.ParseAttachmentFields(handoff) if attachment != nil && attachment.AttachedMolecule != "" { hook.HasWork = true hook.Molecule = attachment.AttachedMolecule hook.Title = handoff.Title } else if handoff.Description != "" { // Has content but no molecule - still has work hook.HasWork = true hook.Title = handoff.Title } return hook }