feat(polecat): add beads environment and sync for worktrees
CRITICAL FIX: Polecats (git worktrees) now get proper beads environment: - BEADS_DIR: Points to rig canonical beads directory - BEADS_NO_DAEMON=1: Prevents daemon from corrupting worktree beads - BEADS_AGENT_NAME: Identity for beads conflict resolution Also adds: - bd sync on spawn (before and after issue assignment) - bd sync on session stop (before killing) - gt polecat sync command for manual beads sync - Polecat role prompt explaining two-level beads architecture
This commit is contained in:
@@ -5,6 +5,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
@@ -149,6 +150,30 @@ Example:
|
||||
RunE: runPolecatReset,
|
||||
}
|
||||
|
||||
var polecatSyncCmd = &cobra.Command{
|
||||
Use: "sync <rig>/<polecat>",
|
||||
Short: "Sync beads for a polecat",
|
||||
Long: `Sync beads for a polecat's worktree.
|
||||
|
||||
Runs 'bd sync' in the polecat's worktree to push local beads changes
|
||||
to the shared sync branch and pull remote changes.
|
||||
|
||||
Use --all to sync all polecats in a rig.
|
||||
Use --from-main to only pull (no push).
|
||||
|
||||
Examples:
|
||||
gt polecat sync gastown/Toast
|
||||
gt polecat sync gastown --all
|
||||
gt polecat sync gastown/Toast --from-main`,
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
RunE: runPolecatSync,
|
||||
}
|
||||
|
||||
var (
|
||||
polecatSyncAll bool
|
||||
polecatSyncFromMain bool
|
||||
)
|
||||
|
||||
func init() {
|
||||
// List flags
|
||||
polecatListCmd.Flags().BoolVar(&polecatListJSON, "json", false, "Output as JSON")
|
||||
@@ -157,6 +182,10 @@ func init() {
|
||||
// Remove flags
|
||||
polecatRemoveCmd.Flags().BoolVarP(&polecatForce, "force", "f", false, "Force removal, bypassing checks")
|
||||
|
||||
// Sync flags
|
||||
polecatSyncCmd.Flags().BoolVar(&polecatSyncAll, "all", false, "Sync all polecats in the rig")
|
||||
polecatSyncCmd.Flags().BoolVar(&polecatSyncFromMain, "from-main", false, "Pull only, no push")
|
||||
|
||||
// Add subcommands
|
||||
polecatCmd.AddCommand(polecatListCmd)
|
||||
polecatCmd.AddCommand(polecatAddCmd)
|
||||
@@ -165,6 +194,7 @@ func init() {
|
||||
polecatCmd.AddCommand(polecatSleepCmd)
|
||||
polecatCmd.AddCommand(polecatDoneCmd)
|
||||
polecatCmd.AddCommand(polecatResetCmd)
|
||||
polecatCmd.AddCommand(polecatSyncCmd)
|
||||
|
||||
rootCmd.AddCommand(polecatCmd)
|
||||
}
|
||||
@@ -467,3 +497,83 @@ func runPolecatReset(cmd *cobra.Command, args []string) error {
|
||||
fmt.Printf("%s Polecat %s has been reset to idle.\n", style.SuccessPrefix, polecatName)
|
||||
return nil
|
||||
}
|
||||
|
||||
func runPolecatSync(cmd *cobra.Command, args []string) error {
|
||||
if len(args) < 1 {
|
||||
return fmt.Errorf("rig or rig/polecat address required")
|
||||
}
|
||||
|
||||
// Parse address - could be "rig" or "rig/polecat"
|
||||
rigName, polecatName, err := parseAddress(args[0])
|
||||
if err != nil {
|
||||
// Might just be a rig name
|
||||
rigName = args[0]
|
||||
polecatName = ""
|
||||
}
|
||||
|
||||
mgr, r, err := getPolecatManager(rigName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Get list of polecats to sync
|
||||
var polecatsToSync []string
|
||||
if polecatSyncAll || polecatName == "" {
|
||||
polecats, err := mgr.List()
|
||||
if err != nil {
|
||||
return fmt.Errorf("listing polecats: %w", err)
|
||||
}
|
||||
for _, p := range polecats {
|
||||
polecatsToSync = append(polecatsToSync, p.Name)
|
||||
}
|
||||
} else {
|
||||
polecatsToSync = []string{polecatName}
|
||||
}
|
||||
|
||||
if len(polecatsToSync) == 0 {
|
||||
fmt.Println("No polecats to sync.")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Sync each polecat
|
||||
var syncErrors []string
|
||||
for _, name := range polecatsToSync {
|
||||
polecatDir := filepath.Join(r.Path, "polecats", name)
|
||||
|
||||
// Check directory exists
|
||||
if _, err := os.Stat(polecatDir); os.IsNotExist(err) {
|
||||
syncErrors = append(syncErrors, fmt.Sprintf("%s: directory not found", name))
|
||||
continue
|
||||
}
|
||||
|
||||
// Build sync command
|
||||
syncArgs := []string{"sync"}
|
||||
if polecatSyncFromMain {
|
||||
syncArgs = append(syncArgs, "--from-main")
|
||||
}
|
||||
|
||||
fmt.Printf("Syncing %s/%s...\n", rigName, name)
|
||||
|
||||
syncCmd := exec.Command("bd", syncArgs...)
|
||||
syncCmd.Dir = polecatDir
|
||||
output, err := syncCmd.CombinedOutput()
|
||||
if err != nil {
|
||||
syncErrors = append(syncErrors, fmt.Sprintf("%s: %v", name, err))
|
||||
if len(output) > 0 {
|
||||
fmt.Printf(" %s\n", style.Dim.Render(string(output)))
|
||||
}
|
||||
} else {
|
||||
fmt.Printf(" %s\n", style.Success.Render("✓ synced"))
|
||||
}
|
||||
}
|
||||
|
||||
if len(syncErrors) > 0 {
|
||||
fmt.Printf("\n%s Some syncs failed:\n", style.Warning.Render("Warning:"))
|
||||
for _, e := range syncErrors {
|
||||
fmt.Printf(" - %s\n", e)
|
||||
}
|
||||
return fmt.Errorf("%d sync(s) failed", len(syncErrors))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -187,6 +187,12 @@ func runSpawn(cmd *cobra.Command, args []string) error {
|
||||
// Beads operations use mayor/rig directory (rig-level beads)
|
||||
beadsPath := filepath.Join(r.Path, "mayor", "rig")
|
||||
|
||||
// Sync beads to ensure fresh state before spawn operations
|
||||
if err := syncBeads(beadsPath, true); err != nil {
|
||||
// Non-fatal - continue with possibly stale beads
|
||||
fmt.Printf("%s beads sync: %v\n", style.Dim.Render("Warning:"), err)
|
||||
}
|
||||
|
||||
// Handle molecule instantiation if specified
|
||||
if spawnMolecule != "" {
|
||||
b := beads.New(beadsPath)
|
||||
@@ -272,6 +278,12 @@ func runSpawn(cmd *cobra.Command, args []string) error {
|
||||
style.Bold.Render("✓"),
|
||||
assignmentID, rigName, polecatName)
|
||||
|
||||
// Sync beads to push assignment changes
|
||||
if err := syncBeads(beadsPath, false); err != nil {
|
||||
// Non-fatal warning
|
||||
fmt.Printf("%s beads push: %v\n", style.Dim.Render("Warning:"), err)
|
||||
}
|
||||
|
||||
// Stop here if --no-start
|
||||
if spawnNoStart {
|
||||
fmt.Printf("\n %s\n", style.Dim.Render("Use 'gt session start' to start the session"))
|
||||
@@ -461,6 +473,18 @@ func createBeadsTask(rigPath, message string) (*BeadsIssue, error) {
|
||||
return &issue, nil
|
||||
}
|
||||
|
||||
// syncBeads runs bd sync in the given directory.
|
||||
// This ensures beads state is fresh before spawn operations.
|
||||
func syncBeads(workDir string, fromMain bool) error {
|
||||
args := []string{"sync"}
|
||||
if fromMain {
|
||||
args = append(args, "--from-main")
|
||||
}
|
||||
cmd := exec.Command("bd", args...)
|
||||
cmd.Dir = workDir
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
// buildSpawnContext creates the initial context message for the polecat.
|
||||
func buildSpawnContext(issue *BeadsIssue, message string) string {
|
||||
var sb strings.Builder
|
||||
@@ -479,7 +503,14 @@ func buildSpawnContext(issue *BeadsIssue, message string) string {
|
||||
sb.WriteString(fmt.Sprintf("Task: %s\n", message))
|
||||
}
|
||||
|
||||
sb.WriteString("\nWork on this task. When complete, commit your changes and signal DONE.\n")
|
||||
sb.WriteString("\n## Workflow\n")
|
||||
sb.WriteString("1. Run `gt prime` to load polecat context\n")
|
||||
sb.WriteString("2. Run `bd sync --from-main` to get fresh beads\n")
|
||||
sb.WriteString("3. Work on your task, commit changes\n")
|
||||
sb.WriteString("4. Run `bd close <issue-id>` when done\n")
|
||||
sb.WriteString("5. Run `bd sync` to push beads changes\n")
|
||||
sb.WriteString("6. Push code: `git push origin HEAD`\n")
|
||||
sb.WriteString("7. Signal DONE with summary\n")
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -123,6 +124,14 @@ func (m *Manager) Start(polecat string, opts StartOptions) error {
|
||||
_ = m.tmux.SetEnvironment(sessionID, "GT_RIG", m.rig.Name)
|
||||
_ = m.tmux.SetEnvironment(sessionID, "GT_POLECAT", polecat)
|
||||
|
||||
// CRITICAL: Set beads environment for worktree polecats
|
||||
// Polecats share the rig's beads directory (in mayor/rig/.beads)
|
||||
// BEADS_NO_DAEMON=1 prevents daemon from committing to wrong branch
|
||||
beadsDir := filepath.Join(m.rig.Path, "mayor", "rig", ".beads")
|
||||
_ = m.tmux.SetEnvironment(sessionID, "BEADS_DIR", beadsDir)
|
||||
_ = m.tmux.SetEnvironment(sessionID, "BEADS_NO_DAEMON", "1")
|
||||
_ = m.tmux.SetEnvironment(sessionID, "BEADS_AGENT_NAME", fmt.Sprintf("%s/%s", m.rig.Name, polecat))
|
||||
|
||||
// Send initial command
|
||||
command := opts.Command
|
||||
if command == "" {
|
||||
@@ -157,6 +166,16 @@ func (m *Manager) Stop(polecat string, force bool) error {
|
||||
return ErrSessionNotFound
|
||||
}
|
||||
|
||||
// Sync beads before shutdown to preserve any changes
|
||||
// Run in the polecat's worktree directory
|
||||
if !force {
|
||||
polecatDir := m.polecatDir(polecat)
|
||||
if err := m.syncBeads(polecatDir); err != nil {
|
||||
// Non-fatal - log and continue with shutdown
|
||||
fmt.Printf("Warning: beads sync failed: %v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Try graceful shutdown first (unless forced)
|
||||
if !force {
|
||||
_ = m.tmux.SendKeysRaw(sessionID, "C-c") // Ctrl+C
|
||||
@@ -171,6 +190,13 @@ func (m *Manager) Stop(polecat string, force bool) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// syncBeads runs bd sync in the given directory.
|
||||
func (m *Manager) syncBeads(workDir string) error {
|
||||
cmd := exec.Command("bd", "sync")
|
||||
cmd.Dir = workDir
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
// IsRunning checks if a polecat session is active.
|
||||
func (m *Manager) IsRunning(polecat string) (bool, error) {
|
||||
sessionID := m.sessionName(polecat)
|
||||
|
||||
Reference in New Issue
Block a user