diff --git a/internal/beads/routes.go b/internal/beads/routes.go index 320520bb..11e4461d 100644 --- a/internal/beads/routes.go +++ b/internal/beads/routes.go @@ -130,6 +130,30 @@ func WriteRoutes(beadsDir string, routes []Route) error { return nil } +// GetPrefixForRig returns the beads prefix for a given rig name. +// The prefix is returned without the trailing hyphen (e.g., "bd" not "bd-"). +// If the rig is not found in routes, returns "gt" as the default. +// The townRoot should be the Gas Town root directory (e.g., ~/gt). +func GetPrefixForRig(townRoot, rigName string) string { + beadsDir := filepath.Join(townRoot, ".beads") + routes, err := LoadRoutes(beadsDir) + if err != nil || routes == nil { + return "gt" // Default prefix + } + + // Look for a route where the path starts with the rig name + // Routes paths are like "gastown/mayor/rig" or "beads/mayor/rig" + for _, r := range routes { + parts := strings.SplitN(r.Path, "/", 2) + if len(parts) > 0 && parts[0] == rigName { + // Return prefix without trailing hyphen + return strings.TrimSuffix(r.Prefix, "-") + } + } + + return "gt" // Default prefix +} + // FindConflictingPrefixes checks for duplicate prefixes in routes. // Returns a map of prefix -> list of paths that use it. func FindConflictingPrefixes(beadsDir string) (map[string][]string, error) { diff --git a/internal/beads/routes_test.go b/internal/beads/routes_test.go new file mode 100644 index 00000000..06561dc2 --- /dev/null +++ b/internal/beads/routes_test.go @@ -0,0 +1,86 @@ +package beads + +import ( + "os" + "path/filepath" + "testing" +) + +func TestGetPrefixForRig(t *testing.T) { + // Create a temporary directory with routes.jsonl + tmpDir := t.TempDir() + beadsDir := filepath.Join(tmpDir, ".beads") + if err := os.MkdirAll(beadsDir, 0755); err != nil { + t.Fatal(err) + } + + routesContent := `{"prefix": "gt-", "path": "gastown/mayor/rig"} +{"prefix": "bd-", "path": "beads/mayor/rig"} +{"prefix": "hq-", "path": "."} +` + if err := os.WriteFile(filepath.Join(beadsDir, "routes.jsonl"), []byte(routesContent), 0644); err != nil { + t.Fatal(err) + } + + tests := []struct { + rig string + expected string + }{ + {"gastown", "gt"}, + {"beads", "bd"}, + {"unknown", "gt"}, // default + {"", "gt"}, // empty rig -> default + } + + for _, tc := range tests { + t.Run(tc.rig, func(t *testing.T) { + result := GetPrefixForRig(tmpDir, tc.rig) + if result != tc.expected { + t.Errorf("GetPrefixForRig(%q, %q) = %q, want %q", tmpDir, tc.rig, result, tc.expected) + } + }) + } +} + +func TestGetPrefixForRig_NoRoutesFile(t *testing.T) { + tmpDir := t.TempDir() + // No routes.jsonl file + + result := GetPrefixForRig(tmpDir, "anything") + if result != "gt" { + t.Errorf("Expected default 'gt' when no routes file, got %q", result) + } +} + +func TestAgentBeadIDsWithPrefix(t *testing.T) { + tests := []struct { + name string + fn func() string + expected string + }{ + {"PolecatBeadIDWithPrefix bd beads obsidian", + func() string { return PolecatBeadIDWithPrefix("bd", "beads", "obsidian") }, + "bd-beads-polecat-obsidian"}, + {"PolecatBeadIDWithPrefix gt gastown Toast", + func() string { return PolecatBeadIDWithPrefix("gt", "gastown", "Toast") }, + "gt-gastown-polecat-Toast"}, + {"WitnessBeadIDWithPrefix bd beads", + func() string { return WitnessBeadIDWithPrefix("bd", "beads") }, + "bd-beads-witness"}, + {"RefineryBeadIDWithPrefix bd beads", + func() string { return RefineryBeadIDWithPrefix("bd", "beads") }, + "bd-beads-refinery"}, + {"CrewBeadIDWithPrefix bd beads max", + func() string { return CrewBeadIDWithPrefix("bd", "beads", "max") }, + "bd-beads-crew-max"}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + result := tc.fn() + if result != tc.expected { + t.Errorf("got %q, want %q", result, tc.expected) + } + }) + } +} diff --git a/internal/cmd/sling.go b/internal/cmd/sling.go index 874d0526..5f719fcc 100644 --- a/internal/cmd/sling.go +++ b/internal/cmd/sling.go @@ -13,6 +13,7 @@ import ( "github.com/steveyegge/gastown/internal/session" "github.com/steveyegge/gastown/internal/style" "github.com/steveyegge/gastown/internal/tmux" + "github.com/steveyegge/gastown/internal/workspace" ) var slingCmd = &cobra.Command{ @@ -790,8 +791,9 @@ func detectActor() string { // agentIDToBeadID converts an agent ID to its corresponding agent bead ID. // Uses canonical naming: prefix-rig-role-name +// The prefix is looked up from routes.jsonl based on the rig name. func agentIDToBeadID(agentID string) string { - // Handle simple cases + // Handle simple cases (town-level agents always use gt- prefix) if agentID == "mayor" { return beads.MayorBeadID() } @@ -807,15 +809,21 @@ func agentIDToBeadID(agentID string) string { rig := parts[0] + // Look up the prefix for this rig from routes.jsonl + prefix := "gt" // default + if townRoot, err := workspace.FindFromCwd(); err == nil && townRoot != "" { + prefix = beads.GetPrefixForRig(townRoot, rig) + } + switch { case len(parts) == 2 && parts[1] == "witness": - return beads.WitnessBeadID(rig) + return beads.WitnessBeadIDWithPrefix(prefix, rig) case len(parts) == 2 && parts[1] == "refinery": - return beads.RefineryBeadID(rig) + return beads.RefineryBeadIDWithPrefix(prefix, rig) case len(parts) == 3 && parts[1] == "crew": - return beads.CrewBeadID(rig, parts[2]) + return beads.CrewBeadIDWithPrefix(prefix, rig, parts[2]) case len(parts) == 3 && parts[1] == "polecats": - return beads.PolecatBeadID(rig, parts[2]) + return beads.PolecatBeadIDWithPrefix(prefix, rig, parts[2]) default: return "" }