gt sling: support standalone formula slinging (gt-z9qoo)

When first arg is a formula (not a bead), sling now:
1. Cooks the formula (bd cook)
2. Creates a wisp instance (bd wisp)
3. Attaches the wisp to the target hook
4. Nudges the target to start

New flags:
- --var key=value: Pass variables to formula (repeatable)

Examples:
  gt sling mol-town-shutdown mayor/
  gt sling towers-of-hanoi --var disks=3

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Steve Yegge
2025-12-25 22:33:25 -08:00
parent e7dfc58a02
commit 9daee341ac

View File

@@ -1,6 +1,7 @@
package cmd package cmd
import ( import (
"encoding/json"
"fmt" "fmt"
"os" "os"
"os/exec" "os/exec"
@@ -13,13 +14,13 @@ import (
) )
var slingCmd = &cobra.Command{ var slingCmd = &cobra.Command{
Use: "sling <bead-id> [target]", Use: "sling <bead-or-formula> [target]",
GroupID: GroupWork, GroupID: GroupWork,
Short: "Hook work and start immediately (no restart)", Short: "Hook work and start immediately (no restart)",
Long: `Sling work onto an agent's hook and start working immediately. Long: `Sling work onto an agent's hook and start working immediately.
Unlike 'gt handoff', sling does NOT restart the session. It: Unlike 'gt handoff', sling does NOT restart the session. It:
1. Attaches the bead to the hook (durability) 1. Attaches the work to the hook (durability)
2. Injects a prompt to start working NOW 2. Injects a prompt to start working NOW
This preserves current context while kicking off work. Use when: This preserves current context while kicking off work. Use when:
@@ -31,17 +32,26 @@ The hook provides durability - the agent can restart, compact, or hand off,
but until the hook is changed or closed, that agent owns the work. but until the hook is changed or closed, that agent owns the work.
Examples: Examples:
gt sling gt-abc # Hook and start on it now gt sling gt-abc # Hook bead and start now
gt sling gt-abc -s "Fix the bug" # With context subject gt sling gt-abc -s "Fix the bug" # With context subject
gt sling gt-abc crew # Sling to crew worker gt sling gt-abc crew # Sling bead to crew worker
gt sling gt-abc gastown/crew/max # Sling to specific agent gt sling gt-abc gastown/crew/max # Sling bead to specific agent
Formula scaffolding (--on flag): Standalone formula slinging:
gt sling mol-town-shutdown mayor/ # Cook + wisp + attach + nudge
gt sling towers-of-hanoi --var disks=3 # With formula variables
When the first argument is a formula (not a bead), sling will:
1. Cook the formula (bd cook)
2. Create a wisp instance (bd wisp)
3. Attach the wisp to the target's hook
4. Nudge the target to start
Formula-on-bead scaffolding (--on flag):
gt sling shiny --on gt-abc # Apply shiny formula to existing work gt sling shiny --on gt-abc # Apply shiny formula to existing work
gt sling mol-review --on gt-abc crew # Apply review formula, sling to crew gt sling mol-review --on gt-abc crew # Apply review formula, sling to crew
When --on is specified, the first argument is a formula name (not a bead). When --on is specified, the formula shapes execution of the target bead.
The formula shapes execution of the target bead, creating wisp scaffolding.
Compare: Compare:
gt hook <bead> # Just attach (no action) gt hook <bead> # Just attach (no action)
@@ -57,7 +67,8 @@ var (
slingSubject string slingSubject string
slingMessage string slingMessage string
slingDryRun bool slingDryRun bool
slingOnTarget string // --on flag: target bead when slinging a formula slingOnTarget string // --on flag: target bead when slinging a formula
slingVars []string // --var flag: formula variables (key=value)
) )
func init() { func init() {
@@ -65,38 +76,48 @@ func init() {
slingCmd.Flags().StringVarP(&slingMessage, "message", "m", "", "Context message for the work") slingCmd.Flags().StringVarP(&slingMessage, "message", "m", "", "Context message for the work")
slingCmd.Flags().BoolVarP(&slingDryRun, "dry-run", "n", false, "Show what would be done") slingCmd.Flags().BoolVarP(&slingDryRun, "dry-run", "n", false, "Show what would be done")
slingCmd.Flags().StringVar(&slingOnTarget, "on", "", "Apply formula to existing bead (implies wisp scaffolding)") slingCmd.Flags().StringVar(&slingOnTarget, "on", "", "Apply formula to existing bead (implies wisp scaffolding)")
slingCmd.Flags().StringArrayVar(&slingVars, "var", nil, "Formula variable (key=value), can be repeated")
rootCmd.AddCommand(slingCmd) rootCmd.AddCommand(slingCmd)
} }
func runSling(cmd *cobra.Command, args []string) error { func runSling(cmd *cobra.Command, args []string) error {
// Determine if we're in formula mode (--on flag)
var beadID string
var formulaName string
if slingOnTarget != "" {
// Formula mode: gt sling <formula> --on <bead>
formulaName = args[0]
beadID = slingOnTarget
} else {
// Normal mode: gt sling <bead>
beadID = args[0]
}
// Polecats cannot sling - check early before writing anything // Polecats cannot sling - check early before writing anything
if polecatName := os.Getenv("GT_POLECAT"); polecatName != "" { if polecatName := os.Getenv("GT_POLECAT"); polecatName != "" {
return fmt.Errorf("polecats cannot sling (use gt done for handoff)") return fmt.Errorf("polecats cannot sling (use gt done for handoff)")
} }
// Verify the bead exists // Determine mode based on flags and argument types
if err := verifyBeadExists(beadID); err != nil { var beadID string
return err var formulaName string
}
// If formula specified, verify it exists if slingOnTarget != "" {
if formulaName != "" { // Formula-on-bead mode: gt sling <formula> --on <bead>
formulaName = args[0]
beadID = slingOnTarget
// Verify both exist
if err := verifyBeadExists(beadID); err != nil {
return err
}
if err := verifyFormulaExists(formulaName); err != nil { if err := verifyFormulaExists(formulaName); err != nil {
return err return err
} }
} else {
// Could be bead mode or standalone formula mode
firstArg := args[0]
// Try as bead first
if err := verifyBeadExists(firstArg); err == nil {
// It's a bead
beadID = firstArg
} else {
// Not a bead - try as standalone formula
if err := verifyFormulaExists(firstArg); err == nil {
// Standalone formula mode: gt sling <formula> [target]
return runSlingFormula(args)
}
// Neither bead nor formula
return fmt.Errorf("'%s' is not a valid bead or formula", firstArg)
}
} }
// Determine target agent (self or specified) // Determine target agent (self or specified)
@@ -333,22 +354,154 @@ func detectCloneRoot() (string, error) {
} }
// verifyFormulaExists checks that the formula exists using bd formula show. // verifyFormulaExists checks that the formula exists using bd formula show.
// Formulas can be proto beads (mol-*) or formula files (.formula.json). // Formulas can be formula files (.formula.json/.formula.toml).
func verifyFormulaExists(formulaName string) error { func verifyFormulaExists(formulaName string) error {
// Try as a proto bead first (mol-* prefix is common) // Try bd formula show (handles all formula file formats)
cmd := exec.Command("bd", "show", formulaName, "--json") cmd := exec.Command("bd", "formula", "show", formulaName)
if err := cmd.Run(); err == nil { if err := cmd.Run(); err == nil {
return nil // Found as a proto return nil
} }
// Try with mol- prefix // Try with mol- prefix
cmd = exec.Command("bd", "show", "mol-"+formulaName, "--json") cmd = exec.Command("bd", "formula", "show", "mol-"+formulaName)
if err := cmd.Run(); err == nil { if err := cmd.Run(); err == nil {
return nil // Found as mol-<name> return nil
} }
// TODO: Check for .formula.json file in search paths return fmt.Errorf("formula '%s' not found (check 'bd formula list')", formulaName)
// For now, we require the formula to exist as a proto }
return fmt.Errorf("formula '%s' not found (try 'bd cook' to create it from a .formula.json file)", formulaName) // runSlingFormula handles standalone formula slinging.
// Flow: cook → wisp → attach to hook → nudge
func runSlingFormula(args []string) error {
formulaName := args[0]
// Determine target (self or specified)
var target string
if len(args) > 1 {
target = args[1]
}
// Resolve target agent and pane
var targetAgent string
var targetPane string
var hookRoot string
var err error
if target != "" {
// Slinging to another agent
targetAgent, targetPane, err = resolveTargetAgent(target)
if err != nil {
return fmt.Errorf("resolving target: %w", err)
}
hookRoot, err = detectCloneRoot()
if err != nil {
return fmt.Errorf("detecting clone root: %w", err)
}
} else {
// Slinging to self
roleInfo, err := GetRole()
if err != nil {
return fmt.Errorf("detecting role: %w", err)
}
switch roleInfo.Role {
case RoleMayor:
targetAgent = "mayor"
case RoleDeacon:
targetAgent = "deacon"
case RoleWitness:
targetAgent = fmt.Sprintf("%s/witness", roleInfo.Rig)
case RoleRefinery:
targetAgent = fmt.Sprintf("%s/refinery", roleInfo.Rig)
case RolePolecat:
targetAgent = fmt.Sprintf("%s/polecats/%s", roleInfo.Rig, roleInfo.Polecat)
case RoleCrew:
targetAgent = fmt.Sprintf("%s/crew/%s", roleInfo.Rig, roleInfo.Polecat)
default:
return fmt.Errorf("cannot determine agent identity (role: %s)", roleInfo.Role)
}
targetPane = os.Getenv("TMUX_PANE")
hookRoot = roleInfo.Home
if hookRoot == "" {
hookRoot, err = detectCloneRoot()
if err != nil {
return fmt.Errorf("detecting clone root: %w", err)
}
}
}
fmt.Printf("%s Slinging formula %s to %s...\n", style.Bold.Render("🎯"), formulaName, targetAgent)
if slingDryRun {
fmt.Printf("Would cook formula: %s\n", formulaName)
fmt.Printf("Would create wisp and attach to hook: %s\n", wisp.HookPath(hookRoot, targetAgent))
for _, v := range slingVars {
fmt.Printf(" --var %s\n", v)
}
fmt.Printf("Would nudge pane: %s\n", targetPane)
return nil
}
// Step 1: Cook the formula (ensures proto exists)
fmt.Printf(" Cooking formula...\n")
cookArgs := []string{"cook", formulaName}
cookCmd := exec.Command("bd", cookArgs...)
cookCmd.Stderr = os.Stderr
if err := cookCmd.Run(); err != nil {
return fmt.Errorf("cooking formula: %w", err)
}
// Step 2: Create wisp instance (ephemeral)
fmt.Printf(" Creating wisp...\n")
wispArgs := []string{"wisp", formulaName}
for _, v := range slingVars {
wispArgs = append(wispArgs, "--var", v)
}
wispArgs = append(wispArgs, "--json")
wispCmd := exec.Command("bd", wispArgs...)
wispOut, err := wispCmd.Output()
if err != nil {
return fmt.Errorf("creating wisp: %w", err)
}
// Parse wisp output to get the root ID
var wispResult struct {
RootID string `json:"root_id"`
}
if err := json.Unmarshal(wispOut, &wispResult); err != nil {
// Fallback: use formula name as identifier
wispResult.RootID = formulaName
}
fmt.Printf("%s Wisp created: %s\n", style.Bold.Render("✓"), wispResult.RootID)
// Step 3: Attach to hook
sw := wisp.NewSlungWork(wispResult.RootID, targetAgent)
sw.Subject = slingSubject
if sw.Subject == "" {
sw.Subject = fmt.Sprintf("Formula: %s", formulaName)
}
sw.Context = slingMessage
sw.Formula = formulaName
if err := wisp.WriteSlungWork(hookRoot, targetAgent, sw); err != nil {
return fmt.Errorf("writing to hook: %w", err)
}
fmt.Printf("%s Attached to hook\n", style.Bold.Render("✓"))
// Step 4: Nudge to start
if targetPane == "" {
fmt.Printf("%s No pane to nudge (target may need manual start)\n", style.Dim.Render("○"))
return nil
}
prompt := fmt.Sprintf("Formula %s slung. Run `gt mol status` to see your hook, then execute the steps.", formulaName)
t := tmux.NewTmux()
if err := t.NudgePane(targetPane, prompt); err != nil {
return fmt.Errorf("nudging: %w", err)
}
fmt.Printf("%s Nudged to start\n", style.Bold.Render("▶"))
return nil
} }