fix(sling): auto-apply mol-polecat-work (#288) and fix wisp orphan lifecycle bug (#842) (#859)

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:
Julian Knutsen
2026-01-21 18:52:26 -10:00
committed by GitHub
parent 1feb48dd11
commit 0dfb0be368
8 changed files with 1050 additions and 149 deletions

View File

@@ -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