Skip to content

Commit f4cbcb4

Browse files
steveyeggeclaude
andcommitted
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>
1 parent c4d956e commit f4cbcb4

2 files changed

Lines changed: 84 additions & 25 deletions

File tree

internal/beads/beads.go

Lines changed: 43 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -161,9 +161,10 @@ func cleanBeadsRuntimeFiles(beadsDir string) {
161161
// - worktreePath: the worktree directory (e.g., <rig>/crew/<name> or <rig>/refinery/rig)
162162
//
163163
// The function:
164-
// 1. Computes the relative path from worktree to rig-level .beads
165-
// 2. Cleans up runtime files (preserving tracked files like formulas/)
166-
// 3. Creates the redirect file
164+
// 1. Finds the canonical beads location (rig/.beads or mayor/rig/.beads)
165+
// 2. Computes the relative path from worktree to that location
166+
// 3. Cleans up runtime files (preserving tracked files like formulas/)
167+
// 4. Creates the redirect file
167168
//
168169
// Safety: This function refuses to create redirects in the canonical beads location
169170
// (mayor/rig) to prevent circular redirect chains.
@@ -186,10 +187,47 @@ func SetupRedirect(townRoot, worktreePath string) error {
186187
}
187188

188189
rigRoot := filepath.Join(townRoot, parts[0])
190+
191+
// Find the canonical beads location. In order of preference:
192+
// 1. rig/.beads (if it exists and has content or a redirect)
193+
// 2. mayor/rig/.beads (tracked beads architecture)
194+
//
195+
// The tracked beads architecture stores the actual database in mayor/rig/.beads
196+
// and may not have a rig/.beads directory at all.
189197
rigBeadsPath := filepath.Join(rigRoot, ".beads")
198+
mayorBeadsPath := filepath.Join(rigRoot, "mayor", "rig", ".beads")
190199

191-
if _, err := os.Stat(rigBeadsPath); os.IsNotExist(err) {
192-
return fmt.Errorf("no rig .beads found at %s", rigBeadsPath)
200+
// Compute depth for relative paths
201+
// e.g., crew/<name> (depth 2) -> ../../
202+
// refinery/rig (depth 2) -> ../../
203+
depth := len(parts) - 1 // subtract 1 for rig name itself
204+
upPath := strings.Repeat("../", depth)
205+
206+
var redirectPath string
207+
208+
// Check if rig-level .beads exists
209+
if _, err := os.Stat(rigBeadsPath); err == nil {
210+
// rig/.beads exists - check if it has a redirect to follow
211+
rigRedirectPath := filepath.Join(rigBeadsPath, "redirect")
212+
if data, err := os.ReadFile(rigRedirectPath); err == nil {
213+
rigRedirectTarget := strings.TrimSpace(string(data))
214+
if rigRedirectTarget != "" {
215+
// Rig has redirect (e.g., "mayor/rig/.beads" for tracked beads).
216+
// Redirect worktree directly to the final destination.
217+
redirectPath = upPath + rigRedirectTarget
218+
}
219+
}
220+
// If no redirect in rig/.beads, point directly to rig/.beads
221+
if redirectPath == "" {
222+
redirectPath = upPath + ".beads"
223+
}
224+
} else if _, err := os.Stat(mayorBeadsPath); err == nil {
225+
// No rig/.beads but mayor/rig/.beads exists (tracked beads architecture).
226+
// Point directly to mayor/rig/.beads.
227+
redirectPath = upPath + "mayor/rig/.beads"
228+
} else {
229+
// Neither location exists - this is an error
230+
return fmt.Errorf("no beads found at %s or %s", rigBeadsPath, mayorBeadsPath)
193231
}
194232

195233
// Clean up runtime files in .beads/ but preserve tracked files (formulas/, README.md, etc.)
@@ -201,25 +239,6 @@ func SetupRedirect(townRoot, worktreePath string) error {
201239
return fmt.Errorf("creating .beads dir: %w", err)
202240
}
203241

204-
// Compute relative path from worktree to rig root
205-
// e.g., crew/<name> (depth 2) -> ../../.beads
206-
// refinery/rig (depth 2) -> ../../.beads
207-
depth := len(parts) - 1 // subtract 1 for rig name itself
208-
redirectPath := strings.Repeat("../", depth) + ".beads"
209-
210-
// Check if rig-level beads has a redirect (tracked beads case).
211-
// If so, redirect directly to the final destination to avoid chains.
212-
// The bd CLI doesn't support redirect chains, so we must skip intermediate hops.
213-
rigRedirectPath := filepath.Join(rigBeadsPath, "redirect")
214-
if data, err := os.ReadFile(rigRedirectPath); err == nil {
215-
rigRedirectTarget := strings.TrimSpace(string(data))
216-
if rigRedirectTarget != "" {
217-
// Rig has redirect (e.g., "mayor/rig/.beads" for tracked beads).
218-
// Redirect worktree directly to the final destination.
219-
redirectPath = strings.Repeat("../", depth) + rigRedirectTarget
220-
}
221-
}
222-
223242
// Create redirect file
224243
redirectFile := filepath.Join(worktreeBeadsDir, "redirect")
225244
if err := os.WriteFile(redirectFile, []byte(redirectPath+"\n"), 0644); err != nil {

internal/beads/beads_test.go

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1741,7 +1741,7 @@ func TestSetupRedirect(t *testing.T) {
17411741
rigRoot := filepath.Join(townRoot, "testrig")
17421742
crewPath := filepath.Join(rigRoot, "crew", "max")
17431743

1744-
// No rig/.beads created
1744+
// No rig/.beads or mayor/rig/.beads created
17451745
if err := os.MkdirAll(crewPath, 0755); err != nil {
17461746
t.Fatalf("mkdir crew: %v", err)
17471747
}
@@ -1751,4 +1751,44 @@ func TestSetupRedirect(t *testing.T) {
17511751
t.Error("SetupRedirect should fail if rig .beads missing")
17521752
}
17531753
})
1754+
1755+
t.Run("crew worktree with mayor/rig beads only", func(t *testing.T) {
1756+
// Setup: no rig/.beads, only mayor/rig/.beads exists
1757+
// This is the tracked beads architecture where rig root has no .beads directory
1758+
townRoot := t.TempDir()
1759+
rigRoot := filepath.Join(townRoot, "testrig")
1760+
mayorRigBeads := filepath.Join(rigRoot, "mayor", "rig", ".beads")
1761+
crewPath := filepath.Join(rigRoot, "crew", "max")
1762+
1763+
// Create only mayor/rig/.beads (no rig/.beads)
1764+
if err := os.MkdirAll(mayorRigBeads, 0755); err != nil {
1765+
t.Fatalf("mkdir mayor/rig beads: %v", err)
1766+
}
1767+
if err := os.MkdirAll(crewPath, 0755); err != nil {
1768+
t.Fatalf("mkdir crew: %v", err)
1769+
}
1770+
1771+
// Run SetupRedirect - should succeed and point to mayor/rig/.beads
1772+
if err := SetupRedirect(townRoot, crewPath); err != nil {
1773+
t.Fatalf("SetupRedirect failed: %v", err)
1774+
}
1775+
1776+
// Verify redirect points to mayor/rig/.beads
1777+
redirectPath := filepath.Join(crewPath, ".beads", "redirect")
1778+
content, err := os.ReadFile(redirectPath)
1779+
if err != nil {
1780+
t.Fatalf("read redirect: %v", err)
1781+
}
1782+
1783+
want := "../../mayor/rig/.beads\n"
1784+
if string(content) != want {
1785+
t.Errorf("redirect content = %q, want %q", string(content), want)
1786+
}
1787+
1788+
// Verify redirect resolves correctly
1789+
resolved := ResolveBeadsDir(crewPath)
1790+
if resolved != mayorRigBeads {
1791+
t.Errorf("resolved = %q, want %q", resolved, mayorRigBeads)
1792+
}
1793+
})
17541794
}

0 commit comments

Comments
 (0)