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 <noreply@anthropic.com>
This commit is contained in:
@@ -161,9 +161,10 @@ func cleanBeadsRuntimeFiles(beadsDir string) {
|
|||||||
// - worktreePath: the worktree directory (e.g., <rig>/crew/<name> or <rig>/refinery/rig)
|
// - worktreePath: the worktree directory (e.g., <rig>/crew/<name> or <rig>/refinery/rig)
|
||||||
//
|
//
|
||||||
// The function:
|
// The function:
|
||||||
// 1. Computes the relative path from worktree to rig-level .beads
|
// 1. Finds the canonical beads location (rig/.beads or mayor/rig/.beads)
|
||||||
// 2. Cleans up runtime files (preserving tracked files like formulas/)
|
// 2. Computes the relative path from worktree to that location
|
||||||
// 3. Creates the redirect file
|
// 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
|
// Safety: This function refuses to create redirects in the canonical beads location
|
||||||
// (mayor/rig) to prevent circular redirect chains.
|
// (mayor/rig) to prevent circular redirect chains.
|
||||||
@@ -186,10 +187,47 @@ func SetupRedirect(townRoot, worktreePath string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
rigRoot := filepath.Join(townRoot, parts[0])
|
rigRoot := filepath.Join(townRoot, parts[0])
|
||||||
rigBeadsPath := filepath.Join(rigRoot, ".beads")
|
|
||||||
|
|
||||||
if _, err := os.Stat(rigBeadsPath); os.IsNotExist(err) {
|
// Find the canonical beads location. In order of preference:
|
||||||
return fmt.Errorf("no rig .beads found at %s", rigBeadsPath)
|
// 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/<name> (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.)
|
// 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)
|
return fmt.Errorf("creating .beads dir: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compute relative path from worktree to rig root
|
|
||||||
// e.g., crew/<name> (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
|
// Create redirect file
|
||||||
redirectFile := filepath.Join(worktreeBeadsDir, "redirect")
|
redirectFile := filepath.Join(worktreeBeadsDir, "redirect")
|
||||||
if err := os.WriteFile(redirectFile, []byte(redirectPath+"\n"), 0644); err != nil {
|
if err := os.WriteFile(redirectFile, []byte(redirectPath+"\n"), 0644); err != nil {
|
||||||
|
|||||||
@@ -1741,7 +1741,7 @@ func TestSetupRedirect(t *testing.T) {
|
|||||||
rigRoot := filepath.Join(townRoot, "testrig")
|
rigRoot := filepath.Join(townRoot, "testrig")
|
||||||
crewPath := filepath.Join(rigRoot, "crew", "max")
|
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 {
|
if err := os.MkdirAll(crewPath, 0755); err != nil {
|
||||||
t.Fatalf("mkdir crew: %v", err)
|
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.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)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user