feat(polecat): add repo path to worktrees for LLM ergonomics (GH#283)
Changes polecat worktree structure from: polecats/<name>/ to: polecats/<name>/<rigname>/ This gives Claude Code agents a recognizable directory name (e.g., tidepool/) in their cwd instead of just the polecat name, preventing confusion about which repo they are working in. Key changes: - Add clonePath() method to manager.go and session_manager.go for the actual git worktree path, keeping polecatDir() for existence checks - Update Add(), RepairWorktree(), Remove() to use new structure - Update daemon lifecycle and restart code for new paths - Update witness handlers to detect both structures - Update doctor checks (rig_check, branch_check, config_check, claude_settings_check) for backward compatibility - All code includes fallback to old structure for existing polecats Fixes #283 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
committed by
Steve Yegge
parent
c8c97fdf64
commit
9b2f4a7652
@@ -579,7 +579,7 @@ func runPolecatSync(cmd *cobra.Command, args []string) error {
|
|||||||
polecatName = ""
|
polecatName = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
mgr, r, err := getPolecatManager(rigName)
|
mgr, _, err := getPolecatManager(rigName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -606,10 +606,15 @@ func runPolecatSync(cmd *cobra.Command, args []string) error {
|
|||||||
// Sync each polecat
|
// Sync each polecat
|
||||||
var syncErrors []string
|
var syncErrors []string
|
||||||
for _, name := range polecatsToSync {
|
for _, name := range polecatsToSync {
|
||||||
polecatDir := filepath.Join(r.Path, "polecats", name)
|
// Get polecat to get correct clone path (handles old vs new structure)
|
||||||
|
p, err := mgr.Get(name)
|
||||||
|
if err != nil {
|
||||||
|
syncErrors = append(syncErrors, fmt.Sprintf("%s: %v", name, err))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
// Check directory exists
|
// Check directory exists
|
||||||
if _, err := os.Stat(polecatDir); os.IsNotExist(err) {
|
if _, err := os.Stat(p.ClonePath); os.IsNotExist(err) {
|
||||||
syncErrors = append(syncErrors, fmt.Sprintf("%s: directory not found", name))
|
syncErrors = append(syncErrors, fmt.Sprintf("%s: directory not found", name))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -623,7 +628,7 @@ func runPolecatSync(cmd *cobra.Command, args []string) error {
|
|||||||
fmt.Printf("Syncing %s/%s...\n", rigName, name)
|
fmt.Printf("Syncing %s/%s...\n", rigName, name)
|
||||||
|
|
||||||
syncCmd := exec.Command("bd", syncArgs...)
|
syncCmd := exec.Command("bd", syncArgs...)
|
||||||
syncCmd.Dir = polecatDir
|
syncCmd.Dir = p.ClonePath
|
||||||
output, err := syncCmd.CombinedOutput()
|
output, err := syncCmd.CombinedOutput()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
syncErrors = append(syncErrors, fmt.Sprintf("%s: %v", name, err))
|
syncErrors = append(syncErrors, fmt.Sprintf("%s: %v", name, err))
|
||||||
|
|||||||
@@ -752,8 +752,14 @@ func (d *Daemon) restartPolecatSession(rigName, polecatName, sessionName string)
|
|||||||
return fmt.Errorf("cannot restart polecat: %s", reason)
|
return fmt.Errorf("cannot restart polecat: %s", reason)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine working directory
|
// Determine working directory (handle both new and old structures)
|
||||||
workDir := filepath.Join(d.config.TownRoot, rigName, "polecats", polecatName)
|
// New structure: polecats/<name>/<rigname>/
|
||||||
|
// Old structure: polecats/<name>/
|
||||||
|
workDir := filepath.Join(d.config.TownRoot, rigName, "polecats", polecatName, rigName)
|
||||||
|
if _, err := os.Stat(workDir); os.IsNotExist(err) {
|
||||||
|
// Fall back to old structure
|
||||||
|
workDir = filepath.Join(d.config.TownRoot, rigName, "polecats", polecatName)
|
||||||
|
}
|
||||||
|
|
||||||
// Verify the worktree exists
|
// Verify the worktree exists
|
||||||
if _, err := os.Stat(workDir); os.IsNotExist(err) {
|
if _, err := os.Stat(workDir); os.IsNotExist(err) {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package daemon
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -427,6 +428,12 @@ func (d *Daemon) getWorkDir(config *beads.RoleConfig, parsed *ParsedIdentity) st
|
|||||||
case "crew":
|
case "crew":
|
||||||
return filepath.Join(d.config.TownRoot, parsed.RigName, "crew", parsed.AgentName)
|
return filepath.Join(d.config.TownRoot, parsed.RigName, "crew", parsed.AgentName)
|
||||||
case "polecat":
|
case "polecat":
|
||||||
|
// New structure: polecats/<name>/<rigname>/ (for LLM ergonomics)
|
||||||
|
// Old structure: polecats/<name>/ (for backward compat)
|
||||||
|
newPath := filepath.Join(d.config.TownRoot, parsed.RigName, "polecats", parsed.AgentName, parsed.RigName)
|
||||||
|
if _, err := os.Stat(newPath); err == nil {
|
||||||
|
return newPath
|
||||||
|
}
|
||||||
return filepath.Join(d.config.TownRoot, parsed.RigName, "polecats", parsed.AgentName)
|
return filepath.Join(d.config.TownRoot, parsed.RigName, "polecats", parsed.AgentName)
|
||||||
default:
|
default:
|
||||||
return ""
|
return ""
|
||||||
|
|||||||
@@ -468,12 +468,20 @@ func (c *CloneDivergenceCheck) findAllClones(townRoot string) []string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add polecats
|
// Add polecats (handle both new and old structures)
|
||||||
|
// New structure: polecats/<name>/<rigname>/
|
||||||
|
// Old structure: polecats/<name>/
|
||||||
|
rigName := entry.Name()
|
||||||
polecatsPath := filepath.Join(rigPath, "polecats")
|
polecatsPath := filepath.Join(rigPath, "polecats")
|
||||||
if polecatEntries, err := os.ReadDir(polecatsPath); err == nil {
|
if polecatEntries, err := os.ReadDir(polecatsPath); err == nil {
|
||||||
for _, polecat := range polecatEntries {
|
for _, polecat := range polecatEntries {
|
||||||
if polecat.IsDir() && !strings.HasPrefix(polecat.Name(), ".") {
|
if polecat.IsDir() && !strings.HasPrefix(polecat.Name(), ".") {
|
||||||
path := filepath.Join(polecatsPath, polecat.Name())
|
// Try new structure first
|
||||||
|
path := filepath.Join(polecatsPath, polecat.Name(), rigName)
|
||||||
|
if !c.isGitRepo(path) {
|
||||||
|
// Fall back to old structure
|
||||||
|
path = filepath.Join(polecatsPath, polecat.Name())
|
||||||
|
}
|
||||||
if c.isGitRepo(path) {
|
if c.isGitRepo(path) {
|
||||||
clones = append(clones, path)
|
clones = append(clones, path)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -288,15 +288,23 @@ func (c *ClaudeSettingsCheck) findSettingsFiles(townRoot string) []staleSettings
|
|||||||
if !pcEntry.IsDir() || pcEntry.Name() == ".claude" {
|
if !pcEntry.IsDir() || pcEntry.Name() == ".claude" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
pcWrongSettings := filepath.Join(polecatsDir, pcEntry.Name(), ".claude", "settings.json")
|
// Check for wrong settings in both structures:
|
||||||
if fileExists(pcWrongSettings) {
|
// Old structure: polecats/<name>/.claude/settings.json
|
||||||
files = append(files, staleSettingsInfo{
|
// New structure: polecats/<name>/<rigname>/.claude/settings.json
|
||||||
path: pcWrongSettings,
|
wrongPaths := []string{
|
||||||
agentType: "polecat",
|
filepath.Join(polecatsDir, pcEntry.Name(), ".claude", "settings.json"),
|
||||||
rigName: rigName,
|
filepath.Join(polecatsDir, pcEntry.Name(), rigName, ".claude", "settings.json"),
|
||||||
sessionName: fmt.Sprintf("gt-%s-%s", rigName, pcEntry.Name()),
|
}
|
||||||
wrongLocation: true,
|
for _, pcWrongSettings := range wrongPaths {
|
||||||
})
|
if fileExists(pcWrongSettings) {
|
||||||
|
files = append(files, staleSettingsInfo{
|
||||||
|
path: pcWrongSettings,
|
||||||
|
agentType: "polecat",
|
||||||
|
rigName: rigName,
|
||||||
|
sessionName: fmt.Sprintf("gt-%s-%s", rigName, pcEntry.Name()),
|
||||||
|
wrongLocation: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -460,14 +460,24 @@ func (c *SessionHookCheck) findSettingsFiles(townRoot string) []string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Polecats
|
// Polecats (handle both new and old structures)
|
||||||
|
// New structure: polecats/<name>/<rigname>/.claude/settings.json
|
||||||
|
// Old structure: polecats/<name>/.claude/settings.json
|
||||||
|
rigName := filepath.Base(rig)
|
||||||
polecatsPath := filepath.Join(rig, "polecats")
|
polecatsPath := filepath.Join(rig, "polecats")
|
||||||
if polecatEntries, err := os.ReadDir(polecatsPath); err == nil {
|
if polecatEntries, err := os.ReadDir(polecatsPath); err == nil {
|
||||||
for _, polecat := range polecatEntries {
|
for _, polecat := range polecatEntries {
|
||||||
if polecat.IsDir() && !strings.HasPrefix(polecat.Name(), ".") {
|
if polecat.IsDir() && !strings.HasPrefix(polecat.Name(), ".") {
|
||||||
polecatSettings := filepath.Join(polecatsPath, polecat.Name(), ".claude", "settings.json")
|
// Try new structure first
|
||||||
|
polecatSettings := filepath.Join(polecatsPath, polecat.Name(), rigName, ".claude", "settings.json")
|
||||||
if _, err := os.Stat(polecatSettings); err == nil {
|
if _, err := os.Stat(polecatSettings); err == nil {
|
||||||
files = append(files, polecatSettings)
|
files = append(files, polecatSettings)
|
||||||
|
} else {
|
||||||
|
// Fall back to old structure
|
||||||
|
polecatSettings = filepath.Join(polecatsPath, polecat.Name(), ".claude", "settings.json")
|
||||||
|
if _, err := os.Stat(polecatSettings); err == nil {
|
||||||
|
files = append(files, polecatSettings)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -698,14 +698,24 @@ func (c *PolecatClonesValidCheck) Run(ctx *CheckContext) *CheckResult {
|
|||||||
var warnings []string
|
var warnings []string
|
||||||
validCount := 0
|
validCount := 0
|
||||||
|
|
||||||
|
// Get rig name for new structure path detection
|
||||||
|
rigName := ctx.RigName
|
||||||
|
|
||||||
for _, entry := range entries {
|
for _, entry := range entries {
|
||||||
if !entry.IsDir() || strings.HasPrefix(entry.Name(), ".") {
|
if !entry.IsDir() || strings.HasPrefix(entry.Name(), ".") {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
polecatPath := filepath.Join(polecatsDir, entry.Name())
|
|
||||||
polecatName := entry.Name()
|
polecatName := entry.Name()
|
||||||
|
|
||||||
|
// Determine worktree path (handle both new and old structures)
|
||||||
|
// New structure: polecats/<name>/<rigname>/
|
||||||
|
// Old structure: polecats/<name>/
|
||||||
|
polecatPath := filepath.Join(polecatsDir, polecatName, rigName)
|
||||||
|
if _, err := os.Stat(polecatPath); os.IsNotExist(err) {
|
||||||
|
polecatPath = filepath.Join(polecatsDir, polecatName)
|
||||||
|
}
|
||||||
|
|
||||||
// Check if it's a git clone
|
// Check if it's a git clone
|
||||||
gitPath := filepath.Join(polecatPath, ".git")
|
gitPath := filepath.Join(polecatPath, ".git")
|
||||||
if _, err := os.Stat(gitPath); os.IsNotExist(err) {
|
if _, err := os.Stat(gitPath); os.IsNotExist(err) {
|
||||||
|
|||||||
@@ -178,11 +178,36 @@ func (m *Manager) repoBase() (*git.Git, error) {
|
|||||||
return git.NewGit(mayorPath), nil
|
return git.NewGit(mayorPath), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// polecatDir returns the directory for a polecat.
|
// polecatDir returns the parent directory for a polecat.
|
||||||
|
// This is polecats/<name>/ - the polecat's home directory.
|
||||||
func (m *Manager) polecatDir(name string) string {
|
func (m *Manager) polecatDir(name string) string {
|
||||||
return filepath.Join(m.rig.Path, "polecats", name)
|
return filepath.Join(m.rig.Path, "polecats", name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// clonePath returns the path where the git worktree lives.
|
||||||
|
// New structure: polecats/<name>/<rigname>/ - gives LLMs recognizable repo context.
|
||||||
|
// Falls back to old structure: polecats/<name>/ for backward compatibility.
|
||||||
|
func (m *Manager) clonePath(name string) string {
|
||||||
|
// New structure: polecats/<name>/<rigname>/
|
||||||
|
newPath := filepath.Join(m.rig.Path, "polecats", name, m.rig.Name)
|
||||||
|
if info, err := os.Stat(newPath); err == nil && info.IsDir() {
|
||||||
|
return newPath
|
||||||
|
}
|
||||||
|
|
||||||
|
// Old structure: polecats/<name>/ (backward compat)
|
||||||
|
oldPath := filepath.Join(m.rig.Path, "polecats", name)
|
||||||
|
if info, err := os.Stat(oldPath); err == nil && info.IsDir() {
|
||||||
|
// Check if this is actually a git worktree (has .git file or dir)
|
||||||
|
gitPath := filepath.Join(oldPath, ".git")
|
||||||
|
if _, err := os.Stat(gitPath); err == nil {
|
||||||
|
return oldPath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default to new structure for new polecats
|
||||||
|
return newPath
|
||||||
|
}
|
||||||
|
|
||||||
// exists checks if a polecat exists.
|
// exists checks if a polecat exists.
|
||||||
func (m *Manager) exists(name string) bool {
|
func (m *Manager) exists(name string) bool {
|
||||||
_, err := os.Stat(m.polecatDir(name))
|
_, err := os.Stat(m.polecatDir(name))
|
||||||
@@ -214,15 +239,18 @@ func (m *Manager) AddWithOptions(name string, opts AddOptions) (*Polecat, error)
|
|||||||
return nil, ErrPolecatExists
|
return nil, ErrPolecatExists
|
||||||
}
|
}
|
||||||
|
|
||||||
polecatPath := m.polecatDir(name)
|
// New structure: polecats/<name>/<rigname>/ for LLM ergonomics
|
||||||
|
// The polecat's home dir is polecats/<name>/, worktree is polecats/<name>/<rigname>/
|
||||||
|
polecatDir := m.polecatDir(name)
|
||||||
|
clonePath := filepath.Join(polecatDir, m.rig.Name)
|
||||||
|
|
||||||
// Unique branch per run - prevents drift from stale branches
|
// Unique branch per run - prevents drift from stale branches
|
||||||
// Use base36 encoding for shorter branch names (8 chars vs 13 digits)
|
// 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))
|
branchName := fmt.Sprintf("polecat/%s-%s", name, strconv.FormatInt(time.Now().UnixMilli(), 36))
|
||||||
|
|
||||||
// Create polecats directory if needed
|
// Create polecat directory (polecats/<name>/)
|
||||||
polecatsDir := filepath.Join(m.rig.Path, "polecats")
|
if err := os.MkdirAll(polecatDir, 0755); err != nil {
|
||||||
if err := os.MkdirAll(polecatsDir, 0755); err != nil {
|
return nil, fmt.Errorf("creating polecat dir: %w", err)
|
||||||
return nil, fmt.Errorf("creating polecats dir: %w", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the repo base (bare repo or mayor/rig)
|
// Get the repo base (bare repo or mayor/rig)
|
||||||
@@ -233,7 +261,8 @@ func (m *Manager) AddWithOptions(name string, opts AddOptions) (*Polecat, error)
|
|||||||
|
|
||||||
// Always create fresh branch - unique name guarantees no collision
|
// Always create fresh branch - unique name guarantees no collision
|
||||||
// git worktree add -b polecat/<name>-<timestamp> <path>
|
// git worktree add -b polecat/<name>-<timestamp> <path>
|
||||||
if err := repoGit.WorktreeAdd(polecatPath, branchName); err != nil {
|
// Worktree goes in polecats/<name>/<rigname>/ for LLM ergonomics
|
||||||
|
if err := repoGit.WorktreeAdd(clonePath, branchName); err != nil {
|
||||||
return nil, fmt.Errorf("creating worktree: %w", err)
|
return nil, fmt.Errorf("creating worktree: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -244,7 +273,7 @@ func (m *Manager) AddWithOptions(name string, opts AddOptions) (*Polecat, error)
|
|||||||
|
|
||||||
// Set up shared beads: polecat uses rig's .beads via redirect file.
|
// Set up shared beads: polecat uses rig's .beads via redirect file.
|
||||||
// This eliminates git sync overhead - all polecats share one database.
|
// This eliminates git sync overhead - all polecats share one database.
|
||||||
if err := m.setupSharedBeads(polecatPath); err != nil {
|
if err := m.setupSharedBeads(clonePath); err != nil {
|
||||||
// Non-fatal - polecat can still work with local beads
|
// Non-fatal - polecat can still work with local beads
|
||||||
// Log warning but don't fail the spawn
|
// Log warning but don't fail the spawn
|
||||||
fmt.Printf("Warning: could not set up shared beads: %v\n", err)
|
fmt.Printf("Warning: could not set up shared beads: %v\n", err)
|
||||||
@@ -283,7 +312,7 @@ func (m *Manager) AddWithOptions(name string, opts AddOptions) (*Polecat, error)
|
|||||||
Name: name,
|
Name: name,
|
||||||
Rig: m.rig.Name,
|
Rig: m.rig.Name,
|
||||||
State: StateWorking, // Transient model: polecat spawns with work
|
State: StateWorking, // Transient model: polecat spawns with work
|
||||||
ClonePath: polecatPath,
|
ClonePath: clonePath,
|
||||||
Branch: branchName,
|
Branch: branchName,
|
||||||
CreatedAt: now,
|
CreatedAt: now,
|
||||||
UpdatedAt: now,
|
UpdatedAt: now,
|
||||||
@@ -310,7 +339,10 @@ func (m *Manager) RemoveWithOptions(name string, force, nuclear bool) error {
|
|||||||
return ErrPolecatNotFound
|
return ErrPolecatNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
polecatPath := m.polecatDir(name)
|
// Clone path is where the git worktree lives (new or old structure)
|
||||||
|
clonePath := m.clonePath(name)
|
||||||
|
// Polecat dir is the parent directory (polecats/<name>/)
|
||||||
|
polecatDir := m.polecatDir(name)
|
||||||
|
|
||||||
// Check for uncommitted work unless bypassed
|
// Check for uncommitted work unless bypassed
|
||||||
if !nuclear {
|
if !nuclear {
|
||||||
@@ -325,7 +357,7 @@ func (m *Manager) RemoveWithOptions(name string, force, nuclear bool) error {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Fallback path: Check git directly (for polecats that haven't reported yet)
|
// Fallback path: Check git directly (for polecats that haven't reported yet)
|
||||||
polecatGit := git.NewGit(polecatPath)
|
polecatGit := git.NewGit(clonePath)
|
||||||
status, err := polecatGit.CheckUncommittedWork()
|
status, err := polecatGit.CheckUncommittedWork()
|
||||||
if err == nil && !status.Clean() {
|
if err == nil && !status.Clean() {
|
||||||
// For backward compatibility: force only bypasses uncommitted changes, not stashes/unpushed
|
// For backward compatibility: force only bypasses uncommitted changes, not stashes/unpushed
|
||||||
@@ -345,18 +377,24 @@ func (m *Manager) RemoveWithOptions(name string, force, nuclear bool) error {
|
|||||||
repoGit, err := m.repoBase()
|
repoGit, err := m.repoBase()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Fall back to direct removal if repo base not found
|
// Fall back to direct removal if repo base not found
|
||||||
return os.RemoveAll(polecatPath)
|
return os.RemoveAll(polecatDir)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to remove as a worktree first (use force flag for worktree removal too)
|
// Try to remove as a worktree first (use force flag for worktree removal too)
|
||||||
if err := repoGit.WorktreeRemove(polecatPath, force); err != nil {
|
if err := repoGit.WorktreeRemove(clonePath, force); err != nil {
|
||||||
// Fall back to direct removal if worktree removal fails
|
// Fall back to direct removal if worktree removal fails
|
||||||
// (e.g., if this is an old-style clone, not a worktree)
|
// (e.g., if this is an old-style clone, not a worktree)
|
||||||
if removeErr := os.RemoveAll(polecatPath); removeErr != nil {
|
if removeErr := os.RemoveAll(clonePath); removeErr != nil {
|
||||||
return fmt.Errorf("removing polecat dir: %w", removeErr)
|
return fmt.Errorf("removing clone path: %w", removeErr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Also remove the parent polecat directory if it's now empty
|
||||||
|
// (for new structure: polecats/<name>/ contains only polecats/<name>/<rigname>/)
|
||||||
|
if polecatDir != clonePath {
|
||||||
|
_ = os.Remove(polecatDir) // Non-fatal: only removes if empty
|
||||||
|
}
|
||||||
|
|
||||||
// Prune any stale worktree entries (non-fatal: cleanup only)
|
// Prune any stale worktree entries (non-fatal: cleanup only)
|
||||||
_ = repoGit.WorktreePrune()
|
_ = repoGit.WorktreePrune()
|
||||||
|
|
||||||
@@ -419,13 +457,19 @@ func (m *Manager) RepairWorktree(name string, force bool) (*Polecat, error) {
|
|||||||
// RepairWorktreeWithOptions repairs a stale polecat and creates a fresh worktree with options.
|
// RepairWorktreeWithOptions repairs a stale polecat and creates a fresh worktree with options.
|
||||||
// This is NOT for normal operation - see RepairWorktree for context.
|
// This is NOT for normal operation - see RepairWorktree for context.
|
||||||
// Allows setting hook_bead atomically at repair time.
|
// Allows setting hook_bead atomically at repair time.
|
||||||
|
// After repair, uses new structure: polecats/<name>/<rigname>/
|
||||||
func (m *Manager) RepairWorktreeWithOptions(name string, force bool, opts AddOptions) (*Polecat, error) {
|
func (m *Manager) RepairWorktreeWithOptions(name string, force bool, opts AddOptions) (*Polecat, error) {
|
||||||
if !m.exists(name) {
|
if !m.exists(name) {
|
||||||
return nil, ErrPolecatNotFound
|
return nil, ErrPolecatNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
polecatPath := m.polecatDir(name)
|
// Get the old clone path (may be old or new structure)
|
||||||
polecatGit := git.NewGit(polecatPath)
|
oldClonePath := m.clonePath(name)
|
||||||
|
polecatGit := git.NewGit(oldClonePath)
|
||||||
|
|
||||||
|
// New clone path uses new structure
|
||||||
|
polecatDir := m.polecatDir(name)
|
||||||
|
newClonePath := filepath.Join(polecatDir, m.rig.Name)
|
||||||
|
|
||||||
// Get the repo base (bare repo or mayor/rig)
|
// Get the repo base (bare repo or mayor/rig)
|
||||||
repoGit, err := m.repoBase()
|
repoGit, err := m.repoBase()
|
||||||
@@ -449,11 +493,11 @@ func (m *Manager) RepairWorktreeWithOptions(name string, force bool, opts AddOpt
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove the worktree (use force for git worktree removal)
|
// Remove the old worktree (use force for git worktree removal)
|
||||||
if err := repoGit.WorktreeRemove(polecatPath, true); err != nil {
|
if err := repoGit.WorktreeRemove(oldClonePath, true); err != nil {
|
||||||
// Fall back to direct removal
|
// Fall back to direct removal
|
||||||
if removeErr := os.RemoveAll(polecatPath); removeErr != nil {
|
if removeErr := os.RemoveAll(oldClonePath); removeErr != nil {
|
||||||
return nil, fmt.Errorf("removing polecat dir: %w", removeErr)
|
return nil, fmt.Errorf("removing old clone path: %w", removeErr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -463,6 +507,11 @@ func (m *Manager) RepairWorktreeWithOptions(name string, force bool, opts AddOpt
|
|||||||
// Fetch latest from origin to ensure we have fresh commits (non-fatal: may be offline)
|
// Fetch latest from origin to ensure we have fresh commits (non-fatal: may be offline)
|
||||||
_ = repoGit.Fetch("origin")
|
_ = repoGit.Fetch("origin")
|
||||||
|
|
||||||
|
// Ensure polecat directory exists for new structure
|
||||||
|
if err := os.MkdirAll(polecatDir, 0755); err != nil {
|
||||||
|
return nil, fmt.Errorf("creating polecat dir: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
// Determine the start point for the new worktree
|
// Determine the start point for the new worktree
|
||||||
// Use origin/<default-branch> to ensure we start from latest fetched commits
|
// Use origin/<default-branch> to ensure we start from latest fetched commits
|
||||||
defaultBranch := "main"
|
defaultBranch := "main"
|
||||||
@@ -476,7 +525,7 @@ func (m *Manager) RepairWorktreeWithOptions(name string, force bool, opts AddOpt
|
|||||||
// and will be cleaned up by garbage collection
|
// and will be cleaned up by garbage collection
|
||||||
// Use base36 encoding for shorter branch names (8 chars vs 13 digits)
|
// 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))
|
branchName := fmt.Sprintf("polecat/%s-%s", name, strconv.FormatInt(time.Now().UnixMilli(), 36))
|
||||||
if err := repoGit.WorktreeAddFromRef(polecatPath, branchName, startPoint); err != nil {
|
if err := repoGit.WorktreeAddFromRef(newClonePath, branchName, startPoint); err != nil {
|
||||||
return nil, fmt.Errorf("creating fresh worktree from %s: %w", startPoint, err)
|
return nil, fmt.Errorf("creating fresh worktree from %s: %w", startPoint, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -484,7 +533,7 @@ func (m *Manager) RepairWorktreeWithOptions(name string, force bool, opts AddOpt
|
|||||||
// Gas Town context is injected ephemerally via SessionStart hook (gt prime).
|
// Gas Town context is injected ephemerally via SessionStart hook (gt prime).
|
||||||
|
|
||||||
// Set up shared beads
|
// Set up shared beads
|
||||||
if err := m.setupSharedBeads(polecatPath); err != nil {
|
if err := m.setupSharedBeads(newClonePath); err != nil {
|
||||||
fmt.Printf("Warning: could not set up shared beads: %v\n", err)
|
fmt.Printf("Warning: could not set up shared beads: %v\n", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -514,7 +563,7 @@ func (m *Manager) RepairWorktreeWithOptions(name string, force bool, opts AddOpt
|
|||||||
Name: name,
|
Name: name,
|
||||||
Rig: m.rig.Name,
|
Rig: m.rig.Name,
|
||||||
State: StateWorking,
|
State: StateWorking,
|
||||||
ClonePath: polecatPath,
|
ClonePath: newClonePath,
|
||||||
Branch: branchName,
|
Branch: branchName,
|
||||||
CreatedAt: now,
|
CreatedAt: now,
|
||||||
UpdatedAt: now,
|
UpdatedAt: now,
|
||||||
@@ -690,10 +739,12 @@ func (m *Manager) ClearIssue(name string) error {
|
|||||||
// Transient polecats should always have work; no work means ready for Witness cleanup.
|
// Transient polecats should always have work; no work means ready for Witness cleanup.
|
||||||
// We don't interpret issue status (ZFC: Go is transport, not decision-maker).
|
// We don't interpret issue status (ZFC: Go is transport, not decision-maker).
|
||||||
func (m *Manager) loadFromBeads(name string) (*Polecat, error) {
|
func (m *Manager) loadFromBeads(name string) (*Polecat, error) {
|
||||||
polecatPath := m.polecatDir(name)
|
// Use clonePath which handles both new (polecats/<name>/<rigname>/)
|
||||||
|
// and old (polecats/<name>/) structures
|
||||||
|
clonePath := m.clonePath(name)
|
||||||
|
|
||||||
// Get actual branch from worktree (branches are now timestamped)
|
// Get actual branch from worktree (branches are now timestamped)
|
||||||
polecatGit := git.NewGit(polecatPath)
|
polecatGit := git.NewGit(clonePath)
|
||||||
branchName, err := polecatGit.CurrentBranch()
|
branchName, err := polecatGit.CurrentBranch()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Fall back to old format if we can't read the branch
|
// Fall back to old format if we can't read the branch
|
||||||
@@ -710,7 +761,7 @@ func (m *Manager) loadFromBeads(name string) (*Polecat, error) {
|
|||||||
Name: name,
|
Name: name,
|
||||||
Rig: m.rig.Name,
|
Rig: m.rig.Name,
|
||||||
State: StateWorking,
|
State: StateWorking,
|
||||||
ClonePath: polecatPath,
|
ClonePath: clonePath,
|
||||||
Branch: branchName,
|
Branch: branchName,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
@@ -728,7 +779,7 @@ func (m *Manager) loadFromBeads(name string) (*Polecat, error) {
|
|||||||
Name: name,
|
Name: name,
|
||||||
Rig: m.rig.Name,
|
Rig: m.rig.Name,
|
||||||
State: state,
|
State: state,
|
||||||
ClonePath: polecatPath,
|
ClonePath: clonePath,
|
||||||
Branch: branchName,
|
Branch: branchName,
|
||||||
Issue: issueID,
|
Issue: issueID,
|
||||||
}, nil
|
}, nil
|
||||||
|
|||||||
@@ -96,11 +96,36 @@ func (m *SessionManager) SessionName(polecat string) string {
|
|||||||
return fmt.Sprintf("gt-%s-%s", m.rig.Name, polecat)
|
return fmt.Sprintf("gt-%s-%s", m.rig.Name, polecat)
|
||||||
}
|
}
|
||||||
|
|
||||||
// polecatDir returns the working directory for a polecat.
|
// polecatDir returns the parent directory for a polecat.
|
||||||
|
// This is polecats/<name>/ - the polecat's home directory.
|
||||||
func (m *SessionManager) polecatDir(polecat string) string {
|
func (m *SessionManager) polecatDir(polecat string) string {
|
||||||
return filepath.Join(m.rig.Path, "polecats", polecat)
|
return filepath.Join(m.rig.Path, "polecats", polecat)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// clonePath returns the path where the git worktree lives.
|
||||||
|
// New structure: polecats/<name>/<rigname>/ - gives LLMs recognizable repo context.
|
||||||
|
// Falls back to old structure: polecats/<name>/ for backward compatibility.
|
||||||
|
func (m *SessionManager) clonePath(polecat string) string {
|
||||||
|
// New structure: polecats/<name>/<rigname>/
|
||||||
|
newPath := filepath.Join(m.rig.Path, "polecats", polecat, m.rig.Name)
|
||||||
|
if info, err := os.Stat(newPath); err == nil && info.IsDir() {
|
||||||
|
return newPath
|
||||||
|
}
|
||||||
|
|
||||||
|
// Old structure: polecats/<name>/ (backward compat)
|
||||||
|
oldPath := filepath.Join(m.rig.Path, "polecats", polecat)
|
||||||
|
if info, err := os.Stat(oldPath); err == nil && info.IsDir() {
|
||||||
|
// Check if this is actually a git worktree (has .git file or dir)
|
||||||
|
gitPath := filepath.Join(oldPath, ".git")
|
||||||
|
if _, err := os.Stat(gitPath); err == nil {
|
||||||
|
return oldPath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default to new structure for new polecats
|
||||||
|
return newPath
|
||||||
|
}
|
||||||
|
|
||||||
// hasPolecat checks if the polecat exists in this rig.
|
// hasPolecat checks if the polecat exists in this rig.
|
||||||
func (m *SessionManager) hasPolecat(polecat string) bool {
|
func (m *SessionManager) hasPolecat(polecat string) bool {
|
||||||
polecatPath := m.polecatDir(polecat)
|
polecatPath := m.polecatDir(polecat)
|
||||||
@@ -131,7 +156,7 @@ func (m *SessionManager) Start(polecat string, opts SessionStartOptions) error {
|
|||||||
// Determine working directory
|
// Determine working directory
|
||||||
workDir := opts.WorkDir
|
workDir := opts.WorkDir
|
||||||
if workDir == "" {
|
if workDir == "" {
|
||||||
workDir = m.polecatDir(polecat)
|
workDir = m.clonePath(polecat)
|
||||||
}
|
}
|
||||||
|
|
||||||
runtimeConfig := config.LoadRuntimeConfig(m.rig.Path)
|
runtimeConfig := config.LoadRuntimeConfig(m.rig.Path)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package witness
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -760,8 +761,14 @@ func verifyCommitOnMain(workDir, rigName, polecatName string) (bool, error) {
|
|||||||
defaultBranch = rigCfg.DefaultBranch
|
defaultBranch = rigCfg.DefaultBranch
|
||||||
}
|
}
|
||||||
|
|
||||||
// Construct polecat path: <townRoot>/<rigName>/polecats/<polecatName>
|
// Construct polecat path, handling both new and old structures
|
||||||
polecatPath := filepath.Join(townRoot, rigName, "polecats", polecatName)
|
// New structure: polecats/<name>/<rigname>/
|
||||||
|
// Old structure: polecats/<name>/
|
||||||
|
polecatPath := filepath.Join(townRoot, rigName, "polecats", polecatName, rigName)
|
||||||
|
if _, err := os.Stat(polecatPath); os.IsNotExist(err) {
|
||||||
|
// Fall back to old structure
|
||||||
|
polecatPath = filepath.Join(townRoot, rigName, "polecats", polecatName)
|
||||||
|
}
|
||||||
|
|
||||||
// Get git for the polecat worktree
|
// Get git for the polecat worktree
|
||||||
g := git.NewGit(polecatPath)
|
g := git.NewGit(polecatPath)
|
||||||
|
|||||||
Reference in New Issue
Block a user