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:
@@ -1,7 +1,6 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
@@ -83,12 +82,13 @@ Batch Slinging:
|
||||
}
|
||||
|
||||
var (
|
||||
slingSubject string
|
||||
slingMessage string
|
||||
slingDryRun bool
|
||||
slingOnTarget string // --on flag: target bead when slinging a formula
|
||||
slingVars []string // --var flag: formula variables (key=value)
|
||||
slingArgs string // --args flag: natural language instructions for executor
|
||||
slingSubject string
|
||||
slingMessage string
|
||||
slingDryRun bool
|
||||
slingOnTarget string // --on flag: target bead when slinging a formula
|
||||
slingVars []string // --var flag: formula variables (key=value)
|
||||
slingArgs string // --args flag: natural language instructions for executor
|
||||
slingHookRawBead bool // --hook-raw-bead: hook raw bead without default formula (expert mode)
|
||||
|
||||
// Flags migrated for polecat spawning (used by sling for work assignment)
|
||||
slingCreate bool // --create: create polecat if it doesn't exist
|
||||
@@ -112,6 +112,7 @@ func init() {
|
||||
slingCmd.Flags().StringVar(&slingAccount, "account", "", "Claude Code account handle to use")
|
||||
slingCmd.Flags().StringVar(&slingAgent, "agent", "", "Override agent/runtime for this sling (e.g., claude, gemini, codex, or custom alias)")
|
||||
slingCmd.Flags().BoolVar(&slingNoConvoy, "no-convoy", false, "Skip auto-convoy creation for single-issue sling")
|
||||
slingCmd.Flags().BoolVar(&slingHookRawBead, "hook-raw-bead", false, "Hook raw bead without default formula (expert mode)")
|
||||
|
||||
rootCmd.AddCommand(slingCmd)
|
||||
}
|
||||
@@ -398,6 +399,14 @@ func runSling(cmd *cobra.Command, args []string) error {
|
||||
}
|
||||
}
|
||||
|
||||
// Issue #288: Auto-apply mol-polecat-work when slinging bare bead to polecat.
|
||||
// This ensures polecats get structured work guidance through formula-on-bead.
|
||||
// Use --hook-raw-bead to bypass for expert/debugging scenarios.
|
||||
if formulaName == "" && !slingHookRawBead && strings.Contains(targetAgent, "/polecats/") {
|
||||
formulaName = "mol-polecat-work"
|
||||
fmt.Printf(" Auto-applying %s for polecat work...\n", formulaName)
|
||||
}
|
||||
|
||||
if slingDryRun {
|
||||
if formulaName != "" {
|
||||
fmt.Printf("Would instantiate formula %s:\n", formulaName)
|
||||
@@ -425,71 +434,30 @@ func runSling(cmd *cobra.Command, args []string) error {
|
||||
if formulaName != "" {
|
||||
fmt.Printf(" Instantiating formula %s...\n", formulaName)
|
||||
|
||||
// Route bd mutations (wisp/bond) to the correct beads context for the target bead.
|
||||
// Some bd mol commands don't support prefix routing, so we must run them from the
|
||||
// rig directory that owns the bead's database.
|
||||
formulaWorkDir := beads.ResolveHookDir(townRoot, beadID, hookWorkDir)
|
||||
|
||||
// Step 1: Cook the formula (ensures proto exists)
|
||||
// Cook runs from rig directory to access the correct formula database
|
||||
cookCmd := exec.Command("bd", "--no-daemon", "cook", formulaName)
|
||||
cookCmd.Dir = formulaWorkDir
|
||||
cookCmd.Stderr = os.Stderr
|
||||
if err := cookCmd.Run(); err != nil {
|
||||
return fmt.Errorf("cooking formula %s: %w", formulaName, err)
|
||||
}
|
||||
|
||||
// Step 2: Create wisp with feature and issue variables from bead
|
||||
// Run from rig directory so wisp is created in correct database
|
||||
featureVar := fmt.Sprintf("feature=%s", info.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()
|
||||
result, err := InstantiateFormulaOnBead(formulaName, beadID, info.Title, hookWorkDir, townRoot, false)
|
||||
if err != nil {
|
||||
return 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 fmt.Errorf("parsing wisp output: %w", err)
|
||||
}
|
||||
fmt.Printf("%s Formula wisp created: %s\n", style.Bold.Render("✓"), wispRootID)
|
||||
|
||||
// Step 3: Bond wisp to original bead (creates compound)
|
||||
// Use --no-daemon for mol bond (requires direct database access)
|
||||
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 fmt.Errorf("bonding formula to bead: %w", err)
|
||||
}
|
||||
|
||||
// Parse bond output - the wisp root becomes the compound root
|
||||
// After bonding, we hook the wisp root (which now contains the original bead)
|
||||
var bondResult struct {
|
||||
RootID string `json:"root_id"`
|
||||
}
|
||||
if err := json.Unmarshal(bondOut, &bondResult); err != nil {
|
||||
// Fallback: use wisp root as the compound root
|
||||
fmt.Printf("%s Could not parse bond output, using wisp root\n", style.Dim.Render("Warning:"))
|
||||
} else if bondResult.RootID != "" {
|
||||
wispRootID = bondResult.RootID
|
||||
return fmt.Errorf("instantiating formula %s: %w", formulaName, err)
|
||||
}
|
||||
|
||||
fmt.Printf("%s Formula wisp created: %s\n", style.Bold.Render("✓"), result.WispRootID)
|
||||
fmt.Printf("%s Formula bonded to %s\n", style.Bold.Render("✓"), beadID)
|
||||
|
||||
// Record attached molecule after other description updates to avoid overwrite.
|
||||
attachedMoleculeID = wispRootID
|
||||
// Record attached molecule - will be stored in BASE bead (not wisp).
|
||||
// The base bead is hooked, and its attached_molecule points to the wisp.
|
||||
// This enables:
|
||||
// - gt hook/gt prime: read base bead, follow attached_molecule to show wisp steps
|
||||
// - gt done: close attached_molecule (wisp) first, then close base bead
|
||||
// - Compound resolution: base bead -> attached_molecule -> wisp
|
||||
attachedMoleculeID = result.WispRootID
|
||||
|
||||
// Update beadID to hook the compound root instead of bare bead
|
||||
beadID = wispRootID
|
||||
// NOTE: We intentionally keep beadID as the ORIGINAL base bead, not the wisp.
|
||||
// The base bead is hooked so that:
|
||||
// 1. gt done closes both the base bead AND the attached molecule (wisp)
|
||||
// 2. The base bead's attached_molecule field points to the wisp for compound resolution
|
||||
// Previously, this line incorrectly set beadID = wispRootID, causing:
|
||||
// - Wisp hooked instead of base bead
|
||||
// - attached_molecule stored as self-reference in wisp (meaningless)
|
||||
// - Base bead left orphaned after gt done
|
||||
}
|
||||
|
||||
// Hook the bead using bd update.
|
||||
@@ -515,17 +483,6 @@ func runSling(cmd *cobra.Command, args []string) error {
|
||||
updateAgentHookBead(targetAgent, beadID, hookWorkDir, townBeadsDir)
|
||||
}
|
||||
|
||||
// Auto-attach mol-polecat-work to polecat agent beads
|
||||
// This ensures polecats have the standard work molecule attached for guidance.
|
||||
// Only do this for bare beads (no --on formula), since formula-on-bead
|
||||
// mode already attaches the formula as a molecule.
|
||||
if formulaName == "" && strings.Contains(targetAgent, "/polecats/") {
|
||||
if err := attachPolecatWorkMolecule(targetAgent, hookWorkDir, townRoot); err != nil {
|
||||
// Warn but don't fail - polecat will still work without molecule
|
||||
fmt.Printf("%s Could not attach work molecule: %v\n", style.Dim.Render("Warning:"), err)
|
||||
}
|
||||
}
|
||||
|
||||
// Store dispatcher in bead description (enables completion notification to dispatcher)
|
||||
if err := storeDispatcherInBead(beadID, actor); err != nil {
|
||||
// Warn but don't fail - polecat will still complete work
|
||||
@@ -542,8 +499,11 @@ func runSling(cmd *cobra.Command, args []string) error {
|
||||
}
|
||||
}
|
||||
|
||||
// Record the attached molecule in the wisp's description.
|
||||
// This is required for gt hook to recognize the molecule attachment.
|
||||
// Record the attached molecule in the BASE bead's description.
|
||||
// This field points to the wisp (compound root) and enables:
|
||||
// - gt hook/gt prime: follow attached_molecule to show molecule steps
|
||||
// - gt done: close attached_molecule (wisp) before closing hooked bead
|
||||
// - Compound resolution: base bead -> attached_molecule -> wisp
|
||||
if attachedMoleculeID != "" {
|
||||
if err := storeAttachedMoleculeInBead(beadID, attachedMoleculeID); err != nil {
|
||||
// Warn but don't fail - polecat can still work through steps
|
||||
|
||||
Reference in New Issue
Block a user