fix(sling): resolve rig path for cross-rig bead hooking

gt sling failed when hooking rig-level beads from town root because
bd update doesn't support cross-database routing like bd show does.

The fix adds a ResolveHookDir helper that:
1. Extracts the prefix from bead ID (e.g., "ap-xxx" → "ap-")
2. Looks up the rig path from routes.jsonl
3. Falls back to townRoot if prefix not found

Also removes the BEADS_DIR environment override which was preventing
routing from working correctly.

Fixes #148
This commit is contained in:
mayor
2026-01-05 08:32:42 -05:00
committed by Steve Yegge
parent 201ef3a9c8
commit 9cb14cc41a
4 changed files with 270 additions and 27 deletions

View File

@@ -443,6 +443,73 @@ func TestBeadsRemoveRoute(t *testing.T) {
}
}
// TestSlingCrossRigRoutingResolution verifies that sling can resolve rig paths
// for cross-rig bead hooking using ExtractPrefix and GetRigPathForPrefix.
// This is the fix for https://github.com/steveyegge/gastown/issues/148
func TestSlingCrossRigRoutingResolution(t *testing.T) {
townRoot := setupRoutingTestTown(t)
tests := []struct {
beadID string
expectedPath string // Relative to townRoot, or "." for town-level
}{
{"gt-mol-abc", "gastown/mayor/rig"},
{"tr-task-xyz", "testrig/mayor/rig"},
{"hq-cv-123", "."}, // Town-level beads
}
for _, tc := range tests {
t.Run(tc.beadID, func(t *testing.T) {
// Step 1: Extract prefix from bead ID
prefix := beads.ExtractPrefix(tc.beadID)
if prefix == "" {
t.Fatalf("ExtractPrefix(%q) returned empty", tc.beadID)
}
// Step 2: Resolve rig path from prefix
rigPath := beads.GetRigPathForPrefix(townRoot, prefix)
if rigPath == "" {
t.Fatalf("GetRigPathForPrefix(%q, %q) returned empty", townRoot, prefix)
}
// Step 3: Verify the path is correct
var expectedFull string
if tc.expectedPath == "." {
expectedFull = townRoot
} else {
expectedFull = filepath.Join(townRoot, tc.expectedPath)
}
if rigPath != expectedFull {
t.Errorf("GetRigPathForPrefix resolved to %q, want %q", rigPath, expectedFull)
}
// Step 4: Verify the .beads directory exists at that path
beadsDir := filepath.Join(rigPath, ".beads")
if _, err := os.Stat(beadsDir); os.IsNotExist(err) {
t.Errorf(".beads directory doesn't exist at resolved path: %s", beadsDir)
}
})
}
}
// TestSlingCrossRigUnknownPrefix verifies behavior for unknown prefixes.
func TestSlingCrossRigUnknownPrefix(t *testing.T) {
townRoot := setupRoutingTestTown(t)
// An unknown prefix should return empty string
unknownBeadID := "xx-unknown-123"
prefix := beads.ExtractPrefix(unknownBeadID)
if prefix != "xx-" {
t.Fatalf("ExtractPrefix(%q) = %q, want %q", unknownBeadID, prefix, "xx-")
}
rigPath := beads.GetRigPathForPrefix(townRoot, prefix)
if rigPath != "" {
t.Errorf("GetRigPathForPrefix for unknown prefix returned %q, want empty", rigPath)
}
}
// TestBeadsGetPrefixForRig verifies prefix lookup by rig name.
func TestBeadsGetPrefixForRig(t *testing.T) {
tmpDir := t.TempDir()

View File

@@ -406,20 +406,10 @@ func runSling(cmd *cobra.Command, args []string) error {
beadID = wispRootID
}
// Hook the bead using bd update
// For town-level beads (hq-*), set BEADS_DIR to town beads
// For rig-level beads (gt-*, bd-*, etc.), use redirect-based routing from hookWorkDir
// Hook the bead using bd update.
// See: https://github.com/steveyegge/gastown/issues/148
hookCmd := exec.Command("bd", "--no-daemon", "update", beadID, "--status=hooked", "--assignee="+targetAgent)
if strings.HasPrefix(beadID, "hq-") {
// Town-level bead: set BEADS_DIR explicitly
hookCmd.Env = append(os.Environ(), "BEADS_DIR="+townBeadsDir)
hookCmd.Dir = townRoot
} else if hookWorkDir != "" {
// Rig-level bead: use redirect from polecat's worktree
hookCmd.Dir = hookWorkDir
} else {
hookCmd.Dir = townRoot
}
hookCmd.Dir = beads.ResolveHookDir(townRoot, beadID, hookWorkDir)
hookCmd.Stderr = os.Stderr
if err := hookCmd.Run(); err != nil {
return fmt.Errorf("hooking bead: %w", err)
@@ -939,11 +929,10 @@ func runSlingFormula(args []string) error {
fmt.Printf("%s Wisp created: %s\n", style.Bold.Render("✓"), wispResult.RootID)
// Step 3: Hook the wisp bead using bd update (discovery-based approach)
// Set BEADS_DIR to town-level beads so hq-* beads are accessible
// Step 3: Hook the wisp bead using bd update.
// See: https://github.com/steveyegge/gastown/issues/148
hookCmd := exec.Command("bd", "--no-daemon", "update", wispResult.RootID, "--status=hooked", "--assignee="+targetAgent)
hookCmd.Env = append(os.Environ(), "BEADS_DIR="+townBeadsDir)
hookCmd.Dir = townRoot
hookCmd.Dir = beads.ResolveHookDir(townRoot, wispResult.RootID, "")
hookCmd.Stderr = os.Stderr
if err := hookCmd.Run(); err != nil {
return fmt.Errorf("hooking wisp bead: %w", err)
@@ -1432,17 +1421,10 @@ func runBatchSling(beadIDs []string, rigName string, townBeadsDir string) error
}
}
// Hook the bead
// For town-level beads (hq-*), set BEADS_DIR; for rig-level beads use redirect
// Hook the bead. See: https://github.com/steveyegge/gastown/issues/148
townRoot := filepath.Dir(townBeadsDir)
hookCmd := exec.Command("bd", "--no-daemon", "update", beadID, "--status=hooked", "--assignee="+targetAgent)
if strings.HasPrefix(beadID, "hq-") {
// Town-level bead: set BEADS_DIR and run from town root (parent of townBeadsDir)
hookCmd.Env = append(os.Environ(), "BEADS_DIR="+townBeadsDir)
hookCmd.Dir = filepath.Dir(townBeadsDir)
} else if hookWorkDir != "" {
// Rig-level bead: use redirect from polecat's worktree
hookCmd.Dir = hookWorkDir
}
hookCmd.Dir = beads.ResolveHookDir(townRoot, beadID, hookWorkDir)
hookCmd.Stderr = os.Stderr
if err := hookCmd.Run(); err != nil {
results = append(results, slingResult{beadID: beadID, polecat: spawnInfo.PolecatName, success: false, errMsg: "hook failed"})