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

@@ -367,6 +367,14 @@ func (m *Manager) AddRig(opts AddRigOptions) (*Rig, error) {
return nil, fmt.Errorf("creating mayor CLAUDE.md: %w", err)
}
// Initialize beads at rig level BEFORE creating worktrees.
// This ensures rig/.beads exists so worktree redirects can point to it.
fmt.Printf(" Initializing beads database...\n")
if err := m.initBeads(rigPath, opts.BeadsPrefix); err != nil {
return nil, fmt.Errorf("initializing beads: %w", err)
}
fmt.Printf(" ✓ Initialized beads (prefix: %s)\n", opts.BeadsPrefix)
// Create refinery as worktree from bare repo on default branch.
// Refinery needs to see polecat branches (shared .repo.git) and merges them.
// Being on the default branch allows direct merge workflow.
@@ -379,6 +387,10 @@ func (m *Manager) AddRig(opts AddRigOptions) (*Rig, error) {
return nil, fmt.Errorf("creating refinery worktree: %w", err)
}
fmt.Printf(" ✓ Created refinery worktree\n")
// Set up beads redirect for refinery (points to rig-level .beads)
if err := beads.SetupRedirect(m.townRoot, refineryRigPath); err != nil {
fmt.Printf(" Warning: Could not set up refinery beads redirect: %v\n", err)
}
// Create refinery CLAUDE.md (overrides any from cloned repo)
if err := m.createRoleCLAUDEmd(refineryRigPath, "refinery", opts.Name, ""); err != nil {
return nil, fmt.Errorf("creating refinery CLAUDE.md: %w", err)
@@ -433,13 +445,6 @@ Use crew for your own workspace. Polecats are for batch work dispatch.
return nil, fmt.Errorf("creating polecats dir: %w", err)
}
// Initialize beads at rig level
fmt.Printf(" Initializing beads database...\n")
if err := m.initBeads(rigPath, opts.BeadsPrefix); err != nil {
return nil, fmt.Errorf("initializing beads: %w", err)
}
fmt.Printf(" ✓ Initialized beads (prefix: %s)\n", opts.BeadsPrefix)
// Create rig-level agent beads (witness, refinery) in rig beads.
// Town-level agents (mayor, deacon) are created by gt install in town beads.
if err := m.initAgentBeads(rigPath, opts.Name, opts.BeadsPrefix); err != nil {
@@ -508,6 +513,23 @@ func (m *Manager) initBeads(rigPath, prefix string) error {
}
beadsDir := filepath.Join(rigPath, ".beads")
mayorRigBeads := filepath.Join(rigPath, "mayor", "rig", ".beads")
// Check if source repo has tracked .beads/ (cloned into mayor/rig).
// If so, create a redirect file instead of a new database.
if _, err := os.Stat(mayorRigBeads); err == nil {
// Tracked beads exist - create redirect to mayor/rig/.beads
if err := os.MkdirAll(beadsDir, 0755); err != nil {
return err
}
redirectPath := filepath.Join(beadsDir, "redirect")
if err := os.WriteFile(redirectPath, []byte("mayor/rig/.beads\n"), 0644); err != nil {
return fmt.Errorf("creating redirect file: %w", err)
}
return nil
}
// No tracked beads - create local database
if err := os.MkdirAll(beadsDir, 0755); err != nil {
return err
}
@@ -572,7 +594,8 @@ func (m *Manager) initBeads(rigPath, prefix string) error {
func (m *Manager) initAgentBeads(rigPath, rigName, prefix string) error {
// Rig-level agents go in rig beads with rig prefix (per docs/architecture.md).
// Town-level agents (Mayor, Deacon) are created by gt install in town beads.
rigBeadsDir := filepath.Join(rigPath, ".beads")
// Use ResolveBeadsDir to follow redirect files for tracked beads.
rigBeadsDir := beads.ResolveBeadsDir(rigPath)
bd := beads.NewWithBeadsDir(rigPath, rigBeadsDir)
// Define rig-level agents to create