feat: Unified beads redirect for tracked and local beads (#222)

* feat: Beads redirect architecture for tracked and local beads

This change implements proper redirect handling so that all rig agents
(Witness, Refinery, Crew, Polecats) can work with both:
- Tracked beads: .beads/ checked into git at mayor/rig/.beads
- Local beads: .beads/ created at rig root during gt rig add

Key changes:

1. SetupRedirect now handles tracked beads by skipping redirect chains.
   The bd CLI doesn't support chains (A→B→C), so worktrees redirect
   directly to the final destination (mayor/rig/.beads for tracked).

2. ResolveBeadsDir is now used consistently in polecat and refinery
   managers instead of hardcoded mayor/rig paths.

3. Rig-level agents (witness, refinery) now use rig beads with rig
   prefix instead of town beads. This follows the architecture where
   town beads are only for Mayor/Deacon.

4. prime.go simplified to always use ../../.beads for crew redirects,
   letting rig-level redirect handle tracked vs local routing.

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat(doctor): Add beads-redirect check for tracked beads

When a repo has .beads/ tracked in git (at mayor/rig/.beads), the rig root
needs a redirect file pointing to that location. This check:

- Detects missing rig-level redirect for tracked beads
- Verifies redirect points to correct location (mayor/rig/.beads)
- Auto-fixes with 'gt doctor --fix'

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: Handle fileLock.Unlock error in daemon

Wrap fileLock.Unlock() return value to satisfy errcheck linter.

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Julian Knutsen
2026-01-06 12:59:37 -08:00
committed by GitHub
parent 16fb45bb2a
commit 9d7dcde1e2
11 changed files with 1075 additions and 231 deletions

View File

@@ -48,12 +48,11 @@ type Manager struct {
// NewManager creates a new polecat manager.
func NewManager(r *rig.Rig, g *git.Git) *Manager {
// Always use mayor/rig as the beads path.
// This matches routes.jsonl which maps prefixes to <rig>/mayor/rig.
// The rig root .beads/ only contains config.yaml (no database),
// so running bd from there causes it to walk up and find town beads
// with the wrong prefix (e.g., 'gm' instead of the rig's prefix).
beadsPath := filepath.Join(r.Path, "mayor", "rig")
// Use the resolved beads directory to find where bd commands should run.
// For tracked beads: rig/.beads/redirect -> mayor/rig/.beads, so use mayor/rig
// For local beads: rig/.beads is the database, so use rig root
resolvedBeads := beads.ResolveBeadsDir(r.Path)
beadsPath := filepath.Dir(resolvedBeads) // Get the directory containing .beads
// Try to load rig settings for namepool config
settingsPath := filepath.Join(r.Path, "settings", "config.json")
@@ -721,74 +720,9 @@ func (m *Manager) loadFromBeads(name string) (*Polecat, error) {
// setupSharedBeads creates a redirect file so the polecat uses the rig's shared .beads database.
// This eliminates the need for git sync between polecat clones - all polecats share one database.
//
// Structure:
//
// rig/
// .beads/ <- Shared database (ensured to exist)
// polecats/
// <name>/
// .beads/
// redirect <- Contains "../../.beads" or "../../mayor/rig/.beads"
//
// IMPORTANT: If the polecat was created from a branch that had .beads/ tracked in git,
// those files will be present. We must clean them out and replace with just the redirect.
//
// The redirect target is conditional: repos with .beads/ tracked in git have their canonical
// database at mayor/rig/.beads, while fresh rigs use the database at rig root .beads/.
func (m *Manager) setupSharedBeads(polecatPath string) error {
// Determine the shared beads location:
// - If mayor/rig/.beads exists (source repo has beads tracked in git), use that
// - Otherwise fall back to rig/.beads (created by initBeads during gt rig add)
// This matches the crew manager's logic for consistency.
mayorRigBeads := filepath.Join(m.rig.Path, "mayor", "rig", ".beads")
rigRootBeads := filepath.Join(m.rig.Path, ".beads")
var sharedBeadsPath string
var redirectContent string
if _, err := os.Stat(mayorRigBeads); err == nil {
// Source repo has .beads/ tracked - use mayor/rig/.beads
sharedBeadsPath = mayorRigBeads
redirectContent = "../../mayor/rig/.beads\n"
} else {
// No beads in source repo - use rig root .beads (from initBeads)
sharedBeadsPath = rigRootBeads
redirectContent = "../../.beads\n"
// Ensure rig root has .beads/ directory
if err := os.MkdirAll(rigRootBeads, 0755); err != nil {
return fmt.Errorf("creating rig .beads dir: %w", err)
}
}
// Verify shared beads exists
if _, err := os.Stat(sharedBeadsPath); os.IsNotExist(err) {
return fmt.Errorf("no shared beads database found at %s", sharedBeadsPath)
}
// Clean up any existing .beads/ contents from the branch
// This handles the case where the polecat was created from a branch that
// had .beads/ tracked (e.g., from previous bd sync operations)
polecatBeadsDir := filepath.Join(polecatPath, ".beads")
if _, err := os.Stat(polecatBeadsDir); err == nil {
// Directory exists - remove it entirely and recreate fresh
if err := os.RemoveAll(polecatBeadsDir); err != nil {
return fmt.Errorf("cleaning existing .beads dir: %w", err)
}
}
// Create fresh .beads directory
if err := os.MkdirAll(polecatBeadsDir, 0755); err != nil {
return fmt.Errorf("creating polecat .beads dir: %w", err)
}
// Create redirect file pointing to the shared beads location
redirectPath := filepath.Join(polecatBeadsDir, "redirect")
if err := os.WriteFile(redirectPath, []byte(redirectContent), 0644); err != nil {
return fmt.Errorf("creating redirect file: %w", err)
}
return nil
townRoot := filepath.Dir(m.rig.Path)
return beads.SetupRedirect(townRoot, polecatPath)
}
// CleanupStaleBranches removes orphaned polecat branches that are no longer in use.