diff --git a/internal/beads/beads.go b/internal/beads/beads.go index 291b8085..466e5789 100644 --- a/internal/beads/beads.go +++ b/internal/beads/beads.go @@ -607,7 +607,8 @@ func ParseAgentFields(description string) *AgentFields { } // CreateAgentBead creates an agent bead for tracking agent lifecycle. -// The ID format is: --- (e.g., gt-polecat-gastown-Toast) +// The ID format is: --- (e.g., gt-gastown-polecat-Toast) +// Use AgentBeadID() helper to generate correct IDs. func (b *Beads) CreateAgentBead(id, title string, fields *AgentFields) (*Issue, error) { description := FormatAgentDescription(title, fields) @@ -706,3 +707,91 @@ func (b *Beads) GetAgentBead(id string) (*Issue, *AgentFields, error) { fields := ParseAgentFields(issue.Description) 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 + } +} diff --git a/internal/cmd/agents.go b/internal/cmd/agents.go index 370f89a0..f506d425 100644 --- a/internal/cmd/agents.go +++ b/internal/cmd/agents.go @@ -143,7 +143,7 @@ func categorizeSession(name string) *AgentSession { return session } - // Witness sessions use different format: gt-witness- + // Witness sessions: legacy format gt-witness- (fallback) if strings.HasPrefix(suffix, "witness-") { session.Type = AgentWitness session.Rig = strings.TrimPrefix(suffix, "witness-") diff --git a/internal/cmd/crew_add.go b/internal/cmd/crew_add.go index 39daaae1..6400708d 100644 --- a/internal/cmd/crew_add.go +++ b/internal/cmd/crew_add.go @@ -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{ diff --git a/internal/cmd/molecule_status.go b/internal/cmd/molecule_status.go index 5c3b08ed..285f6e24 100644 --- a/internal/cmd/molecule_status.go +++ b/internal/cmd/molecule_status.go @@ -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 "" diff --git a/internal/cmd/polecat.go b/internal/cmd/polecat.go index 526d1d8a..2d4f3130 100644 --- a/internal/cmd/polecat.go +++ b/internal/cmd/polecat.go @@ -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 { diff --git a/internal/cmd/prime.go b/internal/cmd/prime.go index 75dad029..e17911ee 100644 --- a/internal/cmd/prime.go +++ b/internal/cmd/prime.go @@ -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: diff --git a/internal/cmd/sling.go b/internal/cmd/sling.go index 58e40473..3c938dcb 100644 --- a/internal/cmd/sling.go +++ b/internal/cmd/sling.go @@ -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 "" } diff --git a/internal/cmd/status.go b/internal/cmd/status.go index 149ad727..9f971643 100644 --- a/internal/cmd/status.go +++ b/internal/cmd/status.go @@ -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 diff --git a/internal/cmd/statusline.go b/internal/cmd/statusline.go index 70b21ab1..e0cf6704 100644 --- a/internal/cmd/statusline.go +++ b/internal/cmd/statusline.go @@ -60,8 +60,8 @@ func runStatusLine(cmd *cobra.Command, args []string) error { return runDeaconStatusLine(t) } - // Witness status line (session naming: gt-witness-) - if role == "witness" || strings.HasPrefix(statusLineSession, "gt-witness-") { + // Witness status line (session naming: gt--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- - if strings.HasPrefix(statusLineSession, "gt-witness-") { - rigName = strings.TrimPrefix(statusLineSession, "gt-witness-") + // Try to extract from session name: gt--witness + if strings.HasSuffix(statusLineSession, "-witness") && strings.HasPrefix(statusLineSession, "gt-") { + rigName = strings.TrimPrefix(strings.TrimSuffix(statusLineSession, "-witness"), "gt-") } } diff --git a/internal/cmd/statusline_test.go b/internal/cmd/statusline_test.go index 3e8a72fd..5f31d337 100644 --- a/internal/cmd/statusline_test.go +++ b/internal/cmd/statusline_test.go @@ -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-) - {"gt-witness-gastown", "gastown"}, - {"gt-witness-myrig", "myrig"}, - // Legacy format still works as fallback + // Witness sessions (canonical format: gt--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}, diff --git a/internal/cmd/theme.go b/internal/cmd/theme.go index 998ac348..0c258f95 100644 --- a/internal/cmd/theme.go +++ b/internal/cmd/theme.go @@ -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 = strings.TrimPrefix(session, "gt-witness-") + } else if strings.HasSuffix(session, "-witness") && strings.HasPrefix(session, "gt-") { + // Witness sessions: gt--witness + rig = strings.TrimPrefix(strings.TrimSuffix(session, "-witness"), "gt-") theme = getThemeForRole(rig, "witness") worker = "witness" role = "witness" diff --git a/internal/daemon/daemon.go b/internal/daemon/daemon.go index ae89b9ac..b8573dc8 100644 --- a/internal/daemon/daemon.go +++ b/internal/daemon/daemon.go @@ -12,6 +12,7 @@ import ( "syscall" "time" + "github.com/steveyegge/gastown/internal/beads" "github.com/steveyegge/gastown/internal/constants" "github.com/steveyegge/gastown/internal/keepalive" "github.com/steveyegge/gastown/internal/polecat" @@ -327,7 +328,7 @@ func (d *Daemon) ensureWitnessesRunning() { // ensureWitnessRunning ensures the witness for a specific rig is running. func (d *Daemon) ensureWitnessRunning(rigName string) { - agentID := "gt-witness-" + rigName + agentID := beads.WitnessBeadID(rigName) sessionName := "gt-" + rigName + "-witness" // 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. func (d *Daemon) pokeWitness(rigName string) { - agentID := "gt-witness-" + rigName + agentID := beads.WitnessBeadID(rigName) sessionName := "gt-" + rigName + "-witness" // Check agent bead state (ZFC: trust what agent reports) diff --git a/internal/daemon/lifecycle.go b/internal/daemon/lifecycle.go index 89f2b440..ddba0c78 100644 --- a/internal/daemon/lifecycle.go +++ b/internal/daemon/lifecycle.go @@ -544,33 +544,34 @@ func (d *Daemon) getAgentBeadInfo(agentBeadID string) (*AgentBeadInfo, error) { } // identityToAgentBeadID maps a daemon identity to an agent bead ID. +// Uses the canonical naming convention: prefix-rig-role-name // Examples: // - "deacon" → "gt-deacon" // - "mayor" → "gt-mayor" -// - "gastown-witness" → "gt-witness-gastown" -// - "gastown-refinery" → "gt-refinery-gastown" +// - "gastown-witness" → "gt-gastown-witness" +// - "gastown-refinery" → "gt-gastown-refinery" func (d *Daemon) identityToAgentBeadID(identity string) string { switch identity { case "deacon": - return "gt-deacon" + return beads.DeaconBeadID() case "mayor": - return "gt-mayor" + return beads.MayorBeadID() default: - // Pattern: -witness → gt-witness- + // Pattern: -witness → gt--witness if strings.HasSuffix(identity, "-witness") { rigName := strings.TrimSuffix(identity, "-witness") - return "gt-witness-" + rigName + return beads.WitnessBeadID(rigName) } - // Pattern: -refinery → gt-refinery- + // Pattern: -refinery → gt--refinery if strings.HasSuffix(identity, "-refinery") { rigName := strings.TrimSuffix(identity, "-refinery") - return "gt-refinery-" + rigName + return beads.RefineryBeadID(rigName) } - // Pattern: -crew- → gt-crew-- + // Pattern: -crew- → gt--crew- if strings.Contains(identity, "-crew-") { parts := strings.SplitN(identity, "-crew-", 2) if len(parts) == 2 { - return "gt-crew-" + parts[0] + "-" + parts[1] + return beads.CrewBeadID(parts[0], parts[1]) } } // Unknown format @@ -588,16 +589,16 @@ const DeadAgentTimeout = 15 * time.Minute func (d *Daemon) checkStaleAgents() { // Known agent bead IDs to check agentBeadIDs := []string{ - "gt-deacon", - "gt-mayor", + beads.DeaconBeadID(), + beads.MayorBeadID(), } // Add rig-specific agents (witness, refinery) for known rigs // For now, we check gastown - could be expanded to discover rigs dynamically rigs := []string{"gastown", "beads"} for _, rig := range rigs { - agentBeadIDs = append(agentBeadIDs, "gt-witness-"+rig) - agentBeadIDs = append(agentBeadIDs, "gt-refinery-"+rig) + agentBeadIDs = append(agentBeadIDs, beads.WitnessBeadID(rig)) + agentBeadIDs = append(agentBeadIDs, beads.RefineryBeadID(rig)) } for _, agentBeadID := range agentBeadIDs { diff --git a/internal/doctor/agent_beads_check.go b/internal/doctor/agent_beads_check.go index 749c6013..708b7188 100644 --- a/internal/doctor/agent_beads_check.go +++ b/internal/doctor/agent_beads_check.go @@ -76,14 +76,12 @@ func (c *AgentBeadsCheck) Run(ctx *CheckContext) *CheckResult { // Find the first rig (by name, alphabetically) for global agents // Only consider gt-prefix rigs since other prefixes can't have agent beads yet var firstRigName string - var firstPrefix string for prefix, rigName := range prefixToRig { if prefix != "gt" { continue // Skip non-gt prefixes for first rig selection } if firstRigName == "" || rigName < firstRigName { firstRigName = rigName - firstPrefix = prefix } } @@ -101,9 +99,9 @@ func (c *AgentBeadsCheck) Run(ctx *CheckContext) *CheckResult { rigBeadsPath := filepath.Join(ctx.TownRoot, rigName, "mayor", "rig") bd := beads.New(rigBeadsPath) - // Check rig-specific agents - witnessID := fmt.Sprintf("%s-witness-%s", prefix, rigName) - refineryID := fmt.Sprintf("%s-refinery-%s", prefix, rigName) + // Check rig-specific agents (using canonical naming: prefix-rig-role-name) + witnessID := beads.WitnessBeadID(rigName) + refineryID := beads.RefineryBeadID(rigName) if _, err := bd.Show(witnessID); err != nil { missing = append(missing, witnessID) @@ -118,7 +116,7 @@ func (c *AgentBeadsCheck) Run(ctx *CheckContext) *CheckResult { // Check crew worker agents crewWorkers := listCrewWorkers(ctx.TownRoot, rigName) 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 { missing = append(missing, crewID) } @@ -127,8 +125,8 @@ func (c *AgentBeadsCheck) Run(ctx *CheckContext) *CheckResult { // Check global agents in first rig if rigName == firstRigName { - deaconID := firstPrefix + "-deacon" - mayorID := firstPrefix + "-mayor" + deaconID := beads.DeaconBeadID() + mayorID := beads.MayorBeadID() if _, err := bd.Show(deaconID); err != nil { 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) var firstRigName string - var firstPrefix string for prefix, rigName := range prefixToRig { if prefix != "gt" { continue } if firstRigName == "" || rigName < firstRigName { firstRigName = rigName - firstPrefix = prefix } } @@ -219,8 +215,8 @@ func (c *AgentBeadsCheck) Fix(ctx *CheckContext) error { rigBeadsPath := filepath.Join(ctx.TownRoot, rigName, "mayor", "rig") bd := beads.New(rigBeadsPath) - // Create rig-specific agents if missing - witnessID := fmt.Sprintf("%s-witness-%s", prefix, rigName) + // Create rig-specific agents if missing (using canonical naming: prefix-rig-role-name) + witnessID := beads.WitnessBeadID(rigName) if _, err := bd.Show(witnessID); err != nil { fields := &beads.AgentFields{ 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 { fields := &beads.AgentFields{ RoleType: "refinery", @@ -251,7 +247,7 @@ func (c *AgentBeadsCheck) Fix(ctx *CheckContext) error { // Create crew worker agents if missing crewWorkers := listCrewWorkers(ctx.TownRoot, rigName) 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 { fields := &beads.AgentFields{ RoleType: "crew", @@ -268,7 +264,7 @@ func (c *AgentBeadsCheck) Fix(ctx *CheckContext) error { // Create global agents in first rig if missing if rigName == firstRigName { - deaconID := firstPrefix + "-deacon" + deaconID := beads.DeaconBeadID() if _, err := bd.Show(deaconID); err != nil { fields := &beads.AgentFields{ 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 { fields := &beads.AgentFields{ RoleType: "mayor", diff --git a/internal/polecat/manager.go b/internal/polecat/manager.go index f57afb5f..776931ab 100644 --- a/internal/polecat/manager.go +++ b/internal/polecat/manager.go @@ -84,9 +84,9 @@ func (m *Manager) assigneeID(name string) string { } // agentBeadID returns the agent bead ID for a polecat. -// Format: "gt-polecat--" (e.g., "gt-polecat-gastown-Toast") +// Format: "gt--polecat-" (e.g., "gt-gastown-polecat-Toast") 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. diff --git a/internal/rig/manager.go b/internal/rig/manager.go index 48cd34c7..24631093 100644 --- a/internal/rig/manager.go +++ b/internal/rig/manager.go @@ -414,16 +414,16 @@ func (m *Manager) initAgentBeads(rigPath, rigName, prefix string, isFirstRig boo var agents []agentDef - // Always create rig-specific agents + // Always create rig-specific agents (using canonical naming: prefix-rig-role-name) agents = append(agents, agentDef{ - id: fmt.Sprintf("%s-witness-%s", prefix, rigName), + id: beads.WitnessBeadID(rigName), roleType: "witness", rig: rigName, desc: fmt.Sprintf("Witness for %s - monitors polecat health and progress.", rigName), }, agentDef{ - id: fmt.Sprintf("%s-refinery-%s", prefix, rigName), + id: beads.RefineryBeadID(rigName), roleType: "refinery", rig: 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 { agents = append(agents, agentDef{ - id: prefix + "-deacon", + id: beads.DeaconBeadID(), roleType: "deacon", rig: "", desc: "Deacon (daemon beacon) - receives mechanical heartbeats, runs town plugins and monitoring.", }, agentDef{ - id: prefix + "-mayor", + id: beads.MayorBeadID(), roleType: "mayor", rig: "", desc: "Mayor - global coordinator, handles cross-rig communication and escalations.", diff --git a/internal/tui/feed/events.go b/internal/tui/feed/events.go index a4e28015..149b6b99 100644 --- a/internal/tui/feed/events.go +++ b/internal/tui/feed/events.go @@ -10,6 +10,8 @@ import ( "regexp" "strings" "time" + + "github.com/steveyegge/gastown/internal/beads" ) // EventSource represents a source of events @@ -170,45 +172,39 @@ func parseSimpleLine(line string) *Event { } // 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) { if beadID == "" { return } - // Agent beads: gt-crew-gastown-joe, gt-witness-gastown, gt-mayor - if strings.HasPrefix(beadID, "gt-crew-") { - parts := strings.Split(beadID, "-") - if len(parts) >= 4 { - rig = parts[2] - actor = strings.Join(parts[2:], "/") - role = "crew" + // Use the canonical parser + parsedRig, parsedRole, name, ok := beads.ParseAgentBeadID(beadID) + if !ok { + return + } + + rig = parsedRig + role = parsedRole + + // Build actor identifier + 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-witness-") { - parts := strings.Split(beadID, "-") - if len(parts) >= 3 { - rig = parts[2] - actor = "witness" - role = "witness" - } - } else if strings.HasPrefix(beadID, "gt-refinery-") { - parts := strings.Split(beadID, "-") - if len(parts) >= 3 { - rig = parts[2] - actor = "refinery" - 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" + case "polecat": + if name != "" { + actor = parsedRig + "/" + name + } else { + actor = parsedRole } }