fix(beads): Auto-create .beads/redirect for crew and polecats
Fixes gt-b6qm: redirect files can get deleted by git clean, causing "no beads database found" errors. Changes: - crew.Manager.Add() now creates .beads/redirect during setup - gt prime regenerates missing redirects silently on startup The redirect points to the shared beads database at either: - rig/mayor/rig/.beads/ (preferred) - rig/.beads/ (fallback) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -74,6 +74,9 @@ func runPrime(cmd *cobra.Command, args []string) error {
|
|||||||
// Detect role
|
// Detect role
|
||||||
ctx := detectRole(cwd, townRoot)
|
ctx := detectRole(cwd, townRoot)
|
||||||
|
|
||||||
|
// Ensure beads redirect exists for worktree-based roles
|
||||||
|
ensureBeadsRedirect(ctx)
|
||||||
|
|
||||||
// Output context
|
// Output context
|
||||||
if err := outputPrimeContext(ctx); err != nil {
|
if err := outputPrimeContext(ctx); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -664,3 +667,81 @@ func outputDeaconPatrolContext(ctx RoleContext) {
|
|||||||
fmt.Println(" bd close <in-progress-issues>")
|
fmt.Println(" bd close <in-progress-issues>")
|
||||||
fmt.Println(" gt mol bond mol-deacon-patrol")
|
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/<name>/.beads -> ../../mayor/rig/.beads
|
||||||
|
redirectContent = "../../mayor/rig/.beads"
|
||||||
|
} else {
|
||||||
|
// polecats/<name>/.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/<name>/.beads -> ../../.beads
|
||||||
|
redirectContent = "../../.beads"
|
||||||
|
} else {
|
||||||
|
// polecats/<name>/.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
|
||||||
|
}
|
||||||
|
|||||||
@@ -98,6 +98,12 @@ func (m *Manager) Add(name string, createBranch bool) (*CrewWorker, error) {
|
|||||||
return nil, fmt.Errorf("creating mail dir: %w", err)
|
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
|
// Create CLAUDE.md with crew worker prompting
|
||||||
if err := m.createClaudeMD(name, crewPath); err != nil {
|
if err := m.createClaudeMD(name, crewPath); err != nil {
|
||||||
_ = os.RemoveAll(crewPath)
|
_ = os.RemoveAll(crewPath)
|
||||||
@@ -380,3 +386,53 @@ type PristineResult struct {
|
|||||||
Synced bool `json:"synced"`
|
Synced bool `json:"synced"`
|
||||||
SyncError string `json:"sync_error,omitempty"`
|
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/
|
||||||
|
// <name>/
|
||||||
|
// .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/<name>/
|
||||||
|
// 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/<name>/.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
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user