Skip to content

Commit e8d27e7

Browse files
steveyeggejohann-taberletclaude
committed
fix: Create and lookup rig agent beads with correct prefix
Per docs/architecture.md, Witness and Refinery are rig-level agents that should use the rig's configured prefix (e.g., pi- for pixelforge) instead of hardcoded "gt-". This extends PR gastownhall#183's creation fix to also fix all lookup paths: - internal/rig/manager.go: Create agent beads in rig beads with rig prefix - internal/daemon/daemon.go: Use rig prefix when looking up agent state - internal/daemon/lifecycle.go: Use rig prefix for identity-to-bead mapping - internal/cmd/sling.go: Pass townRoot for prefix lookup - internal/cmd/unsling.go: Pass townRoot for prefix lookup - internal/cmd/molecule_status.go: Use rig prefix for agent bead lookups - internal/cmd/molecule_attach.go: Use rig prefix for agent bead lookups - internal/config/loader.go: Add GetRigPrefix helper Without this fix, the daemon would: - Create pi-gastown-witness but look for gt-gastown-witness - Report agents as missing/dead when they are running - Fail to manage agent lifecycle correctly Based on work by Johann Taberlet in PR gastownhall#183. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Johann Taberlet <johann.taberlet@gmail.com> Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent fc0b506 commit e8d27e7

8 files changed

Lines changed: 99 additions & 54 deletions

File tree

internal/cmd/molecule_attach.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ func detectAgentBeadID() (string, error) {
8787
return "", fmt.Errorf("cannot determine agent identity (role: %s)", roleCtx.Role)
8888
}
8989

90-
beadID := buildAgentBeadID(identity, roleCtx.Role)
90+
beadID := buildAgentBeadID(identity, roleCtx.Role, townRoot)
9191
if beadID == "" {
9292
return "", fmt.Errorf("cannot build agent bead ID for identity: %s", identity)
9393
}

internal/cmd/molecule_status.go

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010

1111
"github.com/spf13/cobra"
1212
"github.com/steveyegge/gastown/internal/beads"
13+
"github.com/steveyegge/gastown/internal/config"
1314
"github.com/steveyegge/gastown/internal/style"
1415
"github.com/steveyegge/gastown/internal/workspace"
1516
)
@@ -28,9 +29,15 @@ import (
2829
// - "gastown/crew/max" -> "gt-gastown-crew-max"
2930
//
3031
// If role is unknown, it tries to infer from the identity string.
31-
func buildAgentBeadID(identity string, role Role) string {
32+
// townRoot is needed to look up the rig's configured prefix.
33+
func buildAgentBeadID(identity string, role Role, townRoot string) string {
3234
parts := strings.Split(identity, "/")
3335

36+
// Helper to get prefix for a rig
37+
getPrefix := func(rig string) string {
38+
return config.GetRigPrefix(townRoot, rig)
39+
}
40+
3441
// If role is unknown or empty, try to infer from identity
3542
if role == RoleUnknown || role == Role("") {
3643
switch {
@@ -39,18 +46,18 @@ func buildAgentBeadID(identity string, role Role) string {
3946
case identity == "deacon":
4047
return beads.DeaconBeadIDTown()
4148
case len(parts) == 2 && parts[1] == "witness":
42-
return beads.WitnessBeadID(parts[0])
49+
return beads.WitnessBeadIDWithPrefix(getPrefix(parts[0]), parts[0])
4350
case len(parts) == 2 && parts[1] == "refinery":
44-
return beads.RefineryBeadID(parts[0])
51+
return beads.RefineryBeadIDWithPrefix(getPrefix(parts[0]), parts[0])
4552
case len(parts) == 2:
4653
// Assume rig/name is a polecat
47-
return beads.PolecatBeadID(parts[0], parts[1])
54+
return beads.PolecatBeadIDWithPrefix(getPrefix(parts[0]), parts[0], parts[1])
4855
case len(parts) == 3 && parts[1] == "crew":
4956
// rig/crew/name - crew member
50-
return beads.CrewBeadID(parts[0], parts[2])
57+
return beads.CrewBeadIDWithPrefix(getPrefix(parts[0]), parts[0], parts[2])
5158
case len(parts) == 3 && parts[1] == "polecats":
5259
// rig/polecats/name - explicit polecat
53-
return beads.PolecatBeadID(parts[0], parts[2])
60+
return beads.PolecatBeadIDWithPrefix(getPrefix(parts[0]), parts[0], parts[2])
5461
default:
5562
return ""
5663
}
@@ -63,26 +70,26 @@ func buildAgentBeadID(identity string, role Role) string {
6370
return beads.DeaconBeadIDTown()
6471
case RoleWitness:
6572
if len(parts) >= 1 {
66-
return beads.WitnessBeadID(parts[0])
73+
return beads.WitnessBeadIDWithPrefix(getPrefix(parts[0]), parts[0])
6774
}
6875
return ""
6976
case RoleRefinery:
7077
if len(parts) >= 1 {
71-
return beads.RefineryBeadID(parts[0])
78+
return beads.RefineryBeadIDWithPrefix(getPrefix(parts[0]), parts[0])
7279
}
7380
return ""
7481
case RolePolecat:
7582
// Handle both 2-part (rig/name) and 3-part (rig/polecats/name) formats
7683
if len(parts) == 3 && parts[1] == "polecats" {
77-
return beads.PolecatBeadID(parts[0], parts[2])
84+
return beads.PolecatBeadIDWithPrefix(getPrefix(parts[0]), parts[0], parts[2])
7885
}
7986
if len(parts) >= 2 {
80-
return beads.PolecatBeadID(parts[0], parts[1])
87+
return beads.PolecatBeadIDWithPrefix(getPrefix(parts[0]), parts[0], parts[1])
8188
}
8289
return ""
8390
case RoleCrew:
8491
if len(parts) >= 3 && parts[1] == "crew" {
85-
return beads.CrewBeadID(parts[0], parts[2])
92+
return beads.CrewBeadIDWithPrefix(getPrefix(parts[0]), parts[0], parts[2])
8693
}
8794
return ""
8895
default:
@@ -318,7 +325,7 @@ func runMoleculeStatus(cmd *cobra.Command, args []string) error {
318325

319326
// Try to find agent bead and read hook slot
320327
// This is the preferred method - agent beads have a hook_bead field
321-
agentBeadID := buildAgentBeadID(target, roleCtx.Role)
328+
agentBeadID := buildAgentBeadID(target, roleCtx.Role, townRoot)
322329
var hookBead *beads.Issue
323330

324331
if agentBeadID != "" {

internal/cmd/sling.go

Lines changed: 23 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -949,31 +949,31 @@ func runSlingFormula(args []string) error {
949949
func updateAgentHookBead(agentID, beadID, workDir, townBeadsDir string) {
950950
_ = townBeadsDir // Not used - BEADS_DIR breaks redirect mechanism
951951

952+
// Determine the directory to run bd commands from:
953+
// - If workDir is provided (polecat's clone path), use it for redirect-based routing
954+
// - Otherwise fall back to town root
955+
bdWorkDir := workDir
956+
townRoot, err := workspace.FindFromCwd()
957+
if err != nil {
958+
// Not in a Gas Town workspace - can't update agent bead
959+
fmt.Fprintf(os.Stderr, "Warning: couldn't find town root to update agent hook: %v\n", err)
960+
return
961+
}
962+
if bdWorkDir == "" {
963+
bdWorkDir = townRoot
964+
}
965+
952966
// Convert agent ID to agent bead ID
953967
// Format examples (canonical: prefix-rig-role-name):
954968
// greenplace/crew/max -> gt-greenplace-crew-max
955969
// greenplace/polecats/Toast -> gt-greenplace-polecat-Toast
956-
// mayor -> gt-mayor
970+
// mayor -> hq-mayor
957971
// greenplace/witness -> gt-greenplace-witness
958-
agentBeadID := agentIDToBeadID(agentID)
972+
agentBeadID := agentIDToBeadID(agentID, townRoot)
959973
if agentBeadID == "" {
960974
return
961975
}
962976

963-
// Determine the directory to run bd commands from:
964-
// - If workDir is provided (polecat's clone path), use it for redirect-based routing
965-
// - Otherwise fall back to town root
966-
bdWorkDir := workDir
967-
if bdWorkDir == "" {
968-
townRoot, err := workspace.FindFromCwd()
969-
if err != nil {
970-
// Not in a Gas Town workspace - can't update agent bead
971-
fmt.Fprintf(os.Stderr, "Warning: couldn't find town root to update agent hook: %v\n", err)
972-
return
973-
}
974-
bdWorkDir = townRoot
975-
}
976-
977977
// Run from workDir WITHOUT BEADS_DIR to enable redirect-based routing.
978978
// Update agent_state to "running" and set hook_bead to the slung work.
979979
// For same-database beads, the hook slot is set via `bd slot set`.
@@ -1016,7 +1016,8 @@ func detectActor() string {
10161016
// Uses canonical naming: prefix-rig-role-name
10171017
// Town-level agents (Mayor, Deacon) use hq- prefix and are stored in town beads.
10181018
// Rig-level agents use the rig's configured prefix (default "gt-").
1019-
func agentIDToBeadID(agentID string) string {
1019+
// townRoot is needed to look up the rig's configured prefix.
1020+
func agentIDToBeadID(agentID, townRoot string) string {
10201021
// Handle simple cases (town-level agents with hq- prefix)
10211022
if agentID == "mayor" {
10221023
return beads.MayorBeadIDTown()
@@ -1032,16 +1033,17 @@ func agentIDToBeadID(agentID string) string {
10321033
}
10331034

10341035
rig := parts[0]
1036+
prefix := config.GetRigPrefix(townRoot, rig)
10351037

10361038
switch {
10371039
case len(parts) == 2 && parts[1] == "witness":
1038-
return beads.WitnessBeadID(rig)
1040+
return beads.WitnessBeadIDWithPrefix(prefix, rig)
10391041
case len(parts) == 2 && parts[1] == "refinery":
1040-
return beads.RefineryBeadID(rig)
1042+
return beads.RefineryBeadIDWithPrefix(prefix, rig)
10411043
case len(parts) == 3 && parts[1] == "crew":
1042-
return beads.CrewBeadID(rig, parts[2])
1044+
return beads.CrewBeadIDWithPrefix(prefix, rig, parts[2])
10431045
case len(parts) == 3 && parts[1] == "polecats":
1044-
return beads.PolecatBeadID(rig, parts[2])
1046+
return beads.PolecatBeadIDWithPrefix(prefix, rig, parts[2])
10451047
default:
10461048
return ""
10471049
}

internal/cmd/unsling.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ func runUnsling(cmd *cobra.Command, args []string) error {
106106
b := beads.New(beadsPath)
107107

108108
// Convert agent ID to agent bead ID and look up the agent bead
109-
agentBeadID := agentIDToBeadID(agentID)
109+
agentBeadID := agentIDToBeadID(agentID, townRoot)
110110
if agentBeadID == "" {
111111
return fmt.Errorf("could not convert agent ID %s to bead ID", agentID)
112112
}

internal/config/loader.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -947,3 +947,27 @@ func BuildCrewStartupCommand(rigName, crewName, rigPath, prompt string) string {
947947
}
948948
return BuildStartupCommand(envVars, rigPath, prompt)
949949
}
950+
951+
// GetRigPrefix returns the beads prefix for a rig from rigs.json.
952+
// Falls back to "gt" if the rig isn't found or has no prefix configured.
953+
// townRoot is the path to the town directory (e.g., ~/gt).
954+
func GetRigPrefix(townRoot, rigName string) string {
955+
rigsConfigPath := filepath.Join(townRoot, "mayor", "rigs.json")
956+
rigsConfig, err := LoadRigsConfig(rigsConfigPath)
957+
if err != nil {
958+
return "gt" // fallback
959+
}
960+
961+
entry, ok := rigsConfig.Rigs[rigName]
962+
if !ok {
963+
return "gt" // fallback
964+
}
965+
966+
if entry.BeadsConfig == nil || entry.BeadsConfig.Prefix == "" {
967+
return "gt" // fallback
968+
}
969+
970+
// Strip trailing hyphen if present (prefix stored as "gt-" but used as "gt")
971+
prefix := entry.BeadsConfig.Prefix
972+
return strings.TrimSuffix(prefix, "-")
973+
}

internal/daemon/daemon.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -417,7 +417,8 @@ func (d *Daemon) ensureWitnessesRunning() {
417417

418418
// ensureWitnessRunning ensures the witness for a specific rig is running.
419419
func (d *Daemon) ensureWitnessRunning(rigName string) {
420-
agentID := beads.WitnessBeadID(rigName)
420+
prefix := config.GetRigPrefix(d.config.TownRoot, rigName)
421+
agentID := beads.WitnessBeadIDWithPrefix(prefix, rigName)
421422
sessionName := "gt-" + rigName + "-witness"
422423

423424
// Check agent bead state (ZFC: trust what agent reports)
@@ -503,7 +504,8 @@ func (d *Daemon) ensureRefineriesRunning() {
503504

504505
// ensureRefineryRunning ensures the refinery for a specific rig is running.
505506
func (d *Daemon) ensureRefineryRunning(rigName string) {
506-
agentID := beads.RefineryBeadID(rigName)
507+
prefix := config.GetRigPrefix(d.config.TownRoot, rigName)
508+
agentID := beads.RefineryBeadIDWithPrefix(prefix, rigName)
507509
sessionName := "gt-" + rigName + "-refinery"
508510

509511
// Check agent bead state (ZFC: trust what agent reports)

internal/daemon/lifecycle.go

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -626,13 +626,17 @@ func (d *Daemon) identityToAgentBeadID(identity string) string {
626626
case "mayor":
627627
return beads.MayorBeadIDTown()
628628
case "witness":
629-
return beads.WitnessBeadID(parsed.RigName)
629+
prefix := config.GetRigPrefix(d.config.TownRoot, parsed.RigName)
630+
return beads.WitnessBeadIDWithPrefix(prefix, parsed.RigName)
630631
case "refinery":
631-
return beads.RefineryBeadID(parsed.RigName)
632+
prefix := config.GetRigPrefix(d.config.TownRoot, parsed.RigName)
633+
return beads.RefineryBeadIDWithPrefix(prefix, parsed.RigName)
632634
case "crew":
633-
return beads.CrewBeadID(parsed.RigName, parsed.AgentName)
635+
prefix := config.GetRigPrefix(d.config.TownRoot, parsed.RigName)
636+
return beads.CrewBeadIDWithPrefix(prefix, parsed.RigName, parsed.AgentName)
634637
case "polecat":
635-
return beads.PolecatBeadID(parsed.RigName, parsed.AgentName)
638+
prefix := config.GetRigPrefix(d.config.TownRoot, parsed.RigName)
639+
return beads.PolecatBeadIDWithPrefix(prefix, parsed.RigName, parsed.AgentName)
636640
default:
637641
return ""
638642
}
@@ -660,9 +664,14 @@ func (d *Daemon) checkStaleAgents() {
660664
d.logger.Printf("Warning: could not load rigs config: %v", err)
661665
} else {
662666
// Add rig-specific agents (witness, refinery) for each discovered rig
663-
for rigName := range rigsConfig.Rigs {
664-
agentBeadIDs = append(agentBeadIDs, beads.WitnessBeadID(rigName))
665-
agentBeadIDs = append(agentBeadIDs, beads.RefineryBeadID(rigName))
667+
for rigName, rigEntry := range rigsConfig.Rigs {
668+
// Get rig prefix from config (defaults to "gt" if not set)
669+
prefix := "gt"
670+
if rigEntry.BeadsConfig != nil && rigEntry.BeadsConfig.Prefix != "" {
671+
prefix = strings.TrimSuffix(rigEntry.BeadsConfig.Prefix, "-")
672+
}
673+
agentBeadIDs = append(agentBeadIDs, beads.WitnessBeadIDWithPrefix(prefix, rigName))
674+
agentBeadIDs = append(agentBeadIDs, beads.RefineryBeadIDWithPrefix(prefix, rigName))
666675
}
667676
}
668677

internal/rig/manager.go

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -556,14 +556,15 @@ func (m *Manager) initBeads(rigPath, prefix string) error {
556556
// Town-level agents (Mayor, Deacon) are created by gt install in town beads.
557557
// Role beads are also created by gt install with hq- prefix.
558558
//
559-
// Format: <prefix>-<rig>-<role> (e.g., gt-gastown-witness)
559+
// Rig-level agents (Witness, Refinery) are created here in rig beads with rig prefix.
560+
// Format: <prefix>-<rig>-<role> (e.g., pi-pixelforge-witness)
560561
//
561562
// Agent beads track lifecycle state for ZFC compliance (gt-h3hak, gt-pinkq).
562-
func (m *Manager) initAgentBeads(_, rigName, _ string) error { // rigPath and prefix unused until Phase 2
563-
// TEMPORARY (gt-4r1ph): Currently all agent beads go in town beads.
564-
// After Phase 2, only Mayor/Deacon will be here; Witness/Refinery go to rig beads.
565-
townBeadsDir := filepath.Join(m.townRoot, ".beads")
566-
bd := beads.NewWithBeadsDir(m.townRoot, townBeadsDir)
563+
func (m *Manager) initAgentBeads(rigPath, rigName, prefix string) error {
564+
// Rig-level agents go in rig beads with rig prefix (per docs/architecture.md).
565+
// Town-level agents (Mayor, Deacon) are created by gt install in town beads.
566+
rigBeadsDir := filepath.Join(rigPath, ".beads")
567+
bd := beads.NewWithBeadsDir(rigPath, rigBeadsDir)
567568

568569
// Define rig-level agents to create
569570
type agentDef struct {
@@ -573,17 +574,17 @@ func (m *Manager) initAgentBeads(_, rigName, _ string) error { // rigPath and pr
573574
desc string
574575
}
575576

576-
// Create rig-specific agents using gt prefix (agents stored in town beads).
577-
// Format: gt-<rig>-<role> (e.g., gt-gastown-witness)
577+
// Create rig-specific agents using rig prefix in rig beads.
578+
// Format: <prefix>-<rig>-<role> (e.g., pi-pixelforge-witness)
578579
agents := []agentDef{
579580
{
580-
id: beads.WitnessBeadID(rigName),
581+
id: beads.WitnessBeadIDWithPrefix(prefix, rigName),
581582
roleType: "witness",
582583
rig: rigName,
583584
desc: fmt.Sprintf("Witness for %s - monitors polecat health and progress.", rigName),
584585
},
585586
{
586-
id: beads.RefineryBeadID(rigName),
587+
id: beads.RefineryBeadIDWithPrefix(prefix, rigName),
587588
roleType: "refinery",
588589
rig: rigName,
589590
desc: fmt.Sprintf("Refinery for %s - processes merge queue.", rigName),

0 commit comments

Comments
 (0)