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:
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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-")
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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 ""
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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 ""
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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-")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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},
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user