fix(sling): auto-apply mol-polecat-work (#288) and fix wisp orphan lifecycle bug (#842) Fixes the formula-on-bead pattern to hook the base bead instead of the wisp: - Auto-apply mol-polecat-work when slinging bare beads to polecats - Hook BASE bead with attached_molecule pointing to wisp - gt done now closes attached molecule before closing hooked bead - Convoys complete properly when work finishes Fixes #288, #842, #858
This commit is contained in:
@@ -9,9 +9,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/steveyegge/gastown/internal/beads"
|
||||
"github.com/steveyegge/gastown/internal/config"
|
||||
"github.com/steveyegge/gastown/internal/constants"
|
||||
"github.com/steveyegge/gastown/internal/style"
|
||||
"github.com/steveyegge/gastown/internal/tmux"
|
||||
"github.com/steveyegge/gastown/internal/workspace"
|
||||
)
|
||||
@@ -461,57 +459,86 @@ func isPolecatTarget(target string) bool {
|
||||
return len(parts) >= 3 && parts[1] == "polecats"
|
||||
}
|
||||
|
||||
// attachPolecatWorkMolecule attaches the mol-polecat-work molecule to a polecat's agent bead.
|
||||
// This ensures all polecats have the standard work molecule attached for guidance.
|
||||
// The molecule is attached by storing it in the agent bead's description using attachment fields.
|
||||
//
|
||||
// Per issue #288: gt sling should auto-attach mol-polecat-work when slinging to polecats.
|
||||
func attachPolecatWorkMolecule(targetAgent, hookWorkDir, townRoot string) error {
|
||||
// Parse the polecat name from targetAgent (format: "rig/polecats/name")
|
||||
parts := strings.Split(targetAgent, "/")
|
||||
if len(parts) != 3 || parts[1] != "polecats" {
|
||||
return fmt.Errorf("invalid polecat agent format: %s", targetAgent)
|
||||
}
|
||||
rigName := parts[0]
|
||||
polecatName := parts[2]
|
||||
|
||||
// Get the polecat's agent bead ID
|
||||
// Format: "<prefix>-<rig>-polecat-<name>" (e.g., "gt-gastown-polecat-Toast")
|
||||
prefix := config.GetRigPrefix(townRoot, rigName)
|
||||
agentBeadID := beads.PolecatBeadIDWithPrefix(prefix, rigName, polecatName)
|
||||
|
||||
// Resolve the rig directory for running bd commands.
|
||||
// Use ResolveHookDir to ensure we run bd from the correct rig directory
|
||||
// (not from the polecat's worktree, which doesn't have a .beads directory).
|
||||
// This fixes issue #197: polecat fails to hook when slinging with molecule.
|
||||
rigDir := beads.ResolveHookDir(townRoot, prefix+"-"+polecatName, hookWorkDir)
|
||||
|
||||
b := beads.New(rigDir)
|
||||
|
||||
// Check if molecule is already attached (avoid duplicate attach)
|
||||
attachment, err := b.GetAttachment(agentBeadID)
|
||||
if err == nil && attachment != nil && attachment.AttachedMolecule != "" {
|
||||
// Already has a molecule attached - skip
|
||||
return nil
|
||||
}
|
||||
|
||||
// Cook the mol-polecat-work formula to ensure the proto exists
|
||||
// This is safe to run multiple times - cooking is idempotent
|
||||
cookCmd := exec.Command("bd", "--no-daemon", "cook", "mol-polecat-work")
|
||||
cookCmd.Dir = rigDir
|
||||
cookCmd.Stderr = os.Stderr
|
||||
if err := cookCmd.Run(); err != nil {
|
||||
return fmt.Errorf("cooking mol-polecat-work formula: %w", err)
|
||||
}
|
||||
|
||||
// Attach the molecule to the polecat's agent bead
|
||||
// The molecule ID is the formula name "mol-polecat-work"
|
||||
moleculeID := "mol-polecat-work"
|
||||
_, err = b.AttachMolecule(agentBeadID, moleculeID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("attaching molecule %s to %s: %w", moleculeID, agentBeadID, err)
|
||||
}
|
||||
|
||||
fmt.Printf("%s Attached %s to %s\n", style.Bold.Render("✓"), moleculeID, agentBeadID)
|
||||
return nil
|
||||
// FormulaOnBeadResult contains the result of instantiating a formula on a bead.
|
||||
type FormulaOnBeadResult struct {
|
||||
WispRootID string // The wisp root ID (compound root after bonding)
|
||||
BeadToHook string // The bead ID to hook (BASE bead, not wisp - lifecycle fix)
|
||||
}
|
||||
|
||||
// InstantiateFormulaOnBead creates a wisp from a formula, bonds it to a bead.
|
||||
// This is the formula-on-bead pattern used by issue #288 for auto-applying mol-polecat-work.
|
||||
//
|
||||
// Parameters:
|
||||
// - formulaName: the formula to instantiate (e.g., "mol-polecat-work")
|
||||
// - beadID: the base bead to bond the wisp to
|
||||
// - title: the bead title (used for --var feature=<title>)
|
||||
// - hookWorkDir: working directory for bd commands (polecat's worktree)
|
||||
// - townRoot: the town root directory
|
||||
// - skipCook: if true, skip cooking (for batch mode optimization where cook happens once)
|
||||
//
|
||||
// Returns the wisp root ID which should be hooked.
|
||||
func InstantiateFormulaOnBead(formulaName, beadID, title, hookWorkDir, townRoot string, skipCook bool) (*FormulaOnBeadResult, error) {
|
||||
// Route bd mutations (wisp/bond) to the correct beads context for the target bead.
|
||||
formulaWorkDir := beads.ResolveHookDir(townRoot, beadID, hookWorkDir)
|
||||
|
||||
// Step 1: Cook the formula (ensures proto exists)
|
||||
if !skipCook {
|
||||
cookCmd := exec.Command("bd", "--no-daemon", "cook", formulaName)
|
||||
cookCmd.Dir = formulaWorkDir
|
||||
cookCmd.Stderr = os.Stderr
|
||||
if err := cookCmd.Run(); err != nil {
|
||||
return nil, fmt.Errorf("cooking formula %s: %w", formulaName, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Step 2: Create wisp with feature and issue variables from bead
|
||||
featureVar := fmt.Sprintf("feature=%s", title)
|
||||
issueVar := fmt.Sprintf("issue=%s", beadID)
|
||||
wispArgs := []string{"--no-daemon", "mol", "wisp", formulaName, "--var", featureVar, "--var", issueVar, "--json"}
|
||||
wispCmd := exec.Command("bd", wispArgs...)
|
||||
wispCmd.Dir = formulaWorkDir
|
||||
wispCmd.Env = append(os.Environ(), "GT_ROOT="+townRoot)
|
||||
wispCmd.Stderr = os.Stderr
|
||||
wispOut, err := wispCmd.Output()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("creating wisp for formula %s: %w", formulaName, err)
|
||||
}
|
||||
|
||||
// Parse wisp output to get the root ID
|
||||
wispRootID, err := parseWispIDFromJSON(wispOut)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parsing wisp output: %w", err)
|
||||
}
|
||||
|
||||
// Step 3: Bond wisp to original bead (creates compound)
|
||||
bondArgs := []string{"--no-daemon", "mol", "bond", wispRootID, beadID, "--json"}
|
||||
bondCmd := exec.Command("bd", bondArgs...)
|
||||
bondCmd.Dir = formulaWorkDir
|
||||
bondCmd.Stderr = os.Stderr
|
||||
bondOut, err := bondCmd.Output()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("bonding formula to bead: %w", err)
|
||||
}
|
||||
|
||||
// Parse bond output - the wisp root becomes the compound root
|
||||
var bondResult struct {
|
||||
RootID string `json:"root_id"`
|
||||
}
|
||||
if err := json.Unmarshal(bondOut, &bondResult); err == nil && bondResult.RootID != "" {
|
||||
wispRootID = bondResult.RootID
|
||||
}
|
||||
|
||||
return &FormulaOnBeadResult{
|
||||
WispRootID: wispRootID,
|
||||
BeadToHook: beadID, // Hook the BASE bead (lifecycle fix: wisp is attached_molecule)
|
||||
}, nil
|
||||
}
|
||||
|
||||
// CookFormula cooks a formula to ensure its proto exists.
|
||||
// This is useful for batch mode where we cook once before processing multiple beads.
|
||||
func CookFormula(formulaName, workDir string) error {
|
||||
cookCmd := exec.Command("bd", "--no-daemon", "cook", formulaName)
|
||||
cookCmd.Dir = workDir
|
||||
cookCmd.Stderr = os.Stderr
|
||||
return cookCmd.Run()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user