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

View File

@@ -607,7 +607,8 @@ func ParseAgentFields(description string) *AgentFields {
}
// 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) {
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
}
}

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-")

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{

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 ""

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 {

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:

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 ""
}

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

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-")
}
}

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},

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"

View File

@@ -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)

View File

@@ -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: <rig>-witness → gt-witness-<rig>
// Pattern: <rig>-witness → gt-<rig>-witness
if strings.HasSuffix(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") {
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-") {
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 {

View File

@@ -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",

View File

@@ -84,9 +84,9 @@ func (m *Manager) assigneeID(name string) string {
}
// 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 {
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.

View File

@@ -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.",

View File

@@ -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
}
}