From e8d27e7212ffeec1b6894a771cc286a36c5deb02 Mon Sep 17 00:00:00 2001 From: gastown/crew/jack Date: Mon, 5 Jan 2026 19:39:42 -0800 Subject: [PATCH] fix: Create and lookup rig agent beads with correct prefix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 #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 #183. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Johann Taberlet Co-Authored-By: Claude Opus 4.5 --- internal/cmd/molecule_attach.go | 2 +- internal/cmd/molecule_status.go | 31 +++++++++++++--------- internal/cmd/sling.go | 46 +++++++++++++++++---------------- internal/cmd/unsling.go | 2 +- internal/config/loader.go | 24 +++++++++++++++++ internal/daemon/daemon.go | 6 +++-- internal/daemon/lifecycle.go | 23 ++++++++++++----- internal/rig/manager.go | 21 ++++++++------- 8 files changed, 100 insertions(+), 55 deletions(-) diff --git a/internal/cmd/molecule_attach.go b/internal/cmd/molecule_attach.go index d4b5e105..2b2058bb 100644 --- a/internal/cmd/molecule_attach.go +++ b/internal/cmd/molecule_attach.go @@ -87,7 +87,7 @@ func detectAgentBeadID() (string, error) { return "", fmt.Errorf("cannot determine agent identity (role: %s)", roleCtx.Role) } - beadID := buildAgentBeadID(identity, roleCtx.Role) + beadID := buildAgentBeadID(identity, roleCtx.Role, townRoot) if beadID == "" { return "", fmt.Errorf("cannot build agent bead ID for identity: %s", identity) } diff --git a/internal/cmd/molecule_status.go b/internal/cmd/molecule_status.go index 67333183..9faa5f4b 100644 --- a/internal/cmd/molecule_status.go +++ b/internal/cmd/molecule_status.go @@ -10,6 +10,7 @@ import ( "github.com/spf13/cobra" "github.com/steveyegge/gastown/internal/beads" + "github.com/steveyegge/gastown/internal/config" "github.com/steveyegge/gastown/internal/style" "github.com/steveyegge/gastown/internal/workspace" ) @@ -28,9 +29,15 @@ import ( // - "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 { +// townRoot is needed to look up the rig's configured prefix. +func buildAgentBeadID(identity string, role Role, townRoot string) string { parts := strings.Split(identity, "/") + // Helper to get prefix for a rig + getPrefix := func(rig string) string { + return config.GetRigPrefix(townRoot, rig) + } + // If role is unknown or empty, try to infer from identity if role == RoleUnknown || role == Role("") { switch { @@ -39,18 +46,18 @@ func buildAgentBeadID(identity string, role Role) string { case identity == "deacon": return beads.DeaconBeadIDTown() case len(parts) == 2 && parts[1] == "witness": - return beads.WitnessBeadID(parts[0]) + return beads.WitnessBeadIDWithPrefix(getPrefix(parts[0]), parts[0]) case len(parts) == 2 && parts[1] == "refinery": - return beads.RefineryBeadID(parts[0]) + return beads.RefineryBeadIDWithPrefix(getPrefix(parts[0]), parts[0]) case len(parts) == 2: // Assume rig/name is a polecat - return beads.PolecatBeadID(parts[0], parts[1]) + return beads.PolecatBeadIDWithPrefix(getPrefix(parts[0]), parts[0], parts[1]) case len(parts) == 3 && parts[1] == "crew": // rig/crew/name - crew member - return beads.CrewBeadID(parts[0], parts[2]) + return beads.CrewBeadIDWithPrefix(getPrefix(parts[0]), parts[0], parts[2]) case len(parts) == 3 && parts[1] == "polecats": // rig/polecats/name - explicit polecat - return beads.PolecatBeadID(parts[0], parts[2]) + return beads.PolecatBeadIDWithPrefix(getPrefix(parts[0]), parts[0], parts[2]) default: return "" } @@ -63,26 +70,26 @@ func buildAgentBeadID(identity string, role Role) string { return beads.DeaconBeadIDTown() case RoleWitness: if len(parts) >= 1 { - return beads.WitnessBeadID(parts[0]) + return beads.WitnessBeadIDWithPrefix(getPrefix(parts[0]), parts[0]) } return "" case RoleRefinery: if len(parts) >= 1 { - return beads.RefineryBeadID(parts[0]) + return beads.RefineryBeadIDWithPrefix(getPrefix(parts[0]), parts[0]) } return "" case RolePolecat: // Handle both 2-part (rig/name) and 3-part (rig/polecats/name) formats if len(parts) == 3 && parts[1] == "polecats" { - return beads.PolecatBeadID(parts[0], parts[2]) + return beads.PolecatBeadIDWithPrefix(getPrefix(parts[0]), parts[0], parts[2]) } if len(parts) >= 2 { - return beads.PolecatBeadID(parts[0], parts[1]) + return beads.PolecatBeadIDWithPrefix(getPrefix(parts[0]), parts[0], parts[1]) } return "" case RoleCrew: if len(parts) >= 3 && parts[1] == "crew" { - return beads.CrewBeadID(parts[0], parts[2]) + return beads.CrewBeadIDWithPrefix(getPrefix(parts[0]), parts[0], parts[2]) } return "" default: @@ -318,7 +325,7 @@ func runMoleculeStatus(cmd *cobra.Command, args []string) error { // Try to find agent bead and read hook slot // This is the preferred method - agent beads have a hook_bead field - agentBeadID := buildAgentBeadID(target, roleCtx.Role) + agentBeadID := buildAgentBeadID(target, roleCtx.Role, townRoot) var hookBead *beads.Issue if agentBeadID != "" { diff --git a/internal/cmd/sling.go b/internal/cmd/sling.go index 32760531..c8ec0de4 100644 --- a/internal/cmd/sling.go +++ b/internal/cmd/sling.go @@ -949,31 +949,31 @@ func runSlingFormula(args []string) error { func updateAgentHookBead(agentID, beadID, workDir, townBeadsDir string) { _ = townBeadsDir // Not used - BEADS_DIR breaks redirect mechanism - // Convert agent ID to agent bead ID - // Format examples (canonical: prefix-rig-role-name): - // greenplace/crew/max -> gt-greenplace-crew-max - // greenplace/polecats/Toast -> gt-greenplace-polecat-Toast - // mayor -> gt-mayor - // greenplace/witness -> gt-greenplace-witness - agentBeadID := agentIDToBeadID(agentID) - if agentBeadID == "" { - return - } - // Determine the directory to run bd commands from: // - If workDir is provided (polecat's clone path), use it for redirect-based routing // - Otherwise fall back to town root bdWorkDir := workDir + townRoot, err := workspace.FindFromCwd() + if err != nil { + // Not in a Gas Town workspace - can't update agent bead + fmt.Fprintf(os.Stderr, "Warning: couldn't find town root to update agent hook: %v\n", err) + return + } if bdWorkDir == "" { - townRoot, err := workspace.FindFromCwd() - if err != nil { - // Not in a Gas Town workspace - can't update agent bead - fmt.Fprintf(os.Stderr, "Warning: couldn't find town root to update agent hook: %v\n", err) - return - } bdWorkDir = townRoot } + // Convert agent ID to agent bead ID + // Format examples (canonical: prefix-rig-role-name): + // greenplace/crew/max -> gt-greenplace-crew-max + // greenplace/polecats/Toast -> gt-greenplace-polecat-Toast + // mayor -> hq-mayor + // greenplace/witness -> gt-greenplace-witness + agentBeadID := agentIDToBeadID(agentID, townRoot) + if agentBeadID == "" { + return + } + // Run from workDir WITHOUT BEADS_DIR to enable redirect-based routing. // Update agent_state to "running" and set hook_bead to the slung work. // For same-database beads, the hook slot is set via `bd slot set`. @@ -1016,7 +1016,8 @@ func detectActor() string { // Uses canonical naming: prefix-rig-role-name // Town-level agents (Mayor, Deacon) use hq- prefix and are stored in town beads. // Rig-level agents use the rig's configured prefix (default "gt-"). -func agentIDToBeadID(agentID string) string { +// townRoot is needed to look up the rig's configured prefix. +func agentIDToBeadID(agentID, townRoot string) string { // Handle simple cases (town-level agents with hq- prefix) if agentID == "mayor" { return beads.MayorBeadIDTown() @@ -1032,16 +1033,17 @@ func agentIDToBeadID(agentID string) string { } rig := parts[0] + prefix := config.GetRigPrefix(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 "" } diff --git a/internal/cmd/unsling.go b/internal/cmd/unsling.go index 557504ac..ade34fb3 100644 --- a/internal/cmd/unsling.go +++ b/internal/cmd/unsling.go @@ -106,7 +106,7 @@ func runUnsling(cmd *cobra.Command, args []string) error { b := beads.New(beadsPath) // Convert agent ID to agent bead ID and look up the agent bead - agentBeadID := agentIDToBeadID(agentID) + agentBeadID := agentIDToBeadID(agentID, townRoot) if agentBeadID == "" { return fmt.Errorf("could not convert agent ID %s to bead ID", agentID) } diff --git a/internal/config/loader.go b/internal/config/loader.go index 40bae010..986f7a0c 100644 --- a/internal/config/loader.go +++ b/internal/config/loader.go @@ -947,3 +947,27 @@ func BuildCrewStartupCommand(rigName, crewName, rigPath, prompt string) string { } return BuildStartupCommand(envVars, rigPath, prompt) } + +// GetRigPrefix returns the beads prefix for a rig from rigs.json. +// Falls back to "gt" if the rig isn't found or has no prefix configured. +// townRoot is the path to the town directory (e.g., ~/gt). +func GetRigPrefix(townRoot, rigName string) string { + rigsConfigPath := filepath.Join(townRoot, "mayor", "rigs.json") + rigsConfig, err := LoadRigsConfig(rigsConfigPath) + if err != nil { + return "gt" // fallback + } + + entry, ok := rigsConfig.Rigs[rigName] + if !ok { + return "gt" // fallback + } + + if entry.BeadsConfig == nil || entry.BeadsConfig.Prefix == "" { + return "gt" // fallback + } + + // Strip trailing hyphen if present (prefix stored as "gt-" but used as "gt") + prefix := entry.BeadsConfig.Prefix + return strings.TrimSuffix(prefix, "-") +} diff --git a/internal/daemon/daemon.go b/internal/daemon/daemon.go index 00a2536d..fae692c4 100644 --- a/internal/daemon/daemon.go +++ b/internal/daemon/daemon.go @@ -417,7 +417,8 @@ func (d *Daemon) ensureWitnessesRunning() { // ensureWitnessRunning ensures the witness for a specific rig is running. func (d *Daemon) ensureWitnessRunning(rigName string) { - agentID := beads.WitnessBeadID(rigName) + prefix := config.GetRigPrefix(d.config.TownRoot, rigName) + agentID := beads.WitnessBeadIDWithPrefix(prefix, rigName) sessionName := "gt-" + rigName + "-witness" // Check agent bead state (ZFC: trust what agent reports) @@ -503,7 +504,8 @@ func (d *Daemon) ensureRefineriesRunning() { // ensureRefineryRunning ensures the refinery for a specific rig is running. func (d *Daemon) ensureRefineryRunning(rigName string) { - agentID := beads.RefineryBeadID(rigName) + prefix := config.GetRigPrefix(d.config.TownRoot, rigName) + agentID := beads.RefineryBeadIDWithPrefix(prefix, rigName) sessionName := "gt-" + rigName + "-refinery" // Check agent bead state (ZFC: trust what agent reports) diff --git a/internal/daemon/lifecycle.go b/internal/daemon/lifecycle.go index 6fcd634a..39c0b25f 100644 --- a/internal/daemon/lifecycle.go +++ b/internal/daemon/lifecycle.go @@ -626,13 +626,17 @@ func (d *Daemon) identityToAgentBeadID(identity string) string { case "mayor": return beads.MayorBeadIDTown() case "witness": - return beads.WitnessBeadID(parsed.RigName) + prefix := config.GetRigPrefix(d.config.TownRoot, parsed.RigName) + return beads.WitnessBeadIDWithPrefix(prefix, parsed.RigName) case "refinery": - return beads.RefineryBeadID(parsed.RigName) + prefix := config.GetRigPrefix(d.config.TownRoot, parsed.RigName) + return beads.RefineryBeadIDWithPrefix(prefix, parsed.RigName) case "crew": - return beads.CrewBeadID(parsed.RigName, parsed.AgentName) + prefix := config.GetRigPrefix(d.config.TownRoot, parsed.RigName) + return beads.CrewBeadIDWithPrefix(prefix, parsed.RigName, parsed.AgentName) case "polecat": - return beads.PolecatBeadID(parsed.RigName, parsed.AgentName) + prefix := config.GetRigPrefix(d.config.TownRoot, parsed.RigName) + return beads.PolecatBeadIDWithPrefix(prefix, parsed.RigName, parsed.AgentName) default: return "" } @@ -660,9 +664,14 @@ func (d *Daemon) checkStaleAgents() { d.logger.Printf("Warning: could not load rigs config: %v", err) } else { // Add rig-specific agents (witness, refinery) for each discovered rig - for rigName := range rigsConfig.Rigs { - agentBeadIDs = append(agentBeadIDs, beads.WitnessBeadID(rigName)) - agentBeadIDs = append(agentBeadIDs, beads.RefineryBeadID(rigName)) + for rigName, rigEntry := range rigsConfig.Rigs { + // Get rig prefix from config (defaults to "gt" if not set) + prefix := "gt" + if rigEntry.BeadsConfig != nil && rigEntry.BeadsConfig.Prefix != "" { + prefix = strings.TrimSuffix(rigEntry.BeadsConfig.Prefix, "-") + } + agentBeadIDs = append(agentBeadIDs, beads.WitnessBeadIDWithPrefix(prefix, rigName)) + agentBeadIDs = append(agentBeadIDs, beads.RefineryBeadIDWithPrefix(prefix, rigName)) } } diff --git a/internal/rig/manager.go b/internal/rig/manager.go index 02be1056..1395a223 100644 --- a/internal/rig/manager.go +++ b/internal/rig/manager.go @@ -556,14 +556,15 @@ func (m *Manager) initBeads(rigPath, prefix string) error { // Town-level agents (Mayor, Deacon) are created by gt install in town beads. // Role beads are also created by gt install with hq- prefix. // -// Format: -- (e.g., gt-gastown-witness) +// Rig-level agents (Witness, Refinery) are created here in rig beads with rig prefix. +// Format: -- (e.g., pi-pixelforge-witness) // // Agent beads track lifecycle state for ZFC compliance (gt-h3hak, gt-pinkq). -func (m *Manager) initAgentBeads(_, rigName, _ string) error { // rigPath and prefix unused until Phase 2 - // TEMPORARY (gt-4r1ph): Currently all agent beads go in town beads. - // After Phase 2, only Mayor/Deacon will be here; Witness/Refinery go to rig beads. - townBeadsDir := filepath.Join(m.townRoot, ".beads") - bd := beads.NewWithBeadsDir(m.townRoot, townBeadsDir) +func (m *Manager) initAgentBeads(rigPath, rigName, prefix string) error { + // Rig-level agents go in rig beads with rig prefix (per docs/architecture.md). + // Town-level agents (Mayor, Deacon) are created by gt install in town beads. + rigBeadsDir := filepath.Join(rigPath, ".beads") + bd := beads.NewWithBeadsDir(rigPath, rigBeadsDir) // Define rig-level agents to create type agentDef struct { @@ -573,17 +574,17 @@ func (m *Manager) initAgentBeads(_, rigName, _ string) error { // rigPath and pr desc string } - // Create rig-specific agents using gt prefix (agents stored in town beads). - // Format: gt-- (e.g., gt-gastown-witness) + // Create rig-specific agents using rig prefix in rig beads. + // Format: -- (e.g., pi-pixelforge-witness) agents := []agentDef{ { - id: beads.WitnessBeadID(rigName), + id: beads.WitnessBeadIDWithPrefix(prefix, rigName), roleType: "witness", rig: rigName, desc: fmt.Sprintf("Witness for %s - monitors polecat health and progress.", rigName), }, { - id: beads.RefineryBeadID(rigName), + id: beads.RefineryBeadIDWithPrefix(prefix, rigName), roleType: "refinery", rig: rigName, desc: fmt.Sprintf("Refinery for %s - processes merge queue.", rigName),