diff --git a/internal/cmd/prime.go b/internal/cmd/prime.go index 60956bef..4c1a5cfd 100644 --- a/internal/cmd/prime.go +++ b/internal/cmd/prime.go @@ -74,6 +74,9 @@ func runPrime(cmd *cobra.Command, args []string) error { // Detect role ctx := detectRole(cwd, townRoot) + // Ensure beads redirect exists for worktree-based roles + ensureBeadsRedirect(ctx) + // Output context if err := outputPrimeContext(ctx); err != nil { return err @@ -664,3 +667,81 @@ func outputDeaconPatrolContext(ctx RoleContext) { fmt.Println(" bd close ") fmt.Println(" gt mol bond mol-deacon-patrol") } + +// ensureBeadsRedirect ensures the .beads/redirect file exists for worktree-based roles. +// This handles cases where git clean or other operations delete the redirect file. +func ensureBeadsRedirect(ctx RoleContext) { + // Only applies to crew and polecat roles (they use shared beads) + if ctx.Role != RoleCrew && ctx.Role != RolePolecat { + return + } + + // Check if redirect already exists + beadsDir := filepath.Join(ctx.WorkDir, ".beads") + redirectPath := filepath.Join(beadsDir, "redirect") + + if _, err := os.Stat(redirectPath); err == nil { + // Redirect exists, nothing to do + return + } + + // Determine the correct redirect path based on role and rig structure + var redirectContent string + + // Get the rig root (parent of crew/ or polecats/) + var rigRoot string + relPath, err := filepath.Rel(ctx.TownRoot, ctx.WorkDir) + if err != nil { + return + } + parts := strings.Split(filepath.ToSlash(relPath), "/") + if len(parts) >= 1 { + rigRoot = filepath.Join(ctx.TownRoot, parts[0]) + } else { + return + } + + // Check for shared beads locations in order of preference: + // 1. rig/mayor/rig/.beads/ (if mayor rig clone exists) + // 2. rig/.beads/ (rig root beads) + mayorRigBeads := filepath.Join(rigRoot, "mayor", "rig", ".beads") + rigRootBeads := filepath.Join(rigRoot, ".beads") + + if _, err := os.Stat(mayorRigBeads); err == nil { + // Use mayor/rig/.beads + if ctx.Role == RoleCrew { + // crew//.beads -> ../../mayor/rig/.beads + redirectContent = "../../mayor/rig/.beads" + } else { + // polecats//.beads -> ../../mayor/rig/.beads + redirectContent = "../../mayor/rig/.beads" + } + } else if _, err := os.Stat(rigRootBeads); err == nil { + // Use rig root .beads + if ctx.Role == RoleCrew { + // crew//.beads -> ../../.beads + redirectContent = "../../.beads" + } else { + // polecats//.beads -> ../../.beads + redirectContent = "../../.beads" + } + } else { + // No shared beads found, nothing to redirect to + return + } + + // Create .beads directory if needed + if err := os.MkdirAll(beadsDir, 0755); err != nil { + // Silently fail - not critical + return + } + + // Write redirect file + if err := os.WriteFile(redirectPath, []byte(redirectContent+"\n"), 0644); err != nil { + // Silently fail - not critical + return + } + + // Note: We don't print a message here to avoid cluttering prime output + // The redirect is silently restored +} diff --git a/internal/crew/manager.go b/internal/crew/manager.go index 99bc300f..8654732a 100644 --- a/internal/crew/manager.go +++ b/internal/crew/manager.go @@ -98,6 +98,12 @@ func (m *Manager) Add(name string, createBranch bool) (*CrewWorker, error) { return nil, fmt.Errorf("creating mail dir: %w", err) } + // Set up shared beads: crew uses rig's shared beads via redirect file + if err := m.setupSharedBeads(crewPath); err != nil { + // Non-fatal - crew can still work, warn but don't fail + fmt.Printf("Warning: could not set up shared beads: %v\n", err) + } + // Create CLAUDE.md with crew worker prompting if err := m.createClaudeMD(name, crewPath); err != nil { _ = os.RemoveAll(crewPath) @@ -380,3 +386,53 @@ type PristineResult struct { Synced bool `json:"synced"` SyncError string `json:"sync_error,omitempty"` } + +// setupSharedBeads creates a redirect file so the crew worker uses the rig's shared .beads database. +// This eliminates the need for git sync between crew clones - all crew members share one database. +// +// Structure: +// +// rig/ +// mayor/rig/.beads/ <- Shared database (the canonical location) +// crew/ +// / +// .beads/ +// redirect <- Contains "../../mayor/rig/.beads" +func (m *Manager) setupSharedBeads(crewPath string) error { + // The shared beads database is at rig/mayor/rig/.beads/ + // Crew clones are at rig/crew// + // So the relative path is ../../mayor/rig/.beads + sharedBeadsPath := filepath.Join(m.rig.Path, "mayor", "rig", ".beads") + + // Verify the shared beads exists + if _, err := os.Stat(sharedBeadsPath); os.IsNotExist(err) { + // Fall back to rig root .beads if mayor/rig doesn't exist + sharedBeadsPath = filepath.Join(m.rig.Path, ".beads") + if _, err := os.Stat(sharedBeadsPath); os.IsNotExist(err) { + return fmt.Errorf("no shared beads database found") + } + } + + // Create crew's .beads directory + crewBeadsDir := filepath.Join(crewPath, ".beads") + if err := os.MkdirAll(crewBeadsDir, 0755); err != nil { + return fmt.Errorf("creating crew .beads dir: %w", err) + } + + // Calculate relative path from crew/.beads/ to shared beads + // crew//.beads/ -> ../../mayor/rig/.beads or ../../.beads + var redirectContent string + if _, err := os.Stat(filepath.Join(m.rig.Path, "mayor", "rig", ".beads")); err == nil { + redirectContent = "../../mayor/rig/.beads\n" + } else { + redirectContent = "../../.beads\n" + } + + // Create redirect file + redirectPath := filepath.Join(crewBeadsDir, "redirect") + if err := os.WriteFile(redirectPath, []byte(redirectContent), 0644); err != nil { + return fmt.Errorf("creating redirect file: %w", err) + } + + return nil +}