Enhance gt status with detailed agent bead view (gt-um4iu)

- Show full agent bead ID for each agent (e.g., gt-crew-gastown-joe)
- Display hook/pinned work with bead ID and title
- Use role icons and section separators for clarity
- Fall back to hooks array when agent bead lacks hook_bead field

Example output:
  🎩 Mayor
     gt-mayor running
       hook: (none)

  ─── gastown/ ───────────────────────
  🏭 Refinery
     gt-refinery-gastown running
       hook: refinery Handoff

🤖 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-28 20:45:51 -08:00
parent 2f354b1ef6
commit 8c19752432
+95 -174
View File
@@ -5,6 +5,7 @@ import (
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
"strings"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/steveyegge/gastown/internal/beads" "github.com/steveyegge/gastown/internal/beads"
@@ -200,83 +201,38 @@ func outputStatusText(status TownStatus) error {
fmt.Printf("%s %s\n", style.Bold.Render("Town:"), status.Name) fmt.Printf("%s %s\n", style.Bold.Render("Town:"), status.Name)
fmt.Printf("%s\n\n", style.Dim.Render(status.Location)) fmt.Printf("%s\n\n", style.Dim.Render(status.Location))
// Tree characters
const (
treeBranch = "├── "
treeLast = "└── "
treeVert = "│ "
treeSpace = " "
)
// Role icons // Role icons
roleIcons := map[string]string{ roleIcons := map[string]string{
"mayor": "🎩", "mayor": "🎩",
"deacon": "🔔", "coordinator": "🎩",
"witness": "👁", "deacon": "🔔",
"refinery": "🏭", "health-check": "🔔",
"crew": "👷", "witness": "👁",
"polecat": "😺", "refinery": "🏭",
"crew": "👷",
"polecat": "😺",
} }
// Global Agents (Mayor, Deacon) - these are town-level roles // Global Agents (Mayor, Deacon)
hasRigs := len(status.Rigs) > 0 for _, agent := range status.Agents {
for i, agent := range status.Agents {
isLast := i == len(status.Agents)-1 && !hasRigs
prefix := treeBranch
if isLast {
prefix = treeLast
}
icon := roleIcons[agent.Role] icon := roleIcons[agent.Role]
if icon == "" { if icon == "" {
icon = roleIcons[agent.Name] // fallback to name icon = roleIcons[agent.Name]
} }
fmt.Printf("%s %s\n", icon, style.Bold.Render(capitalizeFirst(agent.Name)))
roleLabel := style.Bold.Render(fmt.Sprintf("%s %s", icon, capitalizeFirst(agent.Name))) renderAgentDetails(agent, " ", nil)
fmt.Printf("%s%s\n", prefix, roleLabel) fmt.Println()
// Show agent instance under role
childPrefix := treeVert
if isLast {
childPrefix = treeSpace
}
statusStr := style.Success.Render("running")
if !agent.Running {
statusStr = style.Error.Render("stopped")
}
hookInfo := formatHookInfo(agent.HookBead, agent.WorkTitle, 35)
stateInfo := ""
if agent.State != "" && agent.State != "idle" {
stateInfo = style.Dim.Render(fmt.Sprintf(" [%s]", agent.State))
}
fmt.Printf("%s%s%s %s%s%s\n", childPrefix, treeLast,
style.Dim.Render("gt-"+agent.Name), statusStr, hookInfo, stateInfo)
} }
if !hasRigs { 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("%s\n", style.Dim.Render("No rigs registered. Use 'gt rig add' to add one."))
return nil return nil
} }
// Rigs section // Rigs
fmt.Printf("%s%s\n", treeLast, style.Bold.Render("Rigs")) for _, r := range status.Rigs {
// Rig header with separator
for ri, r := range status.Rigs { fmt.Printf("─── %s ───────────────────────────────────────────\n\n", style.Bold.Render(r.Name+"/"))
isLastRig := ri == len(status.Rigs)-1
rigPrefix := treeVert
if isLastRig {
rigPrefix = treeSpace
}
rigBranch := treeBranch
if isLastRig {
rigBranch = treeLast
}
fmt.Printf("%s%s%s\n", treeSpace, rigBranch, style.Bold.Render(r.Name+"/"))
// Group agents by role // Group agents by role
var witnesses, refineries, crews, polecats []AgentRuntime var witnesses, refineries, crews, polecats []AgentRuntime
@@ -293,146 +249,111 @@ func outputStatusText(status TownStatus) error {
} }
} }
// Count non-empty role groups
roleGroups := 0
if len(witnesses) > 0 {
roleGroups++
}
if len(refineries) > 0 {
roleGroups++
}
if len(crews) > 0 {
roleGroups++
}
if len(polecats) > 0 {
roleGroups++
}
groupsRendered := 0
baseIndent := treeSpace + rigPrefix
// Witness // Witness
if len(witnesses) > 0 { if len(witnesses) > 0 {
groupsRendered++ fmt.Printf("%s %s\n", roleIcons["witness"], style.Bold.Render("Witness"))
isLastGroup := groupsRendered == roleGroups for _, agent := range witnesses {
groupBranch := treeBranch renderAgentDetails(agent, " ", r.Hooks)
if isLastGroup {
groupBranch = treeLast
} }
fmt.Printf("%s%s%s %s\n", baseIndent, groupBranch, fmt.Println()
roleIcons["witness"], style.Bold.Render("Witness"))
groupIndent := baseIndent + treeVert
if isLastGroup {
groupIndent = baseIndent + treeSpace
}
renderAgentList(witnesses, groupIndent, r.Hooks)
} }
// Refinery // Refinery
if len(refineries) > 0 { if len(refineries) > 0 {
groupsRendered++ fmt.Printf("%s %s\n", roleIcons["refinery"], style.Bold.Render("Refinery"))
isLastGroup := groupsRendered == roleGroups for _, agent := range refineries {
groupBranch := treeBranch renderAgentDetails(agent, " ", r.Hooks)
if isLastGroup {
groupBranch = treeLast
} }
fmt.Printf("%s%s%s %s\n", baseIndent, groupBranch, fmt.Println()
roleIcons["refinery"], style.Bold.Render("Refinery"))
groupIndent := baseIndent + treeVert
if isLastGroup {
groupIndent = baseIndent + treeSpace
}
renderAgentList(refineries, groupIndent, r.Hooks)
} }
// Crew // Crew
if len(crews) > 0 { if len(crews) > 0 {
groupsRendered++ fmt.Printf("%s %s (%d)\n", roleIcons["crew"], style.Bold.Render("Crew"), len(crews))
isLastGroup := groupsRendered == roleGroups for _, agent := range crews {
groupBranch := treeBranch renderAgentDetails(agent, " ", r.Hooks)
if isLastGroup {
groupBranch = treeLast
} }
fmt.Printf("%s%s%s %s\n", baseIndent, groupBranch, fmt.Println()
roleIcons["crew"], style.Bold.Render("Crew"))
groupIndent := baseIndent + treeVert
if isLastGroup {
groupIndent = baseIndent + treeSpace
}
renderAgentList(crews, groupIndent, r.Hooks)
} }
// Polecats // Polecats
if len(polecats) > 0 { if len(polecats) > 0 {
groupsRendered++ fmt.Printf("%s %s (%d)\n", roleIcons["polecat"], style.Bold.Render("Polecats"), len(polecats))
isLastGroup := groupsRendered == roleGroups for _, agent := range polecats {
groupBranch := treeBranch renderAgentDetails(agent, " ", r.Hooks)
if isLastGroup {
groupBranch = treeLast
} }
fmt.Printf("%s%s%s %s\n", baseIndent, groupBranch, fmt.Println()
roleIcons["polecat"], style.Bold.Render("Polecats"))
groupIndent := baseIndent + treeVert
if isLastGroup {
groupIndent = baseIndent + treeSpace
}
renderAgentList(polecats, groupIndent, r.Hooks)
} }
// No agents at all // No agents
if roleGroups == 0 { if len(witnesses) == 0 && len(refineries) == 0 && len(crews) == 0 && len(polecats) == 0 {
fmt.Printf("%s%s%s\n", baseIndent, treeLast, style.Dim.Render("(no agents)")) fmt.Printf(" %s\n\n", style.Dim.Render("(no agents)"))
} }
} }
return nil return nil
} }
// renderAgentList renders a list of agents under a role group // renderAgentDetails renders full agent bead details
func renderAgentList(agents []AgentRuntime, indent string, hooks []AgentHookInfo) { func renderAgentDetails(agent AgentRuntime, indent string, hooks []AgentHookInfo) {
const ( // Line 1: Agent bead ID + status
treeBranch = "├── " statusStr := style.Success.Render("running")
treeLast = "└── " if !agent.Running {
) statusStr = style.Error.Render("stopped")
}
for i, agent := range agents { stateInfo := ""
isLast := i == len(agents)-1 if agent.State != "" && agent.State != "idle" && agent.State != "running" {
branch := treeBranch stateInfo = style.Dim.Render(fmt.Sprintf(" [%s]", agent.State))
if isLast { }
branch = treeLast
}
statusStr := style.Success.Render("running") // Build agent bead ID
if !agent.Running { agentBeadID := "gt-" + agent.Name
statusStr = style.Error.Render("stopped") if agent.Address != "" && agent.Address != agent.Name {
} // Use address for full path agents like gastown/crew/joe → gt-crew-gastown-joe
parts := strings.Split(agent.Address, "/")
hookInfo := formatHookInfo(agent.HookBead, agent.WorkTitle, 30) if len(parts) >= 2 {
if hookInfo == "" { if parts[1] == "crew" && len(parts) >= 3 {
// Fall back to legacy Hooks array agentBeadID = fmt.Sprintf("gt-crew-%s-%s", parts[0], parts[2])
for _, h := range hooks { } else if parts[1] == "witness" || parts[1] == "refinery" {
if h.Agent == agent.Address && h.HasWork { agentBeadID = fmt.Sprintf("gt-%s-%s", parts[1], parts[0])
if h.Molecule != "" { } else if len(parts) == 2 {
hookInfo = fmt.Sprintf(" → %s", h.Molecule) // polecat: rig/name
} else if h.Title != "" { agentBeadID = fmt.Sprintf("gt-polecat-%s-%s", parts[0], parts[1])
hookInfo = fmt.Sprintf(" → %s", truncateWithEllipsis(h.Title, 30))
}
break
}
} }
} }
stateInfo := ""
if agent.State != "" && agent.State != "idle" {
stateInfo = style.Dim.Render(fmt.Sprintf(" [%s]", agent.State))
}
fmt.Printf("%s%s%s %s%s%s\n", indent, branch, agent.Name, statusStr, hookInfo, stateInfo)
} }
fmt.Printf("%s%s %s%s\n", indent, style.Dim.Render(agentBeadID), statusStr, stateInfo)
// Line 2: Hook bead (pinned work)
hookStr := style.Dim.Render("(none)")
hookBead := agent.HookBead
hookTitle := agent.WorkTitle
// Fall back to hooks array if agent bead doesn't have hook info
if hookBead == "" && hooks != nil {
for _, h := range hooks {
if h.Agent == agent.Address && h.HasWork {
hookBead = h.Molecule
hookTitle = h.Title
break
}
}
}
if hookBead != "" {
if hookTitle != "" {
hookStr = fmt.Sprintf("%s → %s", hookBead, truncateWithEllipsis(hookTitle, 40))
} else {
hookStr = hookBead
}
} else if hookTitle != "" {
// Has title but no molecule ID
hookStr = truncateWithEllipsis(hookTitle, 50)
}
fmt.Printf("%s hook: %s\n", indent, hookStr)
} }
// formatHookInfo formats the hook bead and title for display // formatHookInfo formats the hook bead and title for display