From f4cbcb4ce948db392134913edd6359f12298a475 Mon Sep 17 00:00:00 2001 From: max Date: Wed, 7 Jan 2026 22:09:53 -0800 Subject: [PATCH] fix: SetupRedirect now works with tracked beads architecture The SetupRedirect function was failing for rigs that use the tracked beads architecture where the canonical beads location is mayor/rig/.beads and there is no rig-level .beads directory. This fix now checks for both locations: 1. rig/.beads (with optional redirect to mayor/rig/.beads) 2. mayor/rig/.beads directly (if no rig/.beads exists) This ensures crew and polecat workspaces get the correct redirect file pointing to the shared beads database in all configurations. Closes: gt-jy77g Co-Authored-By: Claude Opus 4.5 --- internal/beads/beads.go | 69 +++++++++++++++++++++++------------- internal/beads/beads_test.go | 42 +++++++++++++++++++++- 2 files changed, 85 insertions(+), 26 deletions(-) diff --git a/internal/beads/beads.go b/internal/beads/beads.go index 843e2626..b809ee49 100644 --- a/internal/beads/beads.go +++ b/internal/beads/beads.go @@ -161,9 +161,10 @@ func cleanBeadsRuntimeFiles(beadsDir string) { // - worktreePath: the worktree directory (e.g., /crew/ or /refinery/rig) // // The function: -// 1. Computes the relative path from worktree to rig-level .beads -// 2. Cleans up runtime files (preserving tracked files like formulas/) -// 3. Creates the redirect file +// 1. Finds the canonical beads location (rig/.beads or mayor/rig/.beads) +// 2. Computes the relative path from worktree to that location +// 3. Cleans up runtime files (preserving tracked files like formulas/) +// 4. Creates the redirect file // // Safety: This function refuses to create redirects in the canonical beads location // (mayor/rig) to prevent circular redirect chains. @@ -186,10 +187,47 @@ func SetupRedirect(townRoot, worktreePath string) error { } rigRoot := filepath.Join(townRoot, parts[0]) - rigBeadsPath := filepath.Join(rigRoot, ".beads") - if _, err := os.Stat(rigBeadsPath); os.IsNotExist(err) { - return fmt.Errorf("no rig .beads found at %s", rigBeadsPath) + // Find the canonical beads location. In order of preference: + // 1. rig/.beads (if it exists and has content or a redirect) + // 2. mayor/rig/.beads (tracked beads architecture) + // + // The tracked beads architecture stores the actual database in mayor/rig/.beads + // and may not have a rig/.beads directory at all. + rigBeadsPath := filepath.Join(rigRoot, ".beads") + mayorBeadsPath := filepath.Join(rigRoot, "mayor", "rig", ".beads") + + // Compute depth for relative paths + // e.g., crew/ (depth 2) -> ../../ + // refinery/rig (depth 2) -> ../../ + depth := len(parts) - 1 // subtract 1 for rig name itself + upPath := strings.Repeat("../", depth) + + var redirectPath string + + // Check if rig-level .beads exists + if _, err := os.Stat(rigBeadsPath); err == nil { + // rig/.beads exists - check if it has a redirect to follow + rigRedirectPath := filepath.Join(rigBeadsPath, "redirect") + if data, err := os.ReadFile(rigRedirectPath); err == nil { + rigRedirectTarget := strings.TrimSpace(string(data)) + if rigRedirectTarget != "" { + // Rig has redirect (e.g., "mayor/rig/.beads" for tracked beads). + // Redirect worktree directly to the final destination. + redirectPath = upPath + rigRedirectTarget + } + } + // If no redirect in rig/.beads, point directly to rig/.beads + if redirectPath == "" { + redirectPath = upPath + ".beads" + } + } else if _, err := os.Stat(mayorBeadsPath); err == nil { + // No rig/.beads but mayor/rig/.beads exists (tracked beads architecture). + // Point directly to mayor/rig/.beads. + redirectPath = upPath + "mayor/rig/.beads" + } else { + // Neither location exists - this is an error + return fmt.Errorf("no beads found at %s or %s", rigBeadsPath, mayorBeadsPath) } // Clean up runtime files in .beads/ but preserve tracked files (formulas/, README.md, etc.) @@ -201,25 +239,6 @@ func SetupRedirect(townRoot, worktreePath string) error { return fmt.Errorf("creating .beads dir: %w", err) } - // Compute relative path from worktree to rig root - // e.g., crew/ (depth 2) -> ../../.beads - // refinery/rig (depth 2) -> ../../.beads - depth := len(parts) - 1 // subtract 1 for rig name itself - redirectPath := strings.Repeat("../", depth) + ".beads" - - // Check if rig-level beads has a redirect (tracked beads case). - // If so, redirect directly to the final destination to avoid chains. - // The bd CLI doesn't support redirect chains, so we must skip intermediate hops. - rigRedirectPath := filepath.Join(rigBeadsPath, "redirect") - if data, err := os.ReadFile(rigRedirectPath); err == nil { - rigRedirectTarget := strings.TrimSpace(string(data)) - if rigRedirectTarget != "" { - // Rig has redirect (e.g., "mayor/rig/.beads" for tracked beads). - // Redirect worktree directly to the final destination. - redirectPath = strings.Repeat("../", depth) + rigRedirectTarget - } - } - // Create redirect file redirectFile := filepath.Join(worktreeBeadsDir, "redirect") if err := os.WriteFile(redirectFile, []byte(redirectPath+"\n"), 0644); err != nil { diff --git a/internal/beads/beads_test.go b/internal/beads/beads_test.go index aa04dd33..c3e5d0b0 100644 --- a/internal/beads/beads_test.go +++ b/internal/beads/beads_test.go @@ -1741,7 +1741,7 @@ func TestSetupRedirect(t *testing.T) { rigRoot := filepath.Join(townRoot, "testrig") crewPath := filepath.Join(rigRoot, "crew", "max") - // No rig/.beads created + // No rig/.beads or mayor/rig/.beads created if err := os.MkdirAll(crewPath, 0755); err != nil { t.Fatalf("mkdir crew: %v", err) } @@ -1751,4 +1751,44 @@ func TestSetupRedirect(t *testing.T) { t.Error("SetupRedirect should fail if rig .beads missing") } }) + + t.Run("crew worktree with mayor/rig beads only", func(t *testing.T) { + // Setup: no rig/.beads, only mayor/rig/.beads exists + // This is the tracked beads architecture where rig root has no .beads directory + townRoot := t.TempDir() + rigRoot := filepath.Join(townRoot, "testrig") + mayorRigBeads := filepath.Join(rigRoot, "mayor", "rig", ".beads") + crewPath := filepath.Join(rigRoot, "crew", "max") + + // Create only mayor/rig/.beads (no rig/.beads) + if err := os.MkdirAll(mayorRigBeads, 0755); err != nil { + t.Fatalf("mkdir mayor/rig beads: %v", err) + } + if err := os.MkdirAll(crewPath, 0755); err != nil { + t.Fatalf("mkdir crew: %v", err) + } + + // Run SetupRedirect - should succeed and point to mayor/rig/.beads + if err := SetupRedirect(townRoot, crewPath); err != nil { + t.Fatalf("SetupRedirect failed: %v", err) + } + + // Verify redirect points to mayor/rig/.beads + redirectPath := filepath.Join(crewPath, ".beads", "redirect") + content, err := os.ReadFile(redirectPath) + if err != nil { + t.Fatalf("read redirect: %v", err) + } + + want := "../../mayor/rig/.beads\n" + if string(content) != want { + t.Errorf("redirect content = %q, want %q", string(content), want) + } + + // Verify redirect resolves correctly + resolved := ResolveBeadsDir(crewPath) + if resolved != mayorRigBeads { + t.Errorf("resolved = %q, want %q", resolved, mayorRigBeads) + } + }) }