feat: gt status shows agent bead hook/state (gt-k5dip)

- Add HookBead and State fields to AgentRuntime struct
- discoverGlobalAgents and discoverRigAgents now look up agent beads
- Display hook_bead ID and work title in status output
- Show agent_state when not idle
- Fall back to legacy Hooks array for rig agents without agent beads

🤖 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 09:43:47 -08:00
parent 6cfb31419e
commit db1d144fbb

View File

@@ -54,6 +54,8 @@ type AgentRuntime struct {
Running bool `json:"running"` // Is tmux session running? Running bool `json:"running"` // Is tmux session running?
HasWork bool `json:"has_work"` // Has pinned work? HasWork bool `json:"has_work"` // Has pinned work?
WorkTitle string `json:"work_title,omitempty"` // Title of pinned work WorkTitle string `json:"work_title,omitempty"` // Title of pinned work
HookBead string `json:"hook_bead,omitempty"` // Pinned bead ID from agent bead
State string `json:"state,omitempty"` // Agent state from agent bead
} }
// RigStatus represents status of a single rig. // RigStatus represents status of a single rig.
@@ -124,11 +126,15 @@ func runStatus(cmd *cobra.Command, args []string) error {
return fmt.Errorf("discovering rigs: %w", err) return fmt.Errorf("discovering rigs: %w", err)
} }
// Create beads instance for agent bead lookups (gastown rig holds gt- prefix beads)
gastownBeadsPath := filepath.Join(townRoot, "gastown", "mayor", "rig")
agentBeads := beads.New(gastownBeadsPath)
// Build status // Build status
status := TownStatus{ status := TownStatus{
Name: townConfig.Name, Name: townConfig.Name,
Location: townRoot, Location: townRoot,
Agents: discoverGlobalAgents(t), Agents: discoverGlobalAgents(t, agentBeads),
Rigs: make([]RigStatus, 0, len(rigs)), Rigs: make([]RigStatus, 0, len(rigs)),
} }
@@ -160,7 +166,7 @@ func runStatus(cmd *cobra.Command, args []string) error {
} }
// Discover runtime state for all agents in this rig // Discover runtime state for all agents in this rig
rs.Agents = discoverRigAgents(t, r, rs.Crews) rs.Agents = discoverRigAgents(t, r, rs.Crews, agentBeads)
status.Rigs = append(status.Rigs, rs) status.Rigs = append(status.Rigs, rs)
@@ -201,7 +207,27 @@ func outputStatusText(status TownStatus) error {
if !agent.Running { if !agent.Running {
statusStr = style.Error.Render("✗ stopped") statusStr = style.Error.Render("✗ stopped")
} }
fmt.Printf(" %-14s %s\n", agent.Name, statusStr)
// Show hook bead and state from agent bead
hookInfo := ""
if agent.HookBead != "" {
hookInfo = fmt.Sprintf(" → %s", agent.HookBead)
if agent.WorkTitle != "" {
// Truncate title if too long
title := agent.WorkTitle
if len(title) > 30 {
title = title[:27] + "..."
}
hookInfo = fmt.Sprintf(" → %s (%s)", agent.HookBead, title)
}
}
stateInfo := ""
if agent.State != "" && agent.State != "idle" {
stateInfo = fmt.Sprintf(" [%s]", agent.State)
}
fmt.Printf(" %-14s %s%s%s\n", agent.Name, statusStr, hookInfo, stateInfo)
} }
if len(status.Rigs) == 0 { if len(status.Rigs) == 0 {
@@ -221,19 +247,36 @@ func outputStatusText(status TownStatus) error {
statusStr = style.Error.Render("✗ stopped") statusStr = style.Error.Render("✗ stopped")
} }
// Find hook info for this agent // Show hook bead from agent bead (preferred), fall back to Hooks array
hookInfo := "" hookInfo := ""
for _, h := range r.Hooks { if agent.HookBead != "" {
if h.Agent == agent.Address && h.HasWork { hookInfo = fmt.Sprintf(" → %s", agent.HookBead)
if h.Molecule != "" { if agent.WorkTitle != "" {
hookInfo = fmt.Sprintf(" → %s", h.Molecule) title := agent.WorkTitle
} else if h.Title != "" { if len(title) > 25 {
hookInfo = fmt.Sprintf(" → %s", h.Title) title = title[:22] + "..."
} else {
hookInfo = " → (work attached)"
} }
break hookInfo = fmt.Sprintf(" → %s (%s)", agent.HookBead, title)
} }
} else {
// Fall back to legacy Hooks array
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
}
}
}
stateInfo := ""
if agent.State != "" && agent.State != "idle" {
stateInfo = fmt.Sprintf(" [%s]", agent.State)
} }
// Format agent name based on role // Format agent name based on role
@@ -242,7 +285,7 @@ func outputStatusText(status TownStatus) error {
displayName = "crew/" + agent.Name displayName = "crew/" + agent.Name
} }
fmt.Printf(" %-14s %s%s\n", displayName, statusStr, hookInfo) fmt.Printf(" %-14s %s%s%s\n", displayName, statusStr, hookInfo, stateInfo)
} }
// Show polecats if any (these are already in r.Agents if discovered) // Show polecats if any (these are already in r.Agents if discovered)
@@ -290,86 +333,163 @@ func discoverRigHooks(r *rig.Rig, crews []string) []AgentHookInfo {
} }
// discoverGlobalAgents checks runtime state for town-level agents (Mayor, Deacon). // discoverGlobalAgents checks runtime state for town-level agents (Mayor, Deacon).
func discoverGlobalAgents(t *tmux.Tmux) []AgentRuntime { func discoverGlobalAgents(t *tmux.Tmux, agentBeads *beads.Beads) []AgentRuntime {
var agents []AgentRuntime var agents []AgentRuntime
// Check Mayor // Check Mayor
mayorRunning, _ := t.HasSession(MayorSessionName) mayorRunning, _ := t.HasSession(MayorSessionName)
agents = append(agents, AgentRuntime{ mayor := AgentRuntime{
Name: "mayor", Name: "mayor",
Address: "mayor", Address: "mayor",
Session: MayorSessionName, Session: MayorSessionName,
Role: "coordinator", Role: "coordinator",
Running: mayorRunning, Running: mayorRunning,
}) }
// Look up agent bead for hook/state
if issue, fields, err := agentBeads.GetAgentBead("gt-mayor"); err == nil && issue != nil {
mayor.HookBead = fields.HookBead
mayor.State = fields.AgentState
if fields.HookBead != "" {
mayor.HasWork = true
// Try to get the title of the pinned bead
if pinnedIssue, err := agentBeads.Show(fields.HookBead); err == nil {
mayor.WorkTitle = pinnedIssue.Title
}
}
}
agents = append(agents, mayor)
// Check Deacon // Check Deacon
deaconRunning, _ := t.HasSession(DeaconSessionName) deaconRunning, _ := t.HasSession(DeaconSessionName)
agents = append(agents, AgentRuntime{ deacon := AgentRuntime{
Name: "deacon", Name: "deacon",
Address: "deacon", Address: "deacon",
Session: DeaconSessionName, Session: DeaconSessionName,
Role: "health-check", Role: "health-check",
Running: deaconRunning, Running: deaconRunning,
}) }
// Look up agent bead for hook/state
if issue, fields, err := agentBeads.GetAgentBead("gt-deacon"); err == nil && issue != nil {
deacon.HookBead = fields.HookBead
deacon.State = fields.AgentState
if fields.HookBead != "" {
deacon.HasWork = true
if pinnedIssue, err := agentBeads.Show(fields.HookBead); err == nil {
deacon.WorkTitle = pinnedIssue.Title
}
}
}
agents = append(agents, deacon)
return agents return agents
} }
// discoverRigAgents checks runtime state for all agents in a rig. // discoverRigAgents checks runtime state for all agents in a rig.
func discoverRigAgents(t *tmux.Tmux, r *rig.Rig, crews []string) []AgentRuntime { func discoverRigAgents(t *tmux.Tmux, r *rig.Rig, crews []string, agentBeads *beads.Beads) []AgentRuntime {
var agents []AgentRuntime var agents []AgentRuntime
// Check Witness // Check Witness
if r.HasWitness { if r.HasWitness {
sessionName := witnessSessionName(r.Name) sessionName := witnessSessionName(r.Name)
running, _ := t.HasSession(sessionName) running, _ := t.HasSession(sessionName)
agents = append(agents, AgentRuntime{ witness := AgentRuntime{
Name: "witness", Name: "witness",
Address: r.Name + "/witness", Address: r.Name + "/witness",
Session: sessionName, Session: sessionName,
Role: "witness", Role: "witness",
Running: running, Running: running,
}) }
// Look up agent bead
agentID := fmt.Sprintf("gt-witness-%s", r.Name)
if issue, fields, err := agentBeads.GetAgentBead(agentID); err == nil && issue != nil {
witness.HookBead = fields.HookBead
witness.State = fields.AgentState
if fields.HookBead != "" {
witness.HasWork = true
if pinnedIssue, err := agentBeads.Show(fields.HookBead); err == nil {
witness.WorkTitle = pinnedIssue.Title
}
}
}
agents = append(agents, witness)
} }
// Check Refinery // Check Refinery
if r.HasRefinery { if r.HasRefinery {
sessionName := fmt.Sprintf("gt-%s-refinery", r.Name) sessionName := fmt.Sprintf("gt-%s-refinery", r.Name)
running, _ := t.HasSession(sessionName) running, _ := t.HasSession(sessionName)
agents = append(agents, AgentRuntime{ refinery := AgentRuntime{
Name: "refinery", Name: "refinery",
Address: r.Name + "/refinery", Address: r.Name + "/refinery",
Session: sessionName, Session: sessionName,
Role: "refinery", Role: "refinery",
Running: running, Running: running,
}) }
// Look up agent bead
agentID := fmt.Sprintf("gt-refinery-%s", r.Name)
if issue, fields, err := agentBeads.GetAgentBead(agentID); err == nil && issue != nil {
refinery.HookBead = fields.HookBead
refinery.State = fields.AgentState
if fields.HookBead != "" {
refinery.HasWork = true
if pinnedIssue, err := agentBeads.Show(fields.HookBead); err == nil {
refinery.WorkTitle = pinnedIssue.Title
}
}
}
agents = append(agents, refinery)
} }
// Check Polecats // Check Polecats
for _, name := range r.Polecats { for _, name := range r.Polecats {
sessionName := fmt.Sprintf("gt-%s-%s", r.Name, name) sessionName := fmt.Sprintf("gt-%s-%s", r.Name, name)
running, _ := t.HasSession(sessionName) running, _ := t.HasSession(sessionName)
agents = append(agents, AgentRuntime{ polecat := AgentRuntime{
Name: name, Name: name,
Address: r.Name + "/" + name, Address: r.Name + "/" + name,
Session: sessionName, Session: sessionName,
Role: "polecat", Role: "polecat",
Running: running, Running: running,
}) }
// Look up agent bead
agentID := fmt.Sprintf("gt-polecat-%s-%s", r.Name, name)
if issue, fields, err := agentBeads.GetAgentBead(agentID); err == nil && issue != nil {
polecat.HookBead = fields.HookBead
polecat.State = fields.AgentState
if fields.HookBead != "" {
polecat.HasWork = true
if pinnedIssue, err := agentBeads.Show(fields.HookBead); err == nil {
polecat.WorkTitle = pinnedIssue.Title
}
}
}
agents = append(agents, polecat)
} }
// Check Crew // Check Crew
for _, name := range crews { for _, name := range crews {
sessionName := crewSessionName(r.Name, name) sessionName := crewSessionName(r.Name, name)
running, _ := t.HasSession(sessionName) running, _ := t.HasSession(sessionName)
agents = append(agents, AgentRuntime{ crewAgent := AgentRuntime{
Name: name, Name: name,
Address: r.Name + "/crew/" + name, Address: r.Name + "/crew/" + name,
Session: sessionName, Session: sessionName,
Role: "crew", Role: "crew",
Running: running, Running: running,
}) }
// Look up agent bead
agentID := fmt.Sprintf("gt-crew-%s-%s", r.Name, name)
if issue, fields, err := agentBeads.GetAgentBead(agentID); err == nil && issue != nil {
crewAgent.HookBead = fields.HookBead
crewAgent.State = fields.AgentState
if fields.HookBead != "" {
crewAgent.HasWork = true
if pinnedIssue, err := agentBeads.Show(fields.HookBead); err == nil {
crewAgent.WorkTitle = pinnedIssue.Title
}
}
}
agents = append(agents, crewAgent)
} }
return agents return agents