fix: polecat workers start from origin/<default-branch> when recycled
When a polecat worker is recycled via RecreateWithOptions, it now starts from the latest fetched origin/<default-branch> instead of the stale HEAD. Previously, `WorktreeAdd` created branches from the current HEAD, but after fetching, HEAD still pointed to old commits. The new `WorktreeAddFromRef` method allows specifying a start point (e.g., "origin/main"). Fixes #101
This commit is contained in:
@@ -40,6 +40,12 @@ func (g *Git) WorkDir() string {
|
||||
return g.workDir
|
||||
}
|
||||
|
||||
// IsRepo returns true if the workDir is a git repository.
|
||||
func (g *Git) IsRepo() bool {
|
||||
_, err := g.run("rev-parse", "--git-dir")
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// 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
|
||||
@@ -99,6 +105,18 @@ func (g *Git) Clone(url, dest string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// CloneWithReference clones a repository using a local repo as an object reference.
|
||||
// This saves disk by sharing objects without changing remotes.
|
||||
func (g *Git) CloneWithReference(url, dest, reference string) error {
|
||||
cmd := exec.Command("git", "clone", "--reference-if-able", reference, url, dest)
|
||||
var stderr bytes.Buffer
|
||||
cmd.Stderr = &stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
return g.wrapError(err, stderr.String(), []string{"clone", "--reference-if-able", url})
|
||||
}
|
||||
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 {
|
||||
@@ -111,6 +129,17 @@ func (g *Git) CloneBare(url, dest string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// CloneBareWithReference clones a bare repository using a local repo as an object reference.
|
||||
func (g *Git) CloneBareWithReference(url, dest, reference string) error {
|
||||
cmd := exec.Command("git", "clone", "--bare", "--reference-if-able", reference, url, dest)
|
||||
var stderr bytes.Buffer
|
||||
cmd.Stderr = &stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
return g.wrapError(err, stderr.String(), []string{"clone", "--bare", "--reference-if-able", url})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Checkout checks out the given ref.
|
||||
func (g *Git) Checkout(ref string) error {
|
||||
_, err := g.run("checkout", ref)
|
||||
@@ -226,6 +255,36 @@ func (g *Git) DefaultBranch() string {
|
||||
return "main"
|
||||
}
|
||||
|
||||
// RemoteDefaultBranch returns the default branch from the remote (origin).
|
||||
// This is useful in worktrees where HEAD may not reflect the repo's actual default.
|
||||
// Checks origin/HEAD first, then falls back to checking if master/main exists.
|
||||
// Returns "main" as final fallback.
|
||||
func (g *Git) RemoteDefaultBranch() string {
|
||||
// Try to get from origin/HEAD symbolic ref
|
||||
out, err := g.run("symbolic-ref", "refs/remotes/origin/HEAD")
|
||||
if err == nil && out != "" {
|
||||
// Returns refs/remotes/origin/main -> extract branch name
|
||||
parts := strings.Split(out, "/")
|
||||
if len(parts) > 0 {
|
||||
return parts[len(parts)-1]
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: check if origin/master exists
|
||||
_, err = g.run("rev-parse", "--verify", "origin/master")
|
||||
if err == nil {
|
||||
return "master"
|
||||
}
|
||||
|
||||
// Fallback: check if origin/main exists
|
||||
_, err = g.run("rev-parse", "--verify", "origin/main")
|
||||
if err == nil {
|
||||
return "main"
|
||||
}
|
||||
|
||||
return "main" // final fallback
|
||||
}
|
||||
|
||||
// HasUncommittedChanges returns true if there are uncommitted changes.
|
||||
func (g *Git) HasUncommittedChanges() (bool, error) {
|
||||
status, err := g.Status()
|
||||
@@ -464,6 +523,13 @@ func (g *Git) WorktreeAdd(path, branch string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// WorktreeAddFromRef creates a new worktree at the given path with a new branch
|
||||
// starting from the specified ref (e.g., "origin/main").
|
||||
func (g *Git) WorktreeAddFromRef(path, branch, startPoint string) error {
|
||||
_, err := g.run("worktree", "add", "-b", branch, path, startPoint)
|
||||
return err
|
||||
}
|
||||
|
||||
// WorktreeAddDetached creates a new worktree at the given path with a detached HEAD.
|
||||
func (g *Git) WorktreeAddDetached(path, ref string) error {
|
||||
_, err := g.run("worktree", "add", "--detach", path, ref)
|
||||
|
||||
@@ -12,7 +12,6 @@ import (
|
||||
"github.com/steveyegge/gastown/internal/config"
|
||||
"github.com/steveyegge/gastown/internal/git"
|
||||
"github.com/steveyegge/gastown/internal/rig"
|
||||
"github.com/steveyegge/gastown/internal/templates"
|
||||
"github.com/steveyegge/gastown/internal/workspace"
|
||||
)
|
||||
|
||||
@@ -48,16 +47,12 @@ type Manager struct {
|
||||
|
||||
// NewManager creates a new polecat manager.
|
||||
func NewManager(r *rig.Rig, g *git.Git) *Manager {
|
||||
// Determine the canonical beads location:
|
||||
// - If mayor/rig/.beads exists (source repo has beads tracked), use that
|
||||
// - Otherwise use rig root .beads/ (created by initBeads during gt rig add)
|
||||
// This matches the conditional logic in setupSharedBeads and route registration.
|
||||
// For repos that have .beads/ tracked in git, the canonical database lives in mayor/rig/.
|
||||
mayorRigBeads := filepath.Join(r.Path, "mayor", "rig", ".beads")
|
||||
beadsPath := r.Path
|
||||
if _, err := os.Stat(mayorRigBeads); err == nil {
|
||||
beadsPath = filepath.Join(r.Path, "mayor", "rig")
|
||||
}
|
||||
// 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")
|
||||
|
||||
// Try to load rig settings for namepool config
|
||||
settingsPath := filepath.Join(r.Path, "settings", "config.json")
|
||||
@@ -245,12 +240,8 @@ func (m *Manager) AddWithOptions(name string, opts AddOptions) (*Polecat, error)
|
||||
fmt.Printf("Warning: could not set up shared beads: %v\n", err)
|
||||
}
|
||||
|
||||
// Provision .claude/commands/ with standard slash commands (e.g., /handoff)
|
||||
// This ensures polecats have Gas Town utilities even if source repo lacks them.
|
||||
if err := templates.ProvisionCommands(polecatPath); err != nil {
|
||||
// Non-fatal - polecat can still work, warn but don't fail
|
||||
fmt.Printf("Warning: could not provision slash commands: %v\n", err)
|
||||
}
|
||||
// NOTE: Slash commands (.claude/commands/) are provisioned at town level by gt install.
|
||||
// All agents inherit them via Claude's directory traversal - no per-workspace copies needed.
|
||||
|
||||
// Create agent bead for ZFC compliance (self-report state).
|
||||
// State starts as "spawning" - will be updated to "working" when Claude starts.
|
||||
@@ -451,13 +442,21 @@ func (m *Manager) RecreateWithOptions(name string, force bool, opts AddOptions)
|
||||
// Fetch latest from origin to ensure we have fresh commits (non-fatal: may be offline)
|
||||
_ = repoGit.Fetch("origin")
|
||||
|
||||
// Create fresh worktree with unique branch name
|
||||
// Determine the start point for the new worktree
|
||||
// Use origin/<default-branch> to ensure we start from latest fetched commits
|
||||
defaultBranch := "main"
|
||||
if rigCfg, err := rig.LoadRigConfig(m.rig.Path); err == nil && rigCfg.DefaultBranch != "" {
|
||||
defaultBranch = rigCfg.DefaultBranch
|
||||
}
|
||||
startPoint := fmt.Sprintf("origin/%s", defaultBranch)
|
||||
|
||||
// Create fresh worktree with unique branch name, starting from origin's default branch
|
||||
// Old branches are left behind - they're ephemeral (never pushed to origin)
|
||||
// and will be cleaned up by garbage collection
|
||||
// Use base36 encoding for shorter branch names (8 chars vs 13 digits)
|
||||
branchName := fmt.Sprintf("polecat/%s-%s", name, strconv.FormatInt(time.Now().UnixMilli(), 36))
|
||||
if err := repoGit.WorktreeAdd(polecatPath, branchName); err != nil {
|
||||
return nil, fmt.Errorf("creating fresh worktree: %w", err)
|
||||
if err := repoGit.WorktreeAddFromRef(polecatPath, branchName, startPoint); err != nil {
|
||||
return nil, fmt.Errorf("creating fresh worktree from %s: %w", startPoint, err)
|
||||
}
|
||||
|
||||
// NOTE: We intentionally do NOT write to CLAUDE.md here.
|
||||
@@ -468,10 +467,7 @@ func (m *Manager) RecreateWithOptions(name string, force bool, opts AddOptions)
|
||||
fmt.Printf("Warning: could not set up shared beads: %v\n", err)
|
||||
}
|
||||
|
||||
// Provision .claude/commands/ with standard slash commands (e.g., /handoff)
|
||||
if err := templates.ProvisionCommands(polecatPath); err != nil {
|
||||
fmt.Printf("Warning: could not provision slash commands: %v\n", err)
|
||||
}
|
||||
// NOTE: Slash commands inherited from town level - no per-workspace copies needed.
|
||||
|
||||
// Create fresh agent bead for ZFC compliance
|
||||
// HookBead is set atomically at recreation time if provided.
|
||||
|
||||
Reference in New Issue
Block a user