gt status: Show runtime state for all agents (gt-zbmg8)

- Add AgentRuntime struct to track tmux session status
- Implement discoverGlobalAgents() for Mayor/Deacon
- Implement discoverRigAgents() for witness/refinery/crew/polecats
- Update text output to show ✓ running / ✗ stopped for each agent
- Color-code status: green for running, red for stopped
- Include hook info inline with agent status
- JSON output includes full runtime state in 'agents' field

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Steve Yegge
2025-12-26 16:23:02 -08:00
parent 94a41e9d2f
commit 81f3a92a97

View File

@@ -14,6 +14,7 @@ import (
"github.com/steveyegge/gastown/internal/git" "github.com/steveyegge/gastown/internal/git"
"github.com/steveyegge/gastown/internal/rig" "github.com/steveyegge/gastown/internal/rig"
"github.com/steveyegge/gastown/internal/style" "github.com/steveyegge/gastown/internal/style"
"github.com/steveyegge/gastown/internal/tmux"
"github.com/steveyegge/gastown/internal/workspace" "github.com/steveyegge/gastown/internal/workspace"
) )
@@ -37,22 +38,35 @@ func init() {
// TownStatus represents the overall status of the workspace. // TownStatus represents the overall status of the workspace.
type TownStatus struct { type TownStatus struct {
Name string `json:"name"` Name string `json:"name"`
Location string `json:"location"` Location string `json:"location"`
Rigs []RigStatus `json:"rigs"` Agents []AgentRuntime `json:"agents"` // Global agents (Mayor, Deacon)
Summary StatusSum `json:"summary"` 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. // RigStatus represents status of a single rig.
type RigStatus struct { type RigStatus struct {
Name string `json:"name"` Name string `json:"name"`
Polecats []string `json:"polecats"` Polecats []string `json:"polecats"`
PolecatCount int `json:"polecat_count"` PolecatCount int `json:"polecat_count"`
Crews []string `json:"crews"` Crews []string `json:"crews"`
CrewCount int `json:"crew_count"` CrewCount int `json:"crew_count"`
HasWitness bool `json:"has_witness"` HasWitness bool `json:"has_witness"`
HasRefinery bool `json:"has_refinery"` HasRefinery bool `json:"has_refinery"`
Hooks []AgentHookInfo `json:"hooks,omitempty"` 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. // AgentHookInfo represents an agent's hook (pinned work) status.
@@ -101,6 +115,9 @@ func runStatus(cmd *cobra.Command, args []string) error {
g := git.NewGit(townRoot) g := git.NewGit(townRoot)
mgr := rig.NewManager(townRoot, rigsConfig, g) mgr := rig.NewManager(townRoot, rigsConfig, g)
// Create tmux instance for runtime checks
t := tmux.NewTmux()
// Discover rigs // Discover rigs
rigs, err := mgr.DiscoverRigs() rigs, err := mgr.DiscoverRigs()
if err != nil { if err != nil {
@@ -111,6 +128,7 @@ func runStatus(cmd *cobra.Command, args []string) error {
status := TownStatus{ status := TownStatus{
Name: townConfig.Name, Name: townConfig.Name,
Location: townRoot, Location: townRoot,
Agents: discoverGlobalAgents(t),
Rigs: make([]RigStatus, 0, len(rigs)), Rigs: make([]RigStatus, 0, len(rigs)),
} }
@@ -141,6 +159,9 @@ func runStatus(cmd *cobra.Command, args []string) error {
} }
} }
// Discover runtime state for all agents in this rig
rs.Agents = discoverRigAgents(t, r, rs.Crews)
status.Rigs = append(status.Rigs, rs) status.Rigs = append(status.Rigs, rs)
// Update summary // Update summary
@@ -173,65 +194,60 @@ func outputStatusText(status TownStatus) error {
fmt.Printf("%s %s\n", style.Bold.Render("⚙️ Gas Town:"), status.Name) fmt.Printf("%s %s\n", style.Bold.Render("⚙️ Gas Town:"), status.Name)
fmt.Printf(" Location: %s\n\n", style.Dim.Render(status.Location)) fmt.Printf(" Location: %s\n\n", style.Dim.Render(status.Location))
// Summary // Global Agents (Mayor, Deacon)
fmt.Printf("%s\n", style.Bold.Render("Summary")) fmt.Printf("%s\n", style.Bold.Render("Agents"))
fmt.Printf(" Rigs: %d\n", status.Summary.RigCount) for _, agent := range status.Agents {
fmt.Printf(" Polecats: %d\n", status.Summary.PolecatCount) statusStr := style.Success.Render("✓ running")
fmt.Printf(" Crews: %d\n", status.Summary.CrewCount) if !agent.Running {
fmt.Printf(" Witnesses: %d\n", status.Summary.WitnessCount) statusStr = style.Error.Render("✗ stopped")
fmt.Printf(" Refineries: %d\n", status.Summary.RefineryCount) }
fmt.Printf(" Active Hooks: %d\n", status.Summary.ActiveHooks) fmt.Printf(" %-14s %s\n", agent.Name, statusStr)
}
if len(status.Rigs) == 0 { if len(status.Rigs) == 0 {
fmt.Printf("\n%s\n", style.Dim.Render("No rigs registered. Use 'gt rig add' to add one.")) fmt.Printf("\n%s\n", style.Dim.Render("No rigs registered. Use 'gt rig add' to add one."))
return nil return nil
} }
// Rigs detail // Rigs detail with runtime state
fmt.Printf("\n%s\n", style.Bold.Render("Rigs")) fmt.Printf("\n%s\n", style.Bold.Render("Rigs"))
for _, r := range status.Rigs { for _, r := range status.Rigs {
// Rig name with indicators fmt.Printf(" %s\n", style.Bold.Render(r.Name))
indicators := ""
if r.HasWitness {
indicators += " " + AgentTypeIcons[AgentWitness]
}
if r.HasRefinery {
indicators += " " + AgentTypeIcons[AgentRefinery]
}
if r.CrewCount > 0 {
indicators += " " + AgentTypeIcons[AgentCrew]
}
fmt.Printf(" %s%s\n", style.Bold.Render(r.Name), indicators) // Show all agents with their runtime state
for _, agent := range r.Agents {
if len(r.Polecats) > 0 { statusStr := style.Success.Render("✓ running")
fmt.Printf(" Polecats: %v\n", r.Polecats) if !agent.Running {
} else { statusStr = style.Error.Render("✗ stopped")
fmt.Printf(" %s\n", style.Dim.Render("No polecats"))
}
if len(r.Crews) > 0 {
fmt.Printf(" Crews: %v\n", r.Crews)
}
// Show active hooks
activeHooks := []AgentHookInfo{}
for _, h := range r.Hooks {
if h.HasWork {
activeHooks = append(activeHooks, h)
} }
}
if len(activeHooks) > 0 { // Find hook info for this agent
fmt.Printf(" %s\n", style.Bold.Render("Hooks:")) hookInfo := ""
for _, h := range activeHooks { for _, h := range r.Hooks {
if h.Molecule != "" { if h.Agent == agent.Address && h.HasWork {
fmt.Printf(" %s %s → %s\n", AgentTypeIcons[AgentPolecat], h.Agent, h.Molecule) if h.Molecule != "" {
} else if h.Title != "" { hookInfo = fmt.Sprintf(" → %s", h.Molecule)
fmt.Printf(" %s %s → %s\n", AgentTypeIcons[AgentPolecat], h.Agent, h.Title) } else if h.Title != "" {
} else { hookInfo = fmt.Sprintf(" → %s", h.Title)
fmt.Printf(" %s %s → (work attached)\n", AgentTypeIcons[AgentPolecat], h.Agent) } 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"))
} }
} }
@@ -273,6 +289,92 @@ func discoverRigHooks(r *rig.Rig, crews []string) []AgentHookInfo {
return hooks 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. // getAgentHook retrieves hook status for a specific agent.
func getAgentHook(b *beads.Beads, role, agentAddress, roleType string) AgentHookInfo { func getAgentHook(b *beads.Beads, role, agentAddress, roleType string) AgentHookInfo {
hook := AgentHookInfo{ hook := AgentHookInfo{