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:
@@ -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{
|
||||
|
||||
@@ -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:
|
||||
|
||||
97
internal/cmd/prime_test.go
Normal file
97
internal/cmd/prime_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
97
internal/cmd/status_test.go
Normal file
97
internal/cmd/status_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user