Merge pull request #11 from danshapiro/fix/rig-prefix-bead-ids-2

fix: use rig prefixes for agent bead IDs
This commit is contained in:
Steve Yegge
2026-01-02 01:05:16 -08:00
committed by GitHub
5 changed files with 257 additions and 42 deletions

View File

@@ -100,7 +100,8 @@ func runCrewAdd(cmd *cobra.Command, args []string) error {
fmt.Printf(" Branch: %s\n", worker.Branch)
// Create agent bead for the crew worker
crewID := beads.CrewBeadID(rigName, name)
prefix := beads.GetPrefixForRig(townRoot, rigName)
crewID := beads.CrewBeadIDWithPrefix(prefix, rigName, name)
if _, err := bd.Show(crewID); err != nil {
// Agent bead doesn't exist, create it
fields := &beads.AgentFields{

View File

@@ -1203,9 +1203,7 @@ func getAgentFields(ctx RoleContext, state string) *beads.AgentFields {
}
// getAgentBeadID returns the agent bead ID for the current role.
// Uses canonical naming: gt-rig-role-name
// Agent beads always use "gt-" prefix (required by beads validation).
// Only issue beads use rig-specific prefixes.
// Rig-scoped agents use the rig's configured prefix; town agents remain gt-.
// Returns empty string for unknown roles.
func getAgentBeadID(ctx RoleContext) string {
switch ctx.Role {
@@ -1215,22 +1213,26 @@ func getAgentBeadID(ctx RoleContext) string {
return beads.DeaconBeadID()
case RoleWitness:
if ctx.Rig != "" {
return beads.WitnessBeadID(ctx.Rig)
prefix := beads.GetPrefixForRig(ctx.TownRoot, ctx.Rig)
return beads.WitnessBeadIDWithPrefix(prefix, ctx.Rig)
}
return ""
case RoleRefinery:
if ctx.Rig != "" {
return beads.RefineryBeadID(ctx.Rig)
prefix := beads.GetPrefixForRig(ctx.TownRoot, ctx.Rig)
return beads.RefineryBeadIDWithPrefix(prefix, ctx.Rig)
}
return ""
case RolePolecat:
if ctx.Rig != "" && ctx.Polecat != "" {
return beads.PolecatBeadID(ctx.Rig, ctx.Polecat)
prefix := beads.GetPrefixForRig(ctx.TownRoot, ctx.Rig)
return beads.PolecatBeadIDWithPrefix(prefix, ctx.Rig, ctx.Polecat)
}
return ""
case RoleCrew:
if ctx.Rig != "" && ctx.Polecat != "" {
return beads.CrewBeadID(ctx.Rig, ctx.Polecat)
prefix := beads.GetPrefixForRig(ctx.TownRoot, ctx.Rig)
return beads.CrewBeadIDWithPrefix(prefix, ctx.Rig, ctx.Polecat)
}
return ""
default:

View File

@@ -0,0 +1,97 @@
package cmd
import (
"os"
"path/filepath"
"testing"
"github.com/steveyegge/gastown/internal/beads"
)
func writeTestRoutes(t *testing.T, townRoot string, routes []beads.Route) {
t.Helper()
beadsDir := filepath.Join(townRoot, ".beads")
if err := os.MkdirAll(beadsDir, 0755); err != nil {
t.Fatalf("create beads dir: %v", err)
}
if err := beads.WriteRoutes(beadsDir, routes); err != nil {
t.Fatalf("write routes: %v", err)
}
}
func TestGetAgentBeadID_UsesRigPrefix(t *testing.T) {
townRoot := t.TempDir()
writeTestRoutes(t, townRoot, []beads.Route{
{Prefix: "bd-", Path: "beads/mayor/rig"},
})
cases := []struct {
name string
ctx RoleContext
want string
}{
{
name: "mayor",
ctx: RoleContext{
Role: RoleMayor,
TownRoot: townRoot,
},
want: "gt-mayor",
},
{
name: "deacon",
ctx: RoleContext{
Role: RoleDeacon,
TownRoot: townRoot,
},
want: "gt-deacon",
},
{
name: "witness",
ctx: RoleContext{
Role: RoleWitness,
Rig: "beads",
TownRoot: townRoot,
},
want: "bd-beads-witness",
},
{
name: "refinery",
ctx: RoleContext{
Role: RoleRefinery,
Rig: "beads",
TownRoot: townRoot,
},
want: "bd-beads-refinery",
},
{
name: "polecat",
ctx: RoleContext{
Role: RolePolecat,
Rig: "beads",
Polecat: "lex",
TownRoot: townRoot,
},
want: "bd-beads-polecat-lex",
},
{
name: "crew",
ctx: RoleContext{
Role: RoleCrew,
Rig: "beads",
Polecat: "lex",
TownRoot: townRoot,
},
want: "bd-beads-crew-lex",
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
got := getAgentBeadID(tc.ctx)
if got != tc.want {
t.Fatalf("getAgentBeadID() = %q, want %q", got, tc.want)
}
})
}
}

View File

@@ -163,27 +163,42 @@ func runStatus(cmd *cobra.Command, args []string) error {
return fmt.Errorf("discovering rigs: %w", err)
}
// Create beads instance for agent bead lookups (gastown rig holds gt- prefix beads)
gastownBeadsPath := filepath.Join(townRoot, "gastown", "mayor", "rig")
agentBeads := beads.New(gastownBeadsPath)
// Pre-fetch all agent beads in a single query for performance
allAgentBeads, _ := agentBeads.ListAgentBeads()
if allAgentBeads == nil {
allAgentBeads = make(map[string]*beads.Issue)
}
// Pre-fetch all hook beads (referenced in agent beads) in a single query
// Use the HookBead field from the database column, not parsed from description.
var allHookIDs []string
for _, issue := range allAgentBeads {
if issue.HookBead != "" {
allHookIDs = append(allHookIDs, issue.HookBead)
// Pre-fetch agent beads across all rig-specific beads DBs.
allAgentBeads := make(map[string]*beads.Issue)
allHookBeads := make(map[string]*beads.Issue)
for _, r := range rigs {
rigBeadsPath := filepath.Join(r.Path, "mayor", "rig")
rigBeads := beads.New(rigBeadsPath)
rigAgentBeads, _ := rigBeads.ListAgentBeads()
if rigAgentBeads == nil {
continue
}
for id, issue := range rigAgentBeads {
allAgentBeads[id] = issue
}
var hookIDs []string
for _, issue := range rigAgentBeads {
// Use the HookBead field from the database column; fall back for legacy beads.
hookID := issue.HookBead
if hookID == "" {
fields := beads.ParseAgentFields(issue.Description)
if fields != nil {
hookID = fields.HookBead
}
}
if hookID != "" {
hookIDs = append(hookIDs, hookID)
}
}
if len(hookIDs) == 0 {
continue
}
hookBeads, _ := rigBeads.ShowMultiple(hookIDs)
for id, issue := range hookBeads {
allHookBeads[id] = issue
}
}
allHookBeads, _ := agentBeads.ShowMultiple(allHookIDs)
if allHookBeads == nil {
allHookBeads = make(map[string]*beads.Issue)
}
// Create mail router for inbox lookups
@@ -336,7 +351,7 @@ func outputStatusText(status TownStatus) error {
icon = roleIcons[agent.Name]
}
fmt.Printf("%s %s\n", icon, style.Bold.Render(capitalizeFirst(agent.Name)))
renderAgentDetails(agent, " ", nil)
renderAgentDetails(agent, " ", nil, status.Location)
fmt.Println()
}
@@ -369,7 +384,7 @@ func outputStatusText(status TownStatus) error {
if len(witnesses) > 0 {
fmt.Printf("%s %s\n", roleIcons["witness"], style.Bold.Render("Witness"))
for _, agent := range witnesses {
renderAgentDetails(agent, " ", r.Hooks)
renderAgentDetails(agent, " ", r.Hooks, status.Location)
}
fmt.Println()
}
@@ -378,7 +393,7 @@ func outputStatusText(status TownStatus) error {
if len(refineries) > 0 {
fmt.Printf("%s %s\n", roleIcons["refinery"], style.Bold.Render("Refinery"))
for _, agent := range refineries {
renderAgentDetails(agent, " ", r.Hooks)
renderAgentDetails(agent, " ", r.Hooks, status.Location)
}
// MQ summary (shown under refinery)
if r.MQ != nil {
@@ -416,7 +431,7 @@ func outputStatusText(status TownStatus) error {
if len(crews) > 0 {
fmt.Printf("%s %s (%d)\n", roleIcons["crew"], style.Bold.Render("Crew"), len(crews))
for _, agent := range crews {
renderAgentDetails(agent, " ", r.Hooks)
renderAgentDetails(agent, " ", r.Hooks, status.Location)
}
fmt.Println()
}
@@ -425,7 +440,7 @@ func outputStatusText(status TownStatus) error {
if len(polecats) > 0 {
fmt.Printf("%s %s (%d)\n", roleIcons["polecat"], style.Bold.Render("Polecats"), len(polecats))
for _, agent := range polecats {
renderAgentDetails(agent, " ", r.Hooks)
renderAgentDetails(agent, " ", r.Hooks, status.Location)
}
fmt.Println()
}
@@ -440,7 +455,7 @@ func outputStatusText(status TownStatus) error {
}
// renderAgentDetails renders full agent bead details
func renderAgentDetails(agent AgentRuntime, indent string, hooks []AgentHookInfo) {
func renderAgentDetails(agent AgentRuntime, indent string, hooks []AgentHookInfo, townRoot string) {
// Line 1: Agent bead ID + status
statusStr := style.Success.Render("running")
if !agent.Running {
@@ -463,15 +478,16 @@ func renderAgentDetails(agent AgentRuntime, indent string, hooks []AgentHookInfo
agentBeadID = beads.AgentBeadID("", parts[0], "")
} else if len(parts) >= 2 {
rig := parts[0]
prefix := beads.GetPrefixForRig(townRoot, rig)
if parts[1] == "crew" && len(parts) >= 3 {
agentBeadID = beads.CrewBeadID(rig, parts[2])
agentBeadID = beads.CrewBeadIDWithPrefix(prefix, rig, parts[2])
} else if parts[1] == "witness" {
agentBeadID = beads.WitnessBeadID(rig)
agentBeadID = beads.WitnessBeadIDWithPrefix(prefix, rig)
} else if parts[1] == "refinery" {
agentBeadID = beads.RefineryBeadID(rig)
agentBeadID = beads.RefineryBeadIDWithPrefix(prefix, rig)
} else if len(parts) == 2 {
// polecat: rig/name
agentBeadID = beads.PolecatBeadID(rig, parts[1])
agentBeadID = beads.PolecatBeadIDWithPrefix(prefix, rig, parts[1])
}
}
}
@@ -695,6 +711,8 @@ type agentDef struct {
func discoverRigAgents(allSessions map[string]bool, r *rig.Rig, crews []string, allAgentBeads map[string]*beads.Issue, allHookBeads map[string]*beads.Issue, mailRouter *mail.Router, skipMail bool) []AgentRuntime {
// Build list of all agents to discover
var defs []agentDef
townRoot := filepath.Dir(r.Path)
prefix := beads.GetPrefixForRig(townRoot, r.Name)
// Witness
if r.HasWitness {
@@ -703,7 +721,7 @@ func discoverRigAgents(allSessions map[string]bool, r *rig.Rig, crews []string,
address: r.Name + "/witness",
session: witnessSessionName(r.Name),
role: "witness",
beadID: beads.WitnessBeadID(r.Name),
beadID: beads.WitnessBeadIDWithPrefix(prefix, r.Name),
})
}
@@ -714,7 +732,7 @@ func discoverRigAgents(allSessions map[string]bool, r *rig.Rig, crews []string,
address: r.Name + "/refinery",
session: fmt.Sprintf("gt-%s-refinery", r.Name),
role: "refinery",
beadID: beads.RefineryBeadID(r.Name),
beadID: beads.RefineryBeadIDWithPrefix(prefix, r.Name),
})
}
@@ -725,7 +743,7 @@ func discoverRigAgents(allSessions map[string]bool, r *rig.Rig, crews []string,
address: r.Name + "/" + name,
session: fmt.Sprintf("gt-%s-%s", r.Name, name),
role: "polecat",
beadID: beads.PolecatBeadID(r.Name, name),
beadID: beads.PolecatBeadIDWithPrefix(prefix, r.Name, name),
})
}
@@ -736,7 +754,7 @@ func discoverRigAgents(allSessions map[string]bool, r *rig.Rig, crews []string,
address: r.Name + "/crew/" + name,
session: crewSessionName(r.Name, name),
role: "crew",
beadID: beads.CrewBeadID(r.Name, name),
beadID: beads.CrewBeadIDWithPrefix(prefix, r.Name, name),
})
}

View File

@@ -0,0 +1,97 @@
package cmd
import (
"bytes"
"io"
"os"
"path/filepath"
"strings"
"testing"
"github.com/steveyegge/gastown/internal/beads"
"github.com/steveyegge/gastown/internal/rig"
)
func captureStdout(t *testing.T, fn func()) string {
t.Helper()
old := os.Stdout
r, w, err := os.Pipe()
if err != nil {
t.Fatalf("create pipe: %v", err)
}
os.Stdout = w
fn()
_ = w.Close()
os.Stdout = old
var buf bytes.Buffer
if _, err := io.Copy(&buf, r); err != nil {
t.Fatalf("read stdout: %v", err)
}
_ = r.Close()
return buf.String()
}
func TestDiscoverRigAgents_UsesRigPrefix(t *testing.T) {
townRoot := t.TempDir()
writeTestRoutes(t, townRoot, []beads.Route{
{Prefix: "bd-", Path: "beads/mayor/rig"},
})
r := &rig.Rig{
Name: "beads",
Path: filepath.Join(townRoot, "beads"),
HasWitness: true,
}
allAgentBeads := map[string]*beads.Issue{
"bd-beads-witness": {
ID: "bd-beads-witness",
AgentState: "running",
HookBead: "bd-hook",
},
}
allHookBeads := map[string]*beads.Issue{
"bd-hook": {ID: "bd-hook", Title: "Pinned"},
}
agents := discoverRigAgents(map[string]bool{}, r, nil, allAgentBeads, allHookBeads, nil, true)
if len(agents) != 1 {
t.Fatalf("discoverRigAgents() returned %d agents, want 1", len(agents))
}
if agents[0].State != "running" {
t.Fatalf("agent state = %q, want %q", agents[0].State, "running")
}
if !agents[0].HasWork {
t.Fatalf("agent HasWork = false, want true")
}
if agents[0].WorkTitle != "Pinned" {
t.Fatalf("agent WorkTitle = %q, want %q", agents[0].WorkTitle, "Pinned")
}
}
func TestRenderAgentDetails_UsesRigPrefix(t *testing.T) {
townRoot := t.TempDir()
writeTestRoutes(t, townRoot, []beads.Route{
{Prefix: "bd-", Path: "beads/mayor/rig"},
})
agent := AgentRuntime{
Name: "witness",
Address: "beads/witness",
Role: "witness",
Running: true,
}
output := captureStdout(t, func() {
renderAgentDetails(agent, "", nil, townRoot)
})
if !strings.Contains(output, "bd-beads-witness") {
t.Fatalf("output %q does not contain rig-prefixed bead ID", output)
}
}