fix: Detect and auto-remove circular redirect files in beads (gt-csbjj)

Added multiple layers of protection against circular redirects:

1. ResolveBeadsDir now detects when a redirect points back to itself
   and auto-removes the errant redirect file with a warning

2. ensureBeadsRedirect now includes safety checks:
   - Prevents creating redirects inside mayor/rig/ (the canonical location)
   - Validates that the redirect target is not the same as the beads dir

3. Added test case for circular redirect detection

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Steve Yegge
2025-12-30 00:46:52 -08:00
parent de32cb0be2
commit a68cf54057
3 changed files with 87 additions and 13 deletions

View File

@@ -1187,12 +1187,39 @@ func getAgentBeadID(ctx RoleContext) string {
// ensureBeadsRedirect ensures the .beads/redirect file exists for worktree-based roles.
// This handles cases where git clean or other operations delete the redirect file.
//
// IMPORTANT: This function includes safety checks to prevent creating redirects in
// the canonical beads location (mayor/rig/.beads), which would cause circular redirects.
func ensureBeadsRedirect(ctx RoleContext) {
// Only applies to crew and polecat roles (they use shared beads)
if ctx.Role != RoleCrew && ctx.Role != RolePolecat {
return
}
// Get the rig root (parent of crew/ or polecats/)
relPath, err := filepath.Rel(ctx.TownRoot, ctx.WorkDir)
if err != nil {
return
}
parts := strings.Split(filepath.ToSlash(relPath), "/")
if len(parts) < 1 {
return
}
rigRoot := filepath.Join(ctx.TownRoot, parts[0])
// SAFETY CHECK: Prevent creating redirect in canonical beads location
// If workDir is inside mayor/rig/, we should NOT create a redirect there
// This prevents circular redirects like mayor/rig/.beads/redirect -> ../../mayor/rig/.beads
mayorRigPath := filepath.Join(rigRoot, "mayor", "rig")
workDirAbs, _ := filepath.Abs(ctx.WorkDir)
mayorRigPathAbs, _ := filepath.Abs(mayorRigPath)
if strings.HasPrefix(workDirAbs, mayorRigPathAbs) {
// We're inside mayor/rig/ - this is not a polecat/crew worker location
// Role detection may be wrong (e.g., GT_ROLE env var mismatch)
// Do NOT create a redirect here
return
}
// Check if redirect already exists
beadsDir := filepath.Join(ctx.WorkDir, ".beads")
redirectPath := filepath.Join(beadsDir, "redirect")
@@ -1205,19 +1232,6 @@ func ensureBeadsRedirect(ctx RoleContext) {
// 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)
@@ -1247,6 +1261,15 @@ func ensureBeadsRedirect(ctx RoleContext) {
return
}
// SAFETY CHECK: Verify the redirect won't be circular
// Resolve the redirect target and check it's not the same as our beads dir
resolvedTarget := filepath.Join(ctx.WorkDir, redirectContent)
resolvedTarget = filepath.Clean(resolvedTarget)
if resolvedTarget == beadsDir {
// Would create circular redirect - don't do it
return
}
// Create .beads directory if needed
if err := os.MkdirAll(beadsDir, 0755); err != nil {
// Silently fail - not critical