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 #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 <johann.taberlet@gmail.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
gastown/crew/jack
2026-01-05 19:39:42 -08:00
committed by Steve Yegge
parent fc0b506253
commit e8d27e7212
8 changed files with 100 additions and 55 deletions

View File

@@ -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)
}

View File

@@ -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 != "" {

View File

@@ -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 ""
}

View File

@@ -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)
}