feat: Standardize agent bead naming to prefix-rig-role-name (gt-zvte2)

Implements canonical naming convention for agent bead IDs:
- Town-level: gt-mayor, gt-deacon (unchanged)
- Rig-level: gt-<rig>-witness, gt-<rig>-refinery (was gt-witness-<rig>)
- Named: gt-<rig>-crew-<name>, gt-<rig>-polecat-<name> (was gt-crew-<rig>-<name>)

Changes:
- Added AgentBeadID helper functions to internal/beads/beads.go
- Updated all ID generation call sites to use helpers
- Fixed session parsing in theme.go, statusline.go, agents.go
- Updated doctor check and fix to use canonical format
- Updated tests for new format

🤖 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-29 14:54:30 -08:00
parent 1b20e1bd2c
commit c92b11d1bd
17 changed files with 230 additions and 139 deletions
+90 -1
View File
@@ -607,7 +607,8 @@ func ParseAgentFields(description string) *AgentFields {
} }
// CreateAgentBead creates an agent bead for tracking agent lifecycle. // CreateAgentBead creates an agent bead for tracking agent lifecycle.
// The ID format is: <prefix>-<role>-<rig>-<name> (e.g., gt-polecat-gastown-Toast) // The ID format is: <prefix>-<rig>-<role>-<name> (e.g., gt-gastown-polecat-Toast)
// Use AgentBeadID() helper to generate correct IDs.
func (b *Beads) CreateAgentBead(id, title string, fields *AgentFields) (*Issue, error) { func (b *Beads) CreateAgentBead(id, title string, fields *AgentFields) (*Issue, error) {
description := FormatAgentDescription(title, fields) description := FormatAgentDescription(title, fields)
@@ -706,3 +707,91 @@ func (b *Beads) GetAgentBead(id string) (*Issue, *AgentFields, error) {
fields := ParseAgentFields(issue.Description) fields := ParseAgentFields(issue.Description)
return issue, fields, nil return issue, fields, nil
} }
// Agent bead ID naming convention:
// prefix-rig-role-name
//
// Examples:
// - gt-mayor (town-level, no rig)
// - gt-deacon (town-level, no rig)
// - gt-gastown-witness (rig-level singleton)
// - gt-gastown-refinery (rig-level singleton)
// - gt-gastown-crew-max (rig-level named agent)
// - gt-gastown-polecat-Toast (rig-level named agent)
// AgentBeadID generates the canonical agent bead ID.
// For town-level agents (mayor, deacon), pass empty rig and name.
// For rig-level singletons (witness, refinery), pass empty name.
// For named agents (crew, polecat), pass all three.
func AgentBeadID(rig, role, name string) string {
if rig == "" {
// Town-level agent: gt-mayor, gt-deacon
return "gt-" + role
}
if name == "" {
// Rig-level singleton: gt-gastown-witness, gt-gastown-refinery
return "gt-" + rig + "-" + role
}
// Rig-level named agent: gt-gastown-crew-max, gt-gastown-polecat-Toast
return "gt-" + rig + "-" + role + "-" + name
}
// MayorBeadID returns the Mayor agent bead ID.
func MayorBeadID() string {
return "gt-mayor"
}
// DeaconBeadID returns the Deacon agent bead ID.
func DeaconBeadID() string {
return "gt-deacon"
}
// WitnessBeadID returns the Witness agent bead ID for a rig.
func WitnessBeadID(rig string) string {
return AgentBeadID(rig, "witness", "")
}
// RefineryBeadID returns the Refinery agent bead ID for a rig.
func RefineryBeadID(rig string) string {
return AgentBeadID(rig, "refinery", "")
}
// CrewBeadID returns a Crew worker agent bead ID.
func CrewBeadID(rig, name string) string {
return AgentBeadID(rig, "crew", name)
}
// PolecatBeadID returns a Polecat agent bead ID.
func PolecatBeadID(rig, name string) string {
return AgentBeadID(rig, "polecat", name)
}
// ParseAgentBeadID parses an agent bead ID into its components.
// Returns rig, role, name, and whether parsing succeeded.
// For town-level agents, rig will be empty.
// For singletons, name will be empty.
func ParseAgentBeadID(id string) (rig, role, name string, ok bool) {
if !strings.HasPrefix(id, "gt-") {
return "", "", "", false
}
rest := strings.TrimPrefix(id, "gt-")
parts := strings.Split(rest, "-")
switch len(parts) {
case 1:
// Town-level: gt-mayor, gt-deacon
return "", parts[0], "", true
case 2:
// Rig-level singleton: gt-gastown-witness
return parts[0], parts[1], "", true
case 3:
// Rig-level named: gt-gastown-crew-max
return parts[0], parts[1], parts[2], true
default:
// Handle names with hyphens: gt-gastown-polecat-my-agent-name
if len(parts) >= 3 {
return parts[0], parts[1], strings.Join(parts[2:], "-"), true
}
return "", "", "", false
}
}
+1 -1
View File
@@ -143,7 +143,7 @@ func categorizeSession(name string) *AgentSession {
return session return session
} }
// Witness sessions use different format: gt-witness-<rig> // Witness sessions: legacy format gt-witness-<rig> (fallback)
if strings.HasPrefix(suffix, "witness-") { if strings.HasPrefix(suffix, "witness-") {
session.Type = AgentWitness session.Type = AgentWitness
session.Rig = strings.TrimPrefix(suffix, "witness-") session.Rig = strings.TrimPrefix(suffix, "witness-")
+1 -1
View File
@@ -81,7 +81,7 @@ func runCrewAdd(cmd *cobra.Command, args []string) error {
// Create agent bead for the crew worker // Create agent bead for the crew worker
rigBeadsPath := filepath.Join(r.Path, "mayor", "rig") rigBeadsPath := filepath.Join(r.Path, "mayor", "rig")
bd := beads.New(rigBeadsPath) bd := beads.New(rigBeadsPath)
crewID := fmt.Sprintf("gt-crew-%s-%s", rigName, name) crewID := beads.CrewBeadID(rigName, name)
if _, err := bd.Show(crewID); err != nil { if _, err := bd.Show(crewID); err != nil {
// Agent bead doesn't exist, create it // Agent bead doesn't exist, create it
fields := &beads.AgentFields{ fields := &beads.AgentFields{
+23 -21
View File
@@ -17,12 +17,14 @@ import (
// Note: Agent field parsing is now in internal/beads/fields.go (AgentFields, ParseAgentFieldsFromDescription) // Note: Agent field parsing is now in internal/beads/fields.go (AgentFields, ParseAgentFieldsFromDescription)
// buildAgentBeadID constructs the agent bead ID from an agent identity. // buildAgentBeadID constructs the agent bead ID from an agent identity.
// Uses canonical naming: prefix-rig-role-name
// Examples: // Examples:
// - "mayor" -> "gt-mayor" // - "mayor" -> "gt-mayor"
// - "deacon" -> "gt-deacon" // - "deacon" -> "gt-deacon"
// - "gastown/witness" -> "gt-witness-gastown" // - "gastown/witness" -> "gt-gastown-witness"
// - "gastown/refinery" -> "gt-refinery-gastown" // - "gastown/refinery" -> "gt-gastown-refinery"
// - "gastown/nux" (polecat) -> "gt-polecat-gastown-nux" // - "gastown/nux" (polecat) -> "gt-gastown-polecat-nux"
// - "gastown/crew/max" -> "gt-gastown-crew-max"
// //
// If role is unknown, it tries to infer from the identity string. // If role is unknown, it tries to infer from the identity string.
func buildAgentBeadID(identity string, role Role) string { func buildAgentBeadID(identity string, role Role) string {
@@ -32,22 +34,22 @@ func buildAgentBeadID(identity string, role Role) string {
if role == RoleUnknown || role == Role("") { if role == RoleUnknown || role == Role("") {
switch { switch {
case identity == "mayor": case identity == "mayor":
return "gt-mayor" return beads.MayorBeadID()
case identity == "deacon": case identity == "deacon":
return "gt-deacon" return beads.DeaconBeadID()
case len(parts) == 2 && parts[1] == "witness": case len(parts) == 2 && parts[1] == "witness":
return "gt-witness-" + parts[0] return beads.WitnessBeadID(parts[0])
case len(parts) == 2 && parts[1] == "refinery": case len(parts) == 2 && parts[1] == "refinery":
return "gt-refinery-" + parts[0] return beads.RefineryBeadID(parts[0])
case len(parts) == 2: case len(parts) == 2:
// Assume rig/name is a polecat // Assume rig/name is a polecat
return "gt-polecat-" + parts[0] + "-" + parts[1] return beads.PolecatBeadID(parts[0], parts[1])
case len(parts) == 3 && parts[1] == "crew": case len(parts) == 3 && parts[1] == "crew":
// rig/crew/name - crew member (no agent bead) // rig/crew/name - crew member
return "" return beads.CrewBeadID(parts[0], parts[2])
case len(parts) == 3 && parts[1] == "polecats": case len(parts) == 3 && parts[1] == "polecats":
// rig/polecats/name - explicit polecat // rig/polecats/name - explicit polecat
return "gt-polecat-" + parts[0] + "-" + parts[2] return beads.PolecatBeadID(parts[0], parts[2])
default: default:
return "" return ""
} }
@@ -55,28 +57,28 @@ func buildAgentBeadID(identity string, role Role) string {
switch role { switch role {
case RoleMayor: case RoleMayor:
return "gt-mayor" return beads.MayorBeadID()
case RoleDeacon: case RoleDeacon:
return "gt-deacon" return beads.DeaconBeadID()
case RoleWitness: case RoleWitness:
if len(parts) >= 1 { if len(parts) >= 1 {
return "gt-witness-" + parts[0] return beads.WitnessBeadID(parts[0])
} }
return "gt-witness" return ""
case RoleRefinery: case RoleRefinery:
if len(parts) >= 1 { if len(parts) >= 1 {
return "gt-refinery-" + parts[0] return beads.RefineryBeadID(parts[0])
} }
return "gt-refinery" return ""
case RolePolecat: case RolePolecat:
if len(parts) >= 2 { if len(parts) >= 2 {
return "gt-polecat-" + parts[0] + "-" + parts[1] return beads.PolecatBeadID(parts[0], parts[1])
} else if len(parts) == 1 {
return "gt-polecat-" + parts[0]
} }
return "" return ""
case RoleCrew: case RoleCrew:
// Crew members may not have agent beads if len(parts) >= 3 && parts[1] == "crew" {
return beads.CrewBeadID(parts[0], parts[2])
}
return "" return ""
default: default:
return "" return ""
+3 -2
View File
@@ -11,6 +11,7 @@ import (
"time" "time"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/steveyegge/gastown/internal/beads"
"github.com/steveyegge/gastown/internal/git" "github.com/steveyegge/gastown/internal/git"
"github.com/steveyegge/gastown/internal/polecat" "github.com/steveyegge/gastown/internal/polecat"
"github.com/steveyegge/gastown/internal/rig" "github.com/steveyegge/gastown/internal/rig"
@@ -1208,7 +1209,7 @@ func runPolecatNuke(cmd *cobra.Command, args []string) error {
fmt.Printf(" - Kill session: gt-%s-%s\n", p.rigName, p.polecatName) fmt.Printf(" - Kill session: gt-%s-%s\n", p.rigName, p.polecatName)
fmt.Printf(" - Delete worktree: %s/polecats/%s\n", p.r.Path, p.polecatName) fmt.Printf(" - Delete worktree: %s/polecats/%s\n", p.r.Path, p.polecatName)
fmt.Printf(" - Delete branch (if exists)\n") fmt.Printf(" - Delete branch (if exists)\n")
fmt.Printf(" - Close agent bead: gt-polecat-%s-%s\n", p.rigName, p.polecatName) fmt.Printf(" - Close agent bead: %s\n", beads.PolecatBeadID(p.rigName, p.polecatName))
continue continue
} }
@@ -1257,7 +1258,7 @@ func runPolecatNuke(cmd *cobra.Command, args []string) error {
} }
// Step 5: Close agent bead (if exists) // Step 5: Close agent bead (if exists)
agentBeadID := fmt.Sprintf("gt-polecat-%s-%s", p.rigName, p.polecatName) agentBeadID := beads.PolecatBeadID(p.rigName, p.polecatName)
closeCmd := exec.Command("bd", "close", agentBeadID, "--reason=nuked") closeCmd := exec.Command("bd", "close", agentBeadID, "--reason=nuked")
closeCmd.Dir = filepath.Join(p.r.Path, "mayor", "rig") closeCmd.Dir = filepath.Join(p.r.Path, "mayor", "rig")
if err := closeCmd.Run(); err != nil { if err := closeCmd.Run(); err != nil {
+7 -6
View File
@@ -1137,31 +1137,32 @@ func getAgentFields(ctx RoleContext, state string) *beads.AgentFields {
} }
// getAgentBeadID returns the agent bead ID for the current role. // getAgentBeadID returns the agent bead ID for the current role.
// Uses canonical naming: prefix-rig-role-name
// Returns empty string for unknown roles. // Returns empty string for unknown roles.
func getAgentBeadID(ctx RoleContext) string { func getAgentBeadID(ctx RoleContext) string {
switch ctx.Role { switch ctx.Role {
case RoleMayor: case RoleMayor:
return "gt-mayor" return beads.MayorBeadID()
case RoleDeacon: case RoleDeacon:
return "gt-deacon" return beads.DeaconBeadID()
case RoleWitness: case RoleWitness:
if ctx.Rig != "" { if ctx.Rig != "" {
return fmt.Sprintf("gt-witness-%s", ctx.Rig) return beads.WitnessBeadID(ctx.Rig)
} }
return "" return ""
case RoleRefinery: case RoleRefinery:
if ctx.Rig != "" { if ctx.Rig != "" {
return fmt.Sprintf("gt-refinery-%s", ctx.Rig) return beads.RefineryBeadID(ctx.Rig)
} }
return "" return ""
case RolePolecat: case RolePolecat:
if ctx.Rig != "" && ctx.Polecat != "" { if ctx.Rig != "" && ctx.Polecat != "" {
return fmt.Sprintf("gt-polecat-%s-%s", ctx.Rig, ctx.Polecat) return beads.PolecatBeadID(ctx.Rig, ctx.Polecat)
} }
return "" return ""
case RoleCrew: case RoleCrew:
if ctx.Rig != "" && ctx.Polecat != "" { if ctx.Rig != "" && ctx.Polecat != "" {
return fmt.Sprintf("gt-crew-%s-%s", ctx.Rig, ctx.Polecat) return beads.CrewBeadID(ctx.Rig, ctx.Polecat)
} }
return "" return ""
default: default:
+11 -10
View File
@@ -632,11 +632,11 @@ func runSlingFormula(args []string) error {
// This enables the witness to see what each agent is working on. // This enables the witness to see what each agent is working on.
func updateAgentHookBead(agentID, beadID string) { func updateAgentHookBead(agentID, beadID string) {
// Convert agent ID to agent bead ID // Convert agent ID to agent bead ID
// Format examples: // Format examples (canonical: prefix-rig-role-name):
// gastown/crew/max -> gt-crew-gastown-max // gastown/crew/max -> gt-gastown-crew-max
// gastown/polecats/Toast -> gt-polecat-gastown-Toast // gastown/polecats/Toast -> gt-gastown-polecat-Toast
// mayor -> gt-mayor // mayor -> gt-mayor
// gastown/witness -> gt-witness-gastown // gastown/witness -> gt-gastown-witness
agentBeadID := agentIDToBeadID(agentID) agentBeadID := agentIDToBeadID(agentID)
if agentBeadID == "" { if agentBeadID == "" {
return return
@@ -673,13 +673,14 @@ func wakeRigAgents(rigName string) {
} }
// agentIDToBeadID converts an agent ID to its corresponding agent bead ID. // agentIDToBeadID converts an agent ID to its corresponding agent bead ID.
// Uses canonical naming: prefix-rig-role-name
func agentIDToBeadID(agentID string) string { func agentIDToBeadID(agentID string) string {
// Handle simple cases // Handle simple cases
if agentID == "mayor" { if agentID == "mayor" {
return "gt-mayor" return beads.MayorBeadID()
} }
if agentID == "deacon" { if agentID == "deacon" {
return "gt-deacon" return beads.DeaconBeadID()
} }
// Parse path-style agent IDs // Parse path-style agent IDs
@@ -692,13 +693,13 @@ func agentIDToBeadID(agentID string) string {
switch { switch {
case len(parts) == 2 && parts[1] == "witness": case len(parts) == 2 && parts[1] == "witness":
return fmt.Sprintf("gt-witness-%s", rig) return beads.WitnessBeadID(rig)
case len(parts) == 2 && parts[1] == "refinery": case len(parts) == 2 && parts[1] == "refinery":
return fmt.Sprintf("gt-refinery-%s", rig) return beads.RefineryBeadID(rig)
case len(parts) == 3 && parts[1] == "crew": case len(parts) == 3 && parts[1] == "crew":
return fmt.Sprintf("gt-crew-%s-%s", rig, parts[2]) return beads.CrewBeadID(rig, parts[2])
case len(parts) == 3 && parts[1] == "polecats": case len(parts) == 3 && parts[1] == "polecats":
return fmt.Sprintf("gt-polecat-%s-%s", rig, parts[2]) return beads.PolecatBeadID(rig, parts[2])
default: default:
return "" return ""
} }
+14 -11
View File
@@ -313,23 +313,26 @@ func renderAgentDetails(agent AgentRuntime, indent string, hooks []AgentHookInfo
stateInfo = style.Dim.Render(fmt.Sprintf(" [%s]", agent.State)) stateInfo = style.Dim.Render(fmt.Sprintf(" [%s]", agent.State))
} }
// Build agent bead ID // Build agent bead ID using canonical naming: prefix-rig-role-name
agentBeadID := "gt-" + agent.Name agentBeadID := "gt-" + agent.Name
if agent.Address != "" && agent.Address != agent.Name { if agent.Address != "" && agent.Address != agent.Name {
// Use address for full path agents like gastown/crew/joe → gt-crew-gastown-joe // Use address for full path agents like gastown/crew/joe → gt-gastown-crew-joe
addr := strings.TrimSuffix(agent.Address, "/") // Remove trailing slash for global agents addr := strings.TrimSuffix(agent.Address, "/") // Remove trailing slash for global agents
parts := strings.Split(addr, "/") parts := strings.Split(addr, "/")
if len(parts) == 1 { if len(parts) == 1 {
// Global agent: mayor/, deacon/ → gt-mayor, gt-deacon // Global agent: mayor/, deacon/ → gt-mayor, gt-deacon
agentBeadID = "gt-" + parts[0] agentBeadID = beads.AgentBeadID("", parts[0], "")
} else if len(parts) >= 2 { } else if len(parts) >= 2 {
rig := parts[0]
if parts[1] == "crew" && len(parts) >= 3 { if parts[1] == "crew" && len(parts) >= 3 {
agentBeadID = fmt.Sprintf("gt-crew-%s-%s", parts[0], parts[2]) agentBeadID = beads.CrewBeadID(rig, parts[2])
} else if parts[1] == "witness" || parts[1] == "refinery" { } else if parts[1] == "witness" {
agentBeadID = fmt.Sprintf("gt-%s-%s", parts[1], parts[0]) agentBeadID = beads.WitnessBeadID(rig)
} else if parts[1] == "refinery" {
agentBeadID = beads.RefineryBeadID(rig)
} else if len(parts) == 2 { } else if len(parts) == 2 {
// polecat: rig/name // polecat: rig/name
agentBeadID = fmt.Sprintf("gt-polecat-%s-%s", parts[0], parts[1]) agentBeadID = beads.PolecatBeadID(rig, parts[1])
} }
} }
} }
@@ -531,7 +534,7 @@ func discoverRigAgents(t *tmux.Tmux, r *rig.Rig, crews []string, agentBeads *bea
Running: running, Running: running,
} }
// Look up agent bead // Look up agent bead
agentID := fmt.Sprintf("gt-witness-%s", r.Name) agentID := beads.WitnessBeadID(r.Name)
if issue, fields, err := agentBeads.GetAgentBead(agentID); err == nil && issue != nil { if issue, fields, err := agentBeads.GetAgentBead(agentID); err == nil && issue != nil {
witness.HookBead = fields.HookBead witness.HookBead = fields.HookBead
witness.State = fields.AgentState witness.State = fields.AgentState
@@ -558,7 +561,7 @@ func discoverRigAgents(t *tmux.Tmux, r *rig.Rig, crews []string, agentBeads *bea
Running: running, Running: running,
} }
// Look up agent bead // Look up agent bead
agentID := fmt.Sprintf("gt-refinery-%s", r.Name) agentID := beads.RefineryBeadID(r.Name)
if issue, fields, err := agentBeads.GetAgentBead(agentID); err == nil && issue != nil { if issue, fields, err := agentBeads.GetAgentBead(agentID); err == nil && issue != nil {
refinery.HookBead = fields.HookBead refinery.HookBead = fields.HookBead
refinery.State = fields.AgentState refinery.State = fields.AgentState
@@ -585,7 +588,7 @@ func discoverRigAgents(t *tmux.Tmux, r *rig.Rig, crews []string, agentBeads *bea
Running: running, Running: running,
} }
// Look up agent bead // Look up agent bead
agentID := fmt.Sprintf("gt-polecat-%s-%s", r.Name, name) agentID := beads.PolecatBeadID(r.Name, name)
if issue, fields, err := agentBeads.GetAgentBead(agentID); err == nil && issue != nil { if issue, fields, err := agentBeads.GetAgentBead(agentID); err == nil && issue != nil {
polecat.HookBead = fields.HookBead polecat.HookBead = fields.HookBead
polecat.State = fields.AgentState polecat.State = fields.AgentState
@@ -612,7 +615,7 @@ func discoverRigAgents(t *tmux.Tmux, r *rig.Rig, crews []string, agentBeads *bea
Running: running, Running: running,
} }
// Look up agent bead // Look up agent bead
agentID := fmt.Sprintf("gt-crew-%s-%s", r.Name, name) agentID := beads.CrewBeadID(r.Name, name)
if issue, fields, err := agentBeads.GetAgentBead(agentID); err == nil && issue != nil { if issue, fields, err := agentBeads.GetAgentBead(agentID); err == nil && issue != nil {
crewAgent.HookBead = fields.HookBead crewAgent.HookBead = fields.HookBead
crewAgent.State = fields.AgentState crewAgent.State = fields.AgentState
+5 -5
View File
@@ -60,8 +60,8 @@ func runStatusLine(cmd *cobra.Command, args []string) error {
return runDeaconStatusLine(t) return runDeaconStatusLine(t)
} }
// Witness status line (session naming: gt-witness-<rig>) // Witness status line (session naming: gt-<rig>-witness)
if role == "witness" || strings.HasPrefix(statusLineSession, "gt-witness-") { if role == "witness" || strings.HasSuffix(statusLineSession, "-witness") {
return runWitnessStatusLine(t, rigName) return runWitnessStatusLine(t, rigName)
} }
@@ -221,9 +221,9 @@ func runDeaconStatusLine(t *tmux.Tmux) error {
// Shows: polecat count, crew count, mail preview // Shows: polecat count, crew count, mail preview
func runWitnessStatusLine(t *tmux.Tmux, rigName string) error { func runWitnessStatusLine(t *tmux.Tmux, rigName string) error {
if rigName == "" { if rigName == "" {
// Try to extract from session name: gt-witness-<rig> // Try to extract from session name: gt-<rig>-witness
if strings.HasPrefix(statusLineSession, "gt-witness-") { if strings.HasSuffix(statusLineSession, "-witness") && strings.HasPrefix(statusLineSession, "gt-") {
rigName = strings.TrimPrefix(statusLineSession, "gt-witness-") rigName = strings.TrimPrefix(strings.TrimSuffix(statusLineSession, "-witness"), "gt-")
} }
} }
+6 -6
View File
@@ -16,12 +16,12 @@ func TestCategorizeSessionRig(t *testing.T) {
{"gt-gastown-crew-max", "gastown"}, {"gt-gastown-crew-max", "gastown"},
{"gt-myrig-crew-user", "myrig"}, {"gt-myrig-crew-user", "myrig"},
// Witness sessions (actual format: gt-witness-<rig>) // Witness sessions (canonical format: gt-<rig>-witness)
{"gt-witness-gastown", "gastown"},
{"gt-witness-myrig", "myrig"},
// Legacy format still works as fallback
{"gt-gastown-witness", "gastown"}, {"gt-gastown-witness", "gastown"},
{"gt-myrig-witness", "myrig"}, {"gt-myrig-witness", "myrig"},
// Legacy format still works as fallback
{"gt-witness-gastown", "gastown"},
{"gt-witness-myrig", "myrig"},
// Refinery sessions // Refinery sessions
{"gt-gastown-refinery", "gastown"}, {"gt-gastown-refinery", "gastown"},
@@ -61,8 +61,8 @@ func TestCategorizeSessionType(t *testing.T) {
{"gt-a-b", AgentPolecat}, {"gt-a-b", AgentPolecat},
// Non-polecat sessions // Non-polecat sessions
{"gt-witness-gastown", AgentWitness}, // actual format {"gt-gastown-witness", AgentWitness}, // canonical format
{"gt-gastown-witness", AgentWitness}, // legacy fallback {"gt-witness-gastown", AgentWitness}, // legacy fallback
{"gt-gastown-refinery", AgentRefinery}, {"gt-gastown-refinery", AgentRefinery},
{"gt-gastown-crew-max", AgentCrew}, {"gt-gastown-crew-max", AgentCrew},
{"gt-myrig-crew-user", AgentCrew}, {"gt-myrig-crew-user", AgentCrew},
+3 -3
View File
@@ -135,9 +135,9 @@ func runThemeApply(cmd *cobra.Command, args []string) error {
theme = tmux.DeaconTheme() theme = tmux.DeaconTheme()
worker = "Deacon" worker = "Deacon"
role = "health-check" role = "health-check"
} else if strings.HasPrefix(session, "gt-witness-") { } else if strings.HasSuffix(session, "-witness") && strings.HasPrefix(session, "gt-") {
// Witness sessions: gt-witness-<rig> // Witness sessions: gt-<rig>-witness
rig = strings.TrimPrefix(session, "gt-witness-") rig = strings.TrimPrefix(strings.TrimSuffix(session, "-witness"), "gt-")
theme = getThemeForRole(rig, "witness") theme = getThemeForRole(rig, "witness")
worker = "witness" worker = "witness"
role = "witness" role = "witness"
+3 -2
View File
@@ -12,6 +12,7 @@ import (
"syscall" "syscall"
"time" "time"
"github.com/steveyegge/gastown/internal/beads"
"github.com/steveyegge/gastown/internal/constants" "github.com/steveyegge/gastown/internal/constants"
"github.com/steveyegge/gastown/internal/keepalive" "github.com/steveyegge/gastown/internal/keepalive"
"github.com/steveyegge/gastown/internal/polecat" "github.com/steveyegge/gastown/internal/polecat"
@@ -327,7 +328,7 @@ func (d *Daemon) ensureWitnessesRunning() {
// ensureWitnessRunning ensures the witness for a specific rig is running. // ensureWitnessRunning ensures the witness for a specific rig is running.
func (d *Daemon) ensureWitnessRunning(rigName string) { func (d *Daemon) ensureWitnessRunning(rigName string) {
agentID := "gt-witness-" + rigName agentID := beads.WitnessBeadID(rigName)
sessionName := "gt-" + rigName + "-witness" sessionName := "gt-" + rigName + "-witness"
// Check agent bead state (ZFC: trust what agent reports) // Check agent bead state (ZFC: trust what agent reports)
@@ -374,7 +375,7 @@ func (d *Daemon) pokeWitnesses() {
// pokeWitness sends a heartbeat to a specific rig's witness. // pokeWitness sends a heartbeat to a specific rig's witness.
func (d *Daemon) pokeWitness(rigName string) { func (d *Daemon) pokeWitness(rigName string) {
agentID := "gt-witness-" + rigName agentID := beads.WitnessBeadID(rigName)
sessionName := "gt-" + rigName + "-witness" sessionName := "gt-" + rigName + "-witness"
// Check agent bead state (ZFC: trust what agent reports) // Check agent bead state (ZFC: trust what agent reports)
+15 -14
View File
@@ -544,33 +544,34 @@ func (d *Daemon) getAgentBeadInfo(agentBeadID string) (*AgentBeadInfo, error) {
} }
// identityToAgentBeadID maps a daemon identity to an agent bead ID. // identityToAgentBeadID maps a daemon identity to an agent bead ID.
// Uses the canonical naming convention: prefix-rig-role-name
// Examples: // Examples:
// - "deacon" → "gt-deacon" // - "deacon" → "gt-deacon"
// - "mayor" → "gt-mayor" // - "mayor" → "gt-mayor"
// - "gastown-witness" → "gt-witness-gastown" // - "gastown-witness" → "gt-gastown-witness"
// - "gastown-refinery" → "gt-refinery-gastown" // - "gastown-refinery" → "gt-gastown-refinery"
func (d *Daemon) identityToAgentBeadID(identity string) string { func (d *Daemon) identityToAgentBeadID(identity string) string {
switch identity { switch identity {
case "deacon": case "deacon":
return "gt-deacon" return beads.DeaconBeadID()
case "mayor": case "mayor":
return "gt-mayor" return beads.MayorBeadID()
default: default:
// Pattern: <rig>-witness → gt-witness-<rig> // Pattern: <rig>-witness → gt-<rig>-witness
if strings.HasSuffix(identity, "-witness") { if strings.HasSuffix(identity, "-witness") {
rigName := strings.TrimSuffix(identity, "-witness") rigName := strings.TrimSuffix(identity, "-witness")
return "gt-witness-" + rigName return beads.WitnessBeadID(rigName)
} }
// Pattern: <rig>-refinery → gt-refinery-<rig> // Pattern: <rig>-refinery → gt-<rig>-refinery
if strings.HasSuffix(identity, "-refinery") { if strings.HasSuffix(identity, "-refinery") {
rigName := strings.TrimSuffix(identity, "-refinery") rigName := strings.TrimSuffix(identity, "-refinery")
return "gt-refinery-" + rigName return beads.RefineryBeadID(rigName)
} }
// Pattern: <rig>-crew-<name> → gt-crew-<rig>-<name> // Pattern: <rig>-crew-<name> → gt-<rig>-crew-<name>
if strings.Contains(identity, "-crew-") { if strings.Contains(identity, "-crew-") {
parts := strings.SplitN(identity, "-crew-", 2) parts := strings.SplitN(identity, "-crew-", 2)
if len(parts) == 2 { if len(parts) == 2 {
return "gt-crew-" + parts[0] + "-" + parts[1] return beads.CrewBeadID(parts[0], parts[1])
} }
} }
// Unknown format // Unknown format
@@ -588,16 +589,16 @@ const DeadAgentTimeout = 15 * time.Minute
func (d *Daemon) checkStaleAgents() { func (d *Daemon) checkStaleAgents() {
// Known agent bead IDs to check // Known agent bead IDs to check
agentBeadIDs := []string{ agentBeadIDs := []string{
"gt-deacon", beads.DeaconBeadID(),
"gt-mayor", beads.MayorBeadID(),
} }
// Add rig-specific agents (witness, refinery) for known rigs // Add rig-specific agents (witness, refinery) for known rigs
// For now, we check gastown - could be expanded to discover rigs dynamically // For now, we check gastown - could be expanded to discover rigs dynamically
rigs := []string{"gastown", "beads"} rigs := []string{"gastown", "beads"}
for _, rig := range rigs { for _, rig := range rigs {
agentBeadIDs = append(agentBeadIDs, "gt-witness-"+rig) agentBeadIDs = append(agentBeadIDs, beads.WitnessBeadID(rig))
agentBeadIDs = append(agentBeadIDs, "gt-refinery-"+rig) agentBeadIDs = append(agentBeadIDs, beads.RefineryBeadID(rig))
} }
for _, agentBeadID := range agentBeadIDs { for _, agentBeadID := range agentBeadIDs {
+12 -16
View File
@@ -76,14 +76,12 @@ func (c *AgentBeadsCheck) Run(ctx *CheckContext) *CheckResult {
// Find the first rig (by name, alphabetically) for global agents // Find the first rig (by name, alphabetically) for global agents
// Only consider gt-prefix rigs since other prefixes can't have agent beads yet // Only consider gt-prefix rigs since other prefixes can't have agent beads yet
var firstRigName string var firstRigName string
var firstPrefix string
for prefix, rigName := range prefixToRig { for prefix, rigName := range prefixToRig {
if prefix != "gt" { if prefix != "gt" {
continue // Skip non-gt prefixes for first rig selection continue // Skip non-gt prefixes for first rig selection
} }
if firstRigName == "" || rigName < firstRigName { if firstRigName == "" || rigName < firstRigName {
firstRigName = rigName firstRigName = rigName
firstPrefix = prefix
} }
} }
@@ -101,9 +99,9 @@ func (c *AgentBeadsCheck) Run(ctx *CheckContext) *CheckResult {
rigBeadsPath := filepath.Join(ctx.TownRoot, rigName, "mayor", "rig") rigBeadsPath := filepath.Join(ctx.TownRoot, rigName, "mayor", "rig")
bd := beads.New(rigBeadsPath) bd := beads.New(rigBeadsPath)
// Check rig-specific agents // Check rig-specific agents (using canonical naming: prefix-rig-role-name)
witnessID := fmt.Sprintf("%s-witness-%s", prefix, rigName) witnessID := beads.WitnessBeadID(rigName)
refineryID := fmt.Sprintf("%s-refinery-%s", prefix, rigName) refineryID := beads.RefineryBeadID(rigName)
if _, err := bd.Show(witnessID); err != nil { if _, err := bd.Show(witnessID); err != nil {
missing = append(missing, witnessID) missing = append(missing, witnessID)
@@ -118,7 +116,7 @@ func (c *AgentBeadsCheck) Run(ctx *CheckContext) *CheckResult {
// Check crew worker agents // Check crew worker agents
crewWorkers := listCrewWorkers(ctx.TownRoot, rigName) crewWorkers := listCrewWorkers(ctx.TownRoot, rigName)
for _, workerName := range crewWorkers { for _, workerName := range crewWorkers {
crewID := fmt.Sprintf("%s-crew-%s-%s", prefix, rigName, workerName) crewID := beads.CrewBeadID(rigName, workerName)
if _, err := bd.Show(crewID); err != nil { if _, err := bd.Show(crewID); err != nil {
missing = append(missing, crewID) missing = append(missing, crewID)
} }
@@ -127,8 +125,8 @@ func (c *AgentBeadsCheck) Run(ctx *CheckContext) *CheckResult {
// Check global agents in first rig // Check global agents in first rig
if rigName == firstRigName { if rigName == firstRigName {
deaconID := firstPrefix + "-deacon" deaconID := beads.DeaconBeadID()
mayorID := firstPrefix + "-mayor" mayorID := beads.MayorBeadID()
if _, err := bd.Show(deaconID); err != nil { if _, err := bd.Show(deaconID); err != nil {
missing = append(missing, deaconID) missing = append(missing, deaconID)
@@ -198,14 +196,12 @@ func (c *AgentBeadsCheck) Fix(ctx *CheckContext) error {
// Find the first rig for global agents (only gt-prefix rigs) // Find the first rig for global agents (only gt-prefix rigs)
var firstRigName string var firstRigName string
var firstPrefix string
for prefix, rigName := range prefixToRig { for prefix, rigName := range prefixToRig {
if prefix != "gt" { if prefix != "gt" {
continue continue
} }
if firstRigName == "" || rigName < firstRigName { if firstRigName == "" || rigName < firstRigName {
firstRigName = rigName firstRigName = rigName
firstPrefix = prefix
} }
} }
@@ -219,8 +215,8 @@ func (c *AgentBeadsCheck) Fix(ctx *CheckContext) error {
rigBeadsPath := filepath.Join(ctx.TownRoot, rigName, "mayor", "rig") rigBeadsPath := filepath.Join(ctx.TownRoot, rigName, "mayor", "rig")
bd := beads.New(rigBeadsPath) bd := beads.New(rigBeadsPath)
// Create rig-specific agents if missing // Create rig-specific agents if missing (using canonical naming: prefix-rig-role-name)
witnessID := fmt.Sprintf("%s-witness-%s", prefix, rigName) witnessID := beads.WitnessBeadID(rigName)
if _, err := bd.Show(witnessID); err != nil { if _, err := bd.Show(witnessID); err != nil {
fields := &beads.AgentFields{ fields := &beads.AgentFields{
RoleType: "witness", RoleType: "witness",
@@ -234,7 +230,7 @@ func (c *AgentBeadsCheck) Fix(ctx *CheckContext) error {
} }
} }
refineryID := fmt.Sprintf("%s-refinery-%s", prefix, rigName) refineryID := beads.RefineryBeadID(rigName)
if _, err := bd.Show(refineryID); err != nil { if _, err := bd.Show(refineryID); err != nil {
fields := &beads.AgentFields{ fields := &beads.AgentFields{
RoleType: "refinery", RoleType: "refinery",
@@ -251,7 +247,7 @@ func (c *AgentBeadsCheck) Fix(ctx *CheckContext) error {
// Create crew worker agents if missing // Create crew worker agents if missing
crewWorkers := listCrewWorkers(ctx.TownRoot, rigName) crewWorkers := listCrewWorkers(ctx.TownRoot, rigName)
for _, workerName := range crewWorkers { for _, workerName := range crewWorkers {
crewID := fmt.Sprintf("%s-crew-%s-%s", prefix, rigName, workerName) crewID := beads.CrewBeadID(rigName, workerName)
if _, err := bd.Show(crewID); err != nil { if _, err := bd.Show(crewID); err != nil {
fields := &beads.AgentFields{ fields := &beads.AgentFields{
RoleType: "crew", RoleType: "crew",
@@ -268,7 +264,7 @@ func (c *AgentBeadsCheck) Fix(ctx *CheckContext) error {
// Create global agents in first rig if missing // Create global agents in first rig if missing
if rigName == firstRigName { if rigName == firstRigName {
deaconID := firstPrefix + "-deacon" deaconID := beads.DeaconBeadID()
if _, err := bd.Show(deaconID); err != nil { if _, err := bd.Show(deaconID); err != nil {
fields := &beads.AgentFields{ fields := &beads.AgentFields{
RoleType: "deacon", RoleType: "deacon",
@@ -282,7 +278,7 @@ func (c *AgentBeadsCheck) Fix(ctx *CheckContext) error {
} }
} }
mayorID := firstPrefix + "-mayor" mayorID := beads.MayorBeadID()
if _, err := bd.Show(mayorID); err != nil { if _, err := bd.Show(mayorID); err != nil {
fields := &beads.AgentFields{ fields := &beads.AgentFields{
RoleType: "mayor", RoleType: "mayor",
+2 -2
View File
@@ -84,9 +84,9 @@ func (m *Manager) assigneeID(name string) string {
} }
// agentBeadID returns the agent bead ID for a polecat. // agentBeadID returns the agent bead ID for a polecat.
// Format: "gt-polecat-<rig>-<name>" (e.g., "gt-polecat-gastown-Toast") // Format: "gt-<rig>-polecat-<name>" (e.g., "gt-gastown-polecat-Toast")
func (m *Manager) agentBeadID(name string) string { func (m *Manager) agentBeadID(name string) string {
return fmt.Sprintf("gt-polecat-%s-%s", m.rig.Name, name) return beads.PolecatBeadID(m.rig.Name, name)
} }
// getCleanupStatusFromBead reads the cleanup_status from the polecat's agent bead. // getCleanupStatusFromBead reads the cleanup_status from the polecat's agent bead.
+5 -5
View File
@@ -414,16 +414,16 @@ func (m *Manager) initAgentBeads(rigPath, rigName, prefix string, isFirstRig boo
var agents []agentDef var agents []agentDef
// Always create rig-specific agents // Always create rig-specific agents (using canonical naming: prefix-rig-role-name)
agents = append(agents, agents = append(agents,
agentDef{ agentDef{
id: fmt.Sprintf("%s-witness-%s", prefix, rigName), id: beads.WitnessBeadID(rigName),
roleType: "witness", roleType: "witness",
rig: rigName, rig: rigName,
desc: fmt.Sprintf("Witness for %s - monitors polecat health and progress.", rigName), desc: fmt.Sprintf("Witness for %s - monitors polecat health and progress.", rigName),
}, },
agentDef{ agentDef{
id: fmt.Sprintf("%s-refinery-%s", prefix, rigName), id: beads.RefineryBeadID(rigName),
roleType: "refinery", roleType: "refinery",
rig: rigName, rig: rigName,
desc: fmt.Sprintf("Refinery for %s - processes merge queue.", rigName), desc: fmt.Sprintf("Refinery for %s - processes merge queue.", rigName),
@@ -434,13 +434,13 @@ func (m *Manager) initAgentBeads(rigPath, rigName, prefix string, isFirstRig boo
if isFirstRig { if isFirstRig {
agents = append(agents, agents = append(agents,
agentDef{ agentDef{
id: prefix + "-deacon", id: beads.DeaconBeadID(),
roleType: "deacon", roleType: "deacon",
rig: "", rig: "",
desc: "Deacon (daemon beacon) - receives mechanical heartbeats, runs town plugins and monitoring.", desc: "Deacon (daemon beacon) - receives mechanical heartbeats, runs town plugins and monitoring.",
}, },
agentDef{ agentDef{
id: prefix + "-mayor", id: beads.MayorBeadID(),
roleType: "mayor", roleType: "mayor",
rig: "", rig: "",
desc: "Mayor - global coordinator, handles cross-rig communication and escalations.", desc: "Mayor - global coordinator, handles cross-rig communication and escalations.",
+28 -32
View File
@@ -10,6 +10,8 @@ import (
"regexp" "regexp"
"strings" "strings"
"time" "time"
"github.com/steveyegge/gastown/internal/beads"
) )
// EventSource represents a source of events // EventSource represents a source of events
@@ -170,45 +172,39 @@ func parseSimpleLine(line string) *Event {
} }
// parseBeadContext extracts actor/rig/role from a bead ID // parseBeadContext extracts actor/rig/role from a bead ID
// Uses canonical naming: prefix-rig-role-name
// Examples: gt-gastown-crew-joe, gt-gastown-witness, gt-mayor
func parseBeadContext(beadID string) (actor, rig, role string) { func parseBeadContext(beadID string) (actor, rig, role string) {
if beadID == "" { if beadID == "" {
return return
} }
// Agent beads: gt-crew-gastown-joe, gt-witness-gastown, gt-mayor // Use the canonical parser
if strings.HasPrefix(beadID, "gt-crew-") { parsedRig, parsedRole, name, ok := beads.ParseAgentBeadID(beadID)
parts := strings.Split(beadID, "-") if !ok {
if len(parts) >= 4 { return
rig = parts[2]
actor = strings.Join(parts[2:], "/")
role = "crew"
} }
} else if strings.HasPrefix(beadID, "gt-witness-") {
parts := strings.Split(beadID, "-") rig = parsedRig
if len(parts) >= 3 { role = parsedRole
rig = parts[2]
actor = "witness" // Build actor identifier
role = "witness" switch parsedRole {
case "mayor", "deacon":
actor = parsedRole
case "witness", "refinery":
actor = parsedRole
case "crew":
if name != "" {
actor = parsedRig + "/crew/" + name
} else {
actor = parsedRole
} }
} else if strings.HasPrefix(beadID, "gt-refinery-") { case "polecat":
parts := strings.Split(beadID, "-") if name != "" {
if len(parts) >= 3 { actor = parsedRig + "/" + name
rig = parts[2] } else {
actor = "refinery" actor = parsedRole
role = "refinery"
}
} else if beadID == "gt-mayor" {
actor = "mayor"
role = "mayor"
} else if beadID == "gt-deacon" {
actor = "deacon"
role = "deacon"
} else if strings.HasPrefix(beadID, "gt-polecat-") {
parts := strings.Split(beadID, "-")
if len(parts) >= 3 {
rig = parts[2]
actor = strings.Join(parts[2:], "-")
role = "polecat"
} }
} }