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
+1 -1
View File
@@ -143,7 +143,7 @@ func categorizeSession(name string) *AgentSession {
return session
}
// Witness sessions use different format: gt-witness-<rig>
// Witness sessions: legacy format gt-witness-<rig> (fallback)
if strings.HasPrefix(suffix, "witness-") {
session.Type = AgentWitness
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
rigBeadsPath := filepath.Join(r.Path, "mayor", "rig")
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 {
// Agent bead doesn't exist, create it
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)
// buildAgentBeadID constructs the agent bead ID from an agent identity.
// Uses canonical naming: prefix-rig-role-name
// Examples:
// - "mayor" -> "gt-mayor"
// - "deacon" -> "gt-deacon"
// - "gastown/witness" -> "gt-witness-gastown"
// - "gastown/refinery" -> "gt-refinery-gastown"
// - "gastown/nux" (polecat) -> "gt-polecat-gastown-nux"
// - "gastown/witness" -> "gt-gastown-witness"
// - "gastown/refinery" -> "gt-gastown-refinery"
// - "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.
func buildAgentBeadID(identity string, role Role) string {
@@ -32,22 +34,22 @@ func buildAgentBeadID(identity string, role Role) string {
if role == RoleUnknown || role == Role("") {
switch {
case identity == "mayor":
return "gt-mayor"
return beads.MayorBeadID()
case identity == "deacon":
return "gt-deacon"
return beads.DeaconBeadID()
case len(parts) == 2 && parts[1] == "witness":
return "gt-witness-" + parts[0]
return beads.WitnessBeadID(parts[0])
case len(parts) == 2 && parts[1] == "refinery":
return "gt-refinery-" + parts[0]
return beads.RefineryBeadID(parts[0])
case len(parts) == 2:
// 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":
// rig/crew/name - crew member (no agent bead)
return ""
// rig/crew/name - crew member
return beads.CrewBeadID(parts[0], parts[2])
case len(parts) == 3 && parts[1] == "polecats":
// rig/polecats/name - explicit polecat
return "gt-polecat-" + parts[0] + "-" + parts[2]
return beads.PolecatBeadID(parts[0], parts[2])
default:
return ""
}
@@ -55,28 +57,28 @@ func buildAgentBeadID(identity string, role Role) string {
switch role {
case RoleMayor:
return "gt-mayor"
return beads.MayorBeadID()
case RoleDeacon:
return "gt-deacon"
return beads.DeaconBeadID()
case RoleWitness:
if len(parts) >= 1 {
return "gt-witness-" + parts[0]
return beads.WitnessBeadID(parts[0])
}
return "gt-witness"
return ""
case RoleRefinery:
if len(parts) >= 1 {
return "gt-refinery-" + parts[0]
return beads.RefineryBeadID(parts[0])
}
return "gt-refinery"
return ""
case RolePolecat:
if len(parts) >= 2 {
return "gt-polecat-" + parts[0] + "-" + parts[1]
} else if len(parts) == 1 {
return "gt-polecat-" + parts[0]
return beads.PolecatBeadID(parts[0], parts[1])
}
return ""
case RoleCrew:
// Crew members may not have agent beads
if len(parts) >= 3 && parts[1] == "crew" {
return beads.CrewBeadID(parts[0], parts[2])
}
return ""
default:
return ""
+3 -2
View File
@@ -11,6 +11,7 @@ import (
"time"
"github.com/spf13/cobra"
"github.com/steveyegge/gastown/internal/beads"
"github.com/steveyegge/gastown/internal/git"
"github.com/steveyegge/gastown/internal/polecat"
"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(" - Delete worktree: %s/polecats/%s\n", p.r.Path, p.polecatName)
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
}
@@ -1257,7 +1258,7 @@ func runPolecatNuke(cmd *cobra.Command, args []string) error {
}
// 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.Dir = filepath.Join(p.r.Path, "mayor", "rig")
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.
// Uses canonical naming: prefix-rig-role-name
// Returns empty string for unknown roles.
func getAgentBeadID(ctx RoleContext) string {
switch ctx.Role {
case RoleMayor:
return "gt-mayor"
return beads.MayorBeadID()
case RoleDeacon:
return "gt-deacon"
return beads.DeaconBeadID()
case RoleWitness:
if ctx.Rig != "" {
return fmt.Sprintf("gt-witness-%s", ctx.Rig)
return beads.WitnessBeadID(ctx.Rig)
}
return ""
case RoleRefinery:
if ctx.Rig != "" {
return fmt.Sprintf("gt-refinery-%s", ctx.Rig)
return beads.RefineryBeadID(ctx.Rig)
}
return ""
case RolePolecat:
if ctx.Rig != "" && ctx.Polecat != "" {
return fmt.Sprintf("gt-polecat-%s-%s", ctx.Rig, ctx.Polecat)
return beads.PolecatBeadID(ctx.Rig, ctx.Polecat)
}
return ""
case RoleCrew:
if ctx.Rig != "" && ctx.Polecat != "" {
return fmt.Sprintf("gt-crew-%s-%s", ctx.Rig, ctx.Polecat)
return beads.CrewBeadID(ctx.Rig, ctx.Polecat)
}
return ""
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.
func updateAgentHookBead(agentID, beadID string) {
// Convert agent ID to agent bead ID
// Format examples:
// gastown/crew/max -> gt-crew-gastown-max
// gastown/polecats/Toast -> gt-polecat-gastown-Toast
// Format examples (canonical: prefix-rig-role-name):
// gastown/crew/max -> gt-gastown-crew-max
// gastown/polecats/Toast -> gt-gastown-polecat-Toast
// mayor -> gt-mayor
// gastown/witness -> gt-witness-gastown
// gastown/witness -> gt-gastown-witness
agentBeadID := agentIDToBeadID(agentID)
if agentBeadID == "" {
return
@@ -673,13 +673,14 @@ func wakeRigAgents(rigName string) {
}
// agentIDToBeadID converts an agent ID to its corresponding agent bead ID.
// Uses canonical naming: prefix-rig-role-name
func agentIDToBeadID(agentID string) string {
// Handle simple cases
if agentID == "mayor" {
return "gt-mayor"
return beads.MayorBeadID()
}
if agentID == "deacon" {
return "gt-deacon"
return beads.DeaconBeadID()
}
// Parse path-style agent IDs
@@ -692,13 +693,13 @@ func agentIDToBeadID(agentID string) string {
switch {
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":
return fmt.Sprintf("gt-refinery-%s", rig)
return beads.RefineryBeadID(rig)
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":
return fmt.Sprintf("gt-polecat-%s-%s", rig, parts[2])
return beads.PolecatBeadID(rig, parts[2])
default:
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))
}
// Build agent bead ID
// Build agent bead ID using canonical naming: prefix-rig-role-name
agentBeadID := "gt-" + 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
parts := strings.Split(addr, "/")
if len(parts) == 1 {
// Global agent: mayor/, deacon/ → gt-mayor, gt-deacon
agentBeadID = "gt-" + parts[0]
agentBeadID = beads.AgentBeadID("", parts[0], "")
} else if len(parts) >= 2 {
rig := parts[0]
if parts[1] == "crew" && len(parts) >= 3 {
agentBeadID = fmt.Sprintf("gt-crew-%s-%s", parts[0], parts[2])
} else if parts[1] == "witness" || parts[1] == "refinery" {
agentBeadID = fmt.Sprintf("gt-%s-%s", parts[1], parts[0])
agentBeadID = beads.CrewBeadID(rig, parts[2])
} else if parts[1] == "witness" {
agentBeadID = beads.WitnessBeadID(rig)
} else if parts[1] == "refinery" {
agentBeadID = beads.RefineryBeadID(rig)
} else if len(parts) == 2 {
// 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,
}
// 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 {
witness.HookBead = fields.HookBead
witness.State = fields.AgentState
@@ -558,7 +561,7 @@ func discoverRigAgents(t *tmux.Tmux, r *rig.Rig, crews []string, agentBeads *bea
Running: running,
}
// 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 {
refinery.HookBead = fields.HookBead
refinery.State = fields.AgentState
@@ -585,7 +588,7 @@ func discoverRigAgents(t *tmux.Tmux, r *rig.Rig, crews []string, agentBeads *bea
Running: running,
}
// 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 {
polecat.HookBead = fields.HookBead
polecat.State = fields.AgentState
@@ -612,7 +615,7 @@ func discoverRigAgents(t *tmux.Tmux, r *rig.Rig, crews []string, agentBeads *bea
Running: running,
}
// 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 {
crewAgent.HookBead = fields.HookBead
crewAgent.State = fields.AgentState
+5 -5
View File
@@ -60,8 +60,8 @@ func runStatusLine(cmd *cobra.Command, args []string) error {
return runDeaconStatusLine(t)
}
// Witness status line (session naming: gt-witness-<rig>)
if role == "witness" || strings.HasPrefix(statusLineSession, "gt-witness-") {
// Witness status line (session naming: gt-<rig>-witness)
if role == "witness" || strings.HasSuffix(statusLineSession, "-witness") {
return runWitnessStatusLine(t, rigName)
}
@@ -221,9 +221,9 @@ func runDeaconStatusLine(t *tmux.Tmux) error {
// Shows: polecat count, crew count, mail preview
func runWitnessStatusLine(t *tmux.Tmux, rigName string) error {
if rigName == "" {
// Try to extract from session name: gt-witness-<rig>
if strings.HasPrefix(statusLineSession, "gt-witness-") {
rigName = strings.TrimPrefix(statusLineSession, "gt-witness-")
// Try to extract from session name: gt-<rig>-witness
if strings.HasSuffix(statusLineSession, "-witness") && strings.HasPrefix(statusLineSession, "gt-") {
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-myrig-crew-user", "myrig"},
// Witness sessions (actual format: gt-witness-<rig>)
{"gt-witness-gastown", "gastown"},
{"gt-witness-myrig", "myrig"},
// Legacy format still works as fallback
// Witness sessions (canonical format: gt-<rig>-witness)
{"gt-gastown-witness", "gastown"},
{"gt-myrig-witness", "myrig"},
// Legacy format still works as fallback
{"gt-witness-gastown", "gastown"},
{"gt-witness-myrig", "myrig"},
// Refinery sessions
{"gt-gastown-refinery", "gastown"},
@@ -61,8 +61,8 @@ func TestCategorizeSessionType(t *testing.T) {
{"gt-a-b", AgentPolecat},
// Non-polecat sessions
{"gt-witness-gastown", AgentWitness}, // actual format
{"gt-gastown-witness", AgentWitness}, // legacy fallback
{"gt-gastown-witness", AgentWitness}, // canonical format
{"gt-witness-gastown", AgentWitness}, // legacy fallback
{"gt-gastown-refinery", AgentRefinery},
{"gt-gastown-crew-max", 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()
worker = "Deacon"
role = "health-check"
} else if strings.HasPrefix(session, "gt-witness-") {
// Witness sessions: gt-witness-<rig>
rig = strings.TrimPrefix(session, "gt-witness-")
} else if strings.HasSuffix(session, "-witness") && strings.HasPrefix(session, "gt-") {
// Witness sessions: gt-<rig>-witness
rig = strings.TrimPrefix(strings.TrimSuffix(session, "-witness"), "gt-")
theme = getThemeForRole(rig, "witness")
worker = "witness"
role = "witness"