diff --git a/internal/cmd/rig.go b/internal/cmd/rig.go index 701b6d71..b6a53111 100644 --- a/internal/cmd/rig.go +++ b/internal/cmd/rig.go @@ -217,10 +217,11 @@ func runRigAdd(cmd *cobra.Command, args []string) error { fmt.Printf("\nStructure:\n") fmt.Printf(" %s/\n", name) fmt.Printf(" ├── config.json\n") + fmt.Printf(" ├── .repo.git/ (shared bare repo)\n") fmt.Printf(" ├── .beads/ (prefix: %s)\n", newRig.Config.Prefix) fmt.Printf(" ├── plugins/ (rig-level plugins)\n") - fmt.Printf(" ├── refinery/rig/ (canonical main)\n") - fmt.Printf(" ├── mayor/rig/ (mayor's clone)\n") + fmt.Printf(" ├── mayor/rig/ (worktree: main)\n") + fmt.Printf(" ├── refinery/rig/ (worktree: refinery)\n") fmt.Printf(" ├── crew/%s/ (your workspace)\n", crewName) fmt.Printf(" ├── witness/\n") fmt.Printf(" └── polecats/\n") diff --git a/internal/git/git.go b/internal/git/git.go index 3a5d64bd..a575363c 100644 --- a/internal/git/git.go +++ b/internal/git/git.go @@ -20,6 +20,7 @@ var ( // Git wraps git operations for a working directory. type Git struct { workDir string + gitDir string // Optional: explicit git directory (for bare repos) } // NewGit creates a new Git wrapper for the given directory. @@ -27,6 +28,13 @@ func NewGit(workDir string) *Git { return &Git{workDir: workDir} } +// NewGitWithDir creates a Git wrapper with an explicit git directory. +// This is used for bare repos where gitDir points to the .git directory +// and workDir may be empty or point to a worktree. +func NewGitWithDir(gitDir, workDir string) *Git { + return &Git{gitDir: gitDir, workDir: workDir} +} + // WorkDir returns the working directory for this Git instance. func (g *Git) WorkDir() string { return g.workDir @@ -34,8 +42,15 @@ func (g *Git) WorkDir() string { // run executes a git command and returns stdout. func (g *Git) run(args ...string) (string, error) { + // If gitDir is set (bare repo), prepend --git-dir flag + if g.gitDir != "" { + args = append([]string{"--git-dir=" + g.gitDir}, args...) + } + cmd := exec.Command("git", args...) - cmd.Dir = g.workDir + if g.workDir != "" { + cmd.Dir = g.workDir + } var stdout, stderr bytes.Buffer cmd.Stdout = &stdout @@ -84,6 +99,18 @@ func (g *Git) Clone(url, dest string) error { return nil } +// CloneBare clones a repository as a bare repo (no working directory). +// This is used for the shared repo architecture where all worktrees share a single git database. +func (g *Git) CloneBare(url, dest string) error { + cmd := exec.Command("git", "clone", "--bare", url, dest) + var stderr bytes.Buffer + cmd.Stderr = &stderr + if err := cmd.Run(); err != nil { + return g.wrapError(err, stderr.String(), []string{"clone", "--bare", url}) + } + return nil +} + // Checkout checks out the given ref. func (g *Git) Checkout(ref string) error { _, err := g.run("checkout", ref) diff --git a/internal/polecat/manager.go b/internal/polecat/manager.go index 1e6f32fe..72f64f8d 100644 --- a/internal/polecat/manager.go +++ b/internal/polecat/manager.go @@ -82,6 +82,25 @@ func (m *Manager) assigneeID(name string) string { return fmt.Sprintf("%s/%s", m.rig.Name, name) } +// repoBase returns the git directory and Git object to use for worktree operations. +// Prefers the shared bare repo (.repo.git) if it exists, otherwise falls back to mayor/rig. +// The bare repo architecture allows all worktrees (refinery, polecats) to share branch visibility. +func (m *Manager) repoBase() (*git.Git, error) { + // First check for shared bare repo (new architecture) + bareRepoPath := filepath.Join(m.rig.Path, ".repo.git") + if info, err := os.Stat(bareRepoPath); err == nil && info.IsDir() { + // Bare repo exists - use it + return git.NewGitWithDir(bareRepoPath, ""), nil + } + + // Fall back to mayor/rig (legacy architecture) + mayorPath := filepath.Join(m.rig.Path, "mayor", "rig") + if _, err := os.Stat(mayorPath); os.IsNotExist(err) { + return nil, fmt.Errorf("no repo base found (neither .repo.git nor mayor/rig exists)") + } + return git.NewGit(mayorPath), nil +} + // polecatDir returns the directory for a polecat. func (m *Manager) polecatDir(name string) string { return filepath.Join(m.rig.Path, "polecats", name) @@ -93,8 +112,9 @@ func (m *Manager) exists(name string) bool { return err == nil } -// Add creates a new polecat as a git worktree from the mayor's clone. -// This is much faster than a full clone and shares objects with the mayor. +// Add creates a new polecat as a git worktree from the repo base. +// Uses the shared bare repo (.repo.git) if available, otherwise mayor/rig. +// This is much faster than a full clone and shares objects with all worktrees. // Polecat state is derived from beads assignee field, not state.json. func (m *Manager) Add(name string) (*Polecat, error) { if m.exists(name) { @@ -110,17 +130,14 @@ func (m *Manager) Add(name string) (*Polecat, error) { return nil, fmt.Errorf("creating polecats dir: %w", err) } - // Use Mayor's clone as the base for worktrees (Mayor is canonical for the rig) - mayorPath := filepath.Join(m.rig.Path, "mayor", "rig") - mayorGit := git.NewGit(mayorPath) - - // Verify Mayor's clone exists - if _, err := os.Stat(mayorPath); os.IsNotExist(err) { - return nil, fmt.Errorf("mayor clone not found at %s (run 'gt rig add' to set up rig structure)", mayorPath) + // Get the repo base (bare repo or mayor/rig) + repoGit, err := m.repoBase() + if err != nil { + return nil, fmt.Errorf("finding repo base: %w", err) } // Check if branch already exists (e.g., from previous polecat that wasn't cleaned up) - branchExists, err := mayorGit.BranchExists(branchName) + branchExists, err := repoGit.BranchExists(branchName) if err != nil { return nil, fmt.Errorf("checking branch existence: %w", err) } @@ -128,13 +145,13 @@ func (m *Manager) Add(name string) (*Polecat, error) { // Create worktree - reuse existing branch if it exists if branchExists { // Branch exists, create worktree using existing branch - if err := mayorGit.WorktreeAddExisting(polecatPath, branchName); err != nil { + if err := repoGit.WorktreeAddExisting(polecatPath, branchName); err != nil { return nil, fmt.Errorf("creating worktree with existing branch: %w", err) } } else { // Create new branch with worktree // git worktree add -b polecat/ - if err := mayorGit.WorktreeAdd(polecatPath, branchName); err != nil { + if err := repoGit.WorktreeAdd(polecatPath, branchName); err != nil { return nil, fmt.Errorf("creating worktree: %w", err) } } @@ -197,12 +214,15 @@ func (m *Manager) RemoveWithOptions(name string, force, nuclear bool) error { } } - // Use Mayor's clone to remove the worktree properly - mayorPath := filepath.Join(m.rig.Path, "mayor", "rig") - mayorGit := git.NewGit(mayorPath) + // Get repo base to remove the worktree properly + repoGit, err := m.repoBase() + if err != nil { + // Fall back to direct removal if repo base not found + return os.RemoveAll(polecatPath) + } // Try to remove as a worktree first (use force flag for worktree removal too) - if err := mayorGit.WorktreeRemove(polecatPath, force); err != nil { + if err := repoGit.WorktreeRemove(polecatPath, force); err != nil { // Fall back to direct removal if worktree removal fails // (e.g., if this is an old-style clone, not a worktree) if removeErr := os.RemoveAll(polecatPath); removeErr != nil { @@ -211,7 +231,7 @@ func (m *Manager) RemoveWithOptions(name string, force, nuclear bool) error { } // Prune any stale worktree entries - _ = mayorGit.WorktreePrune() + _ = repoGit.WorktreePrune() // Release name back to pool if it's a pooled name m.namePool.Release(name) @@ -257,10 +277,14 @@ func (m *Manager) Recreate(name string, force bool) (*Polecat, error) { polecatPath := m.polecatDir(name) branchName := fmt.Sprintf("polecat/%s", name) - mayorPath := filepath.Join(m.rig.Path, "mayor", "rig") - mayorGit := git.NewGit(mayorPath) polecatGit := git.NewGit(polecatPath) + // Get the repo base (bare repo or mayor/rig) + repoGit, err := m.repoBase() + if err != nil { + return nil, fmt.Errorf("finding repo base: %w", err) + } + // Check for uncommitted work unless forced if !force { status, err := polecatGit.CheckUncommittedWork() @@ -270,7 +294,7 @@ func (m *Manager) Recreate(name string, force bool) (*Polecat, error) { } // Remove the worktree (use force for git worktree removal) - if err := mayorGit.WorktreeRemove(polecatPath, true); err != nil { + if err := repoGit.WorktreeRemove(polecatPath, true); err != nil { // Fall back to direct removal if removeErr := os.RemoveAll(polecatPath); removeErr != nil { return nil, fmt.Errorf("removing polecat dir: %w", removeErr) @@ -278,14 +302,14 @@ func (m *Manager) Recreate(name string, force bool) (*Polecat, error) { } // Prune stale worktree entries - _ = mayorGit.WorktreePrune() + _ = repoGit.WorktreePrune() // Delete the old branch so worktree starts fresh from current HEAD // Ignore error - branch may not exist (first recreate) or may fail to delete - _ = mayorGit.DeleteBranch(branchName, true) + _ = repoGit.DeleteBranch(branchName, true) // Check if branch still exists (deletion may have failed or branch was protected) - branchExists, err := mayorGit.BranchExists(branchName) + branchExists, err := repoGit.BranchExists(branchName) if err != nil { return nil, fmt.Errorf("checking branch existence: %w", err) } @@ -294,12 +318,12 @@ func (m *Manager) Recreate(name string, force bool) (*Polecat, error) { if branchExists { // Branch still exists, create worktree using existing branch // This happens if delete failed (e.g., protected branch) - if err := mayorGit.WorktreeAddExisting(polecatPath, branchName); err != nil { + if err := repoGit.WorktreeAddExisting(polecatPath, branchName); err != nil { return nil, fmt.Errorf("creating worktree with existing branch: %w", err) } } else { // Branch was deleted, create fresh worktree with new branch from HEAD - if err := mayorGit.WorktreeAdd(polecatPath, branchName); err != nil { + if err := repoGit.WorktreeAdd(polecatPath, branchName); err != nil { return nil, fmt.Errorf("creating fresh worktree: %w", err) } } diff --git a/internal/rig/manager.go b/internal/rig/manager.go index d6e15ac4..01aa67c3 100644 --- a/internal/rig/manager.go +++ b/internal/rig/manager.go @@ -217,28 +217,40 @@ func (m *Manager) AddRig(opts AddRigOptions) (*Rig, error) { return nil, fmt.Errorf("saving rig config: %w", err) } - // Clone repository for mayor (must be first - serves as base for worktrees) + // Create shared bare repo as single source of truth for all worktrees. + // This architecture allows all worktrees (mayor, refinery, polecats) to share + // branch visibility without needing to push to remote. + bareRepoPath := filepath.Join(rigPath, ".repo.git") + if err := m.git.CloneBare(opts.GitURL, bareRepoPath); err != nil { + return nil, fmt.Errorf("creating bare repo: %w", err) + } + bareGit := git.NewGitWithDir(bareRepoPath, "") + + // Create mayor as worktree from bare repo on main mayorRigPath := filepath.Join(rigPath, "mayor", "rig") if err := os.MkdirAll(filepath.Dir(mayorRigPath), 0755); err != nil { return nil, fmt.Errorf("creating mayor dir: %w", err) } - if err := m.git.Clone(opts.GitURL, mayorRigPath); err != nil { - return nil, fmt.Errorf("cloning for mayor: %w", err) + if err := bareGit.WorktreeAddExisting(mayorRigPath, "main"); err != nil { + return nil, fmt.Errorf("creating mayor worktree: %w", err) } // Create mayor CLAUDE.md (overrides any from cloned repo) if err := m.createRoleCLAUDEmd(mayorRigPath, "mayor", opts.Name, ""); err != nil { return nil, fmt.Errorf("creating mayor CLAUDE.md: %w", err) } - // Create refinery as a worktree of mayor's clone. - // This allows refinery to see polecat branches locally (shared .git). - // Refinery uses the "refinery" branch which tracks main. + // Create refinery as worktree from bare repo on main. + // Refinery stays on main to merge polecat branches into main. + // Uses the same main branch as mayor - they share the working copy. refineryRigPath := filepath.Join(rigPath, "refinery", "rig") if err := os.MkdirAll(filepath.Dir(refineryRigPath), 0755); err != nil { return nil, fmt.Errorf("creating refinery dir: %w", err) } - mayorGit := git.NewGit(mayorRigPath) - if err := mayorGit.WorktreeAdd(refineryRigPath, "refinery"); err != nil { + // Create a refinery branch from main for the worktree + if err := bareGit.CreateBranchFrom("refinery", "main"); err != nil { + return nil, fmt.Errorf("creating refinery branch: %w", err) + } + if err := bareGit.WorktreeAddExisting(refineryRigPath, "refinery"); err != nil { return nil, fmt.Errorf("creating refinery worktree: %w", err) } // Create refinery CLAUDE.md (overrides any from cloned repo)