Files
gastown/internal/cmd/sling.go
Steve Yegge 196c3bbf9b Fix gt sling --naked to bypass pane lookup for terminated polecats
When slinging to an existing polecat with --naked flag, the code was still
attempting to look up the tmux pane, which fails for terminated polecats.
Now resolveTargetAgent accepts a skipPane parameter that bypasses the tmux
pane and working directory lookup when true.

This allows work to be slung to terminated polecats that will be restarted
manually later.

Also updated unsling to skip pane lookup since it only needs the agent ID.

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-29 22:08:31 -08:00

716 lines
24 KiB
Go

package cmd
import (
"encoding/json"
"fmt"
"os"
"os/exec"
"strings"
"github.com/spf13/cobra"
"github.com/steveyegge/gastown/internal/beads"
"github.com/steveyegge/gastown/internal/session"
"github.com/steveyegge/gastown/internal/style"
"github.com/steveyegge/gastown/internal/tmux"
)
var slingCmd = &cobra.Command{
Use: "sling <bead-or-formula> [target]",
GroupID: GroupWork,
Short: "Assign work to an agent (THE unified work dispatch command)",
Long: `Sling work onto an agent's hook and start working immediately.
This is THE command for assigning work in Gas Town. It handles:
- Existing agents (mayor, crew, witness, refinery)
- Auto-spawning polecats when target is a rig
- Formula instantiation and wisp creation
- No-tmux mode for manual agent operation
Target Resolution:
gt sling gt-abc # Self (current agent)
gt sling gt-abc crew # Crew worker in current rig
gt sling gt-abc gastown # Auto-spawn polecat in rig
gt sling gt-abc gastown/Toast # Specific polecat
gt sling gt-abc mayor # Mayor
Spawning Options (when target is a rig):
gt sling gt-abc gastown --molecule mol-review # Use specific workflow
gt sling gt-abc gastown --create # Create polecat if missing
gt sling gt-abc gastown --naked # No-tmux (manual start)
gt sling gt-abc gastown --force # Ignore unread mail
gt sling gt-abc gastown --account work # Use specific Claude account
Natural Language Args:
gt sling gt-abc --args "patch release"
gt sling code-review --args "focus on security"
The --args string is stored in the bead and shown via gt prime. Since the
executor is an LLM, it interprets these instructions naturally.
Formula Slinging:
gt sling mol-release mayor/ # Cook + wisp + attach + nudge
gt sling towers-of-hanoi --var disks=3
Formula-on-Bead (--on flag):
gt sling mol-review --on gt-abc # Apply formula to existing work
gt sling shiny --on gt-abc crew # Apply formula, sling to crew
Compare:
gt hook <bead> # Just attach (no action)
gt sling <bead> # Attach + start now (keep context)
gt handoff <bead> # Attach + restart (fresh context)
The propulsion principle: if it's on your hook, YOU RUN IT.`,
Args: cobra.RangeArgs(1, 2),
RunE: runSling,
}
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
// Flags migrated for polecat spawning (used by sling for work assignment
slingNaked bool // --naked: no-tmux mode (skip session creation)
slingCreate bool // --create: create polecat if it doesn't exist
slingMolecule string // --molecule: workflow to instantiate on the bead
slingForce bool // --force: force spawn even if polecat has unread mail
slingAccount string // --account: Claude Code account handle to use
)
func init() {
slingCmd.Flags().StringVarP(&slingSubject, "subject", "s", "", "Context subject 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().StringVar(&slingOnTarget, "on", "", "Apply formula to existing bead (implies wisp scaffolding)")
slingCmd.Flags().StringArrayVar(&slingVars, "var", nil, "Formula variable (key=value), can be repeated")
slingCmd.Flags().StringVarP(&slingArgs, "args", "a", "", "Natural language instructions for the executor (e.g., 'patch release')")
// Flags for polecat spawning (when target is a rig)
slingCmd.Flags().BoolVar(&slingNaked, "naked", false, "No-tmux mode: assign work but skip session creation (manual start)")
slingCmd.Flags().BoolVar(&slingCreate, "create", false, "Create polecat if it doesn't exist")
slingCmd.Flags().StringVar(&slingMolecule, "molecule", "", "Molecule workflow to instantiate on the bead")
slingCmd.Flags().BoolVar(&slingForce, "force", false, "Force spawn even if polecat has unread mail")
slingCmd.Flags().StringVar(&slingAccount, "account", "", "Claude Code account handle to use")
rootCmd.AddCommand(slingCmd)
}
func runSling(cmd *cobra.Command, args []string) error {
// Polecats cannot sling - check early before writing anything
if polecatName := os.Getenv("GT_POLECAT"); polecatName != "" {
return fmt.Errorf("polecats cannot sling (use gt done for handoff)")
}
// --var is only for standalone formula mode, not formula-on-bead mode
if slingOnTarget != "" && len(slingVars) > 0 {
return fmt.Errorf("--var cannot be used with --on (formula-on-bead mode doesn't support variables)")
}
// Determine mode based on flags and argument types
var beadID string
var formulaName string
if slingOnTarget != "" {
// 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 {
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)
var targetAgent string
var targetPane string
var err error
if len(args) > 1 {
target := args[1]
// Check if target is a rig name (auto-spawn polecat)
if rigName, isRig := IsRigName(target); isRig {
if slingDryRun {
// Dry run - just indicate what would happen
fmt.Printf("Would spawn fresh polecat in rig '%s'\n", rigName)
if slingNaked {
fmt.Printf(" --naked: would skip tmux session\n")
}
targetAgent = fmt.Sprintf("%s/polecats/<new>", rigName)
targetPane = "<new-pane>"
} else {
// Spawn a fresh polecat in the rig
fmt.Printf("Target is rig '%s', spawning fresh polecat...\n", rigName)
spawnOpts := SlingSpawnOptions{
Force: slingForce,
Naked: slingNaked,
Account: slingAccount,
Create: slingCreate,
}
spawnInfo, spawnErr := SpawnPolecatForSling(rigName, spawnOpts)
if spawnErr != nil {
return fmt.Errorf("spawning polecat: %w", spawnErr)
}
targetAgent = spawnInfo.AgentID()
targetPane = spawnInfo.Pane
// Wake witness and refinery to monitor the new polecat
wakeRigAgents(rigName)
}
} else {
// Slinging to an existing agent
// Skip pane lookup if --naked (agent may be terminated)
targetAgent, targetPane, _, err = resolveTargetAgent(target, slingNaked)
if err != nil {
return fmt.Errorf("resolving target: %w", err)
}
}
} else {
// Slinging to self
targetAgent, targetPane, _, err = resolveSelfTarget()
if err != nil {
return err
}
}
// Display what we're doing
if formulaName != "" {
fmt.Printf("%s Slinging formula %s on %s to %s...\n", style.Bold.Render("🎯"), formulaName, beadID, targetAgent)
} else {
fmt.Printf("%s Slinging %s to %s...\n", style.Bold.Render("🎯"), beadID, targetAgent)
}
// Check if bead is already pinned (guard against accidental re-sling)
info, err := getBeadInfo(beadID)
if err != nil {
return fmt.Errorf("checking bead status: %w", err)
}
if info.Status == "pinned" && !slingForce {
assignee := info.Assignee
if assignee == "" {
assignee = "(unknown)"
}
return fmt.Errorf("bead %s is already pinned to %s\nUse --force to re-sling", beadID, assignee)
}
if slingDryRun {
fmt.Printf("Would run: bd update %s --status=hooked --assignee=%s\n", beadID, targetAgent)
if formulaName != "" {
fmt.Printf(" formula: %s\n", formulaName)
}
if slingSubject != "" {
fmt.Printf(" subject (in nudge): %s\n", slingSubject)
}
if slingMessage != "" {
fmt.Printf(" context: %s\n", slingMessage)
}
if slingArgs != "" {
fmt.Printf(" args (in nudge): %s\n", slingArgs)
}
fmt.Printf("Would inject start prompt to pane: %s\n", targetPane)
return nil
}
// Hook the bead using bd update (discovery-based approach)
hookCmd := exec.Command("bd", "update", beadID, "--status=hooked", "--assignee="+targetAgent)
hookCmd.Stderr = os.Stderr
if err := hookCmd.Run(); err != nil {
return fmt.Errorf("hooking bead: %w", err)
}
fmt.Printf("%s Work attached to hook (status=hooked)\n", style.Bold.Render("✓"))
// Update agent bead's hook_bead field (ZFC: agents track their current work)
updateAgentHookBead(targetAgent, beadID)
// Store args in bead description (no-tmux mode: beads as data plane)
if slingArgs != "" {
if err := storeArgsInBead(beadID, slingArgs); err != nil {
// Warn but don't fail - args will still be in the nudge prompt
fmt.Printf("%s Could not store args in bead: %v\n", style.Dim.Render("Warning:"), err)
} else {
fmt.Printf("%s Args stored in bead (durable)\n", style.Bold.Render("✓"))
}
}
// Try to inject the "start now" prompt (graceful if no tmux)
if targetPane == "" {
fmt.Printf("%s No pane to nudge (agent will discover work via gt prime)\n", style.Dim.Render("○"))
} else if err := injectStartPrompt(targetPane, beadID, slingSubject, slingArgs); err != nil {
// Graceful fallback for no-tmux mode
fmt.Printf("%s Could not nudge (no tmux?): %v\n", style.Dim.Render("○"), err)
fmt.Printf(" Agent will discover work via gt prime / bd show\n")
} else {
fmt.Printf("%s Start prompt sent\n", style.Bold.Render("▶"))
}
return nil
}
// storeArgsInBead stores args in the bead's description using attached_args field.
// This enables no-tmux mode where agents discover args via gt prime / bd show.
func storeArgsInBead(beadID, args string) error {
// Get the bead to preserve existing description content
showCmd := exec.Command("bd", "show", beadID, "--json")
out, err := showCmd.Output()
if err != nil {
return fmt.Errorf("fetching bead: %w", err)
}
// Parse the bead
var issues []beads.Issue
if err := json.Unmarshal(out, &issues); err != nil {
return fmt.Errorf("parsing bead: %w", err)
}
if len(issues) == 0 {
return fmt.Errorf("bead not found")
}
issue := &issues[0]
// Get or create attachment fields
fields := beads.ParseAttachmentFields(issue)
if fields == nil {
fields = &beads.AttachmentFields{}
}
// Set the args
fields.AttachedArgs = args
// Update the description
newDesc := beads.SetAttachmentFields(issue, fields)
// Update the bead
updateCmd := exec.Command("bd", "update", beadID, "--description="+newDesc)
updateCmd.Stderr = os.Stderr
if err := updateCmd.Run(); err != nil {
return fmt.Errorf("updating bead description: %w", err)
}
return nil
}
// injectStartPrompt sends a prompt to the target pane to start working.
// Uses the reliable nudge pattern: literal mode + 500ms debounce + separate Enter.
func injectStartPrompt(pane, beadID, subject, args string) error {
if pane == "" {
return fmt.Errorf("no target pane")
}
// Build the prompt to inject
var prompt string
if args != "" {
// Args provided - include them prominently in the prompt
if subject != "" {
prompt = fmt.Sprintf("Work slung: %s (%s). Args: %s. Start working now - use these args to guide your execution.", beadID, subject, args)
} else {
prompt = fmt.Sprintf("Work slung: %s. Args: %s. Start working now - use these args to guide your execution.", beadID, args)
}
} else if subject != "" {
prompt = fmt.Sprintf("Work slung: %s (%s). Start working on it now - no questions, just begin.", beadID, subject)
} else {
prompt = fmt.Sprintf("Work slung: %s. Start working on it now - run `gt mol status` to see the hook, then begin.", beadID)
}
// Use the reliable nudge pattern (same as gt nudge / tmux.NudgeSession)
t := tmux.NewTmux()
return t.NudgePane(pane, prompt)
}
// resolveTargetAgent converts a target spec to agent ID, pane, and hook root.
// If skipPane is true, skip tmux pane lookup (for --naked mode).
func resolveTargetAgent(target string, skipPane bool) (agentID string, pane string, hookRoot string, err error) {
// First resolve to session name
sessionName, err := resolveRoleToSession(target)
if err != nil {
return "", "", "", err
}
// Convert session name to agent ID format (this doesn't require tmux)
agentID = sessionToAgentID(sessionName)
// Skip pane lookup if requested (--naked mode)
if skipPane {
return agentID, "", "", nil
}
// Get the pane for that session
pane, err = getSessionPane(sessionName)
if err != nil {
return "", "", "", fmt.Errorf("getting pane for %s: %w", sessionName, err)
}
// Get the target's working directory for hook storage
t := tmux.NewTmux()
hookRoot, err = t.GetPaneWorkDir(sessionName)
if err != nil {
return "", "", "", fmt.Errorf("getting working dir for %s: %w", sessionName, err)
}
return agentID, pane, hookRoot, nil
}
// sessionToAgentID converts a session name to agent ID format.
// Uses session.ParseSessionName for consistent parsing across the codebase.
func sessionToAgentID(sessionName string) string {
identity, err := session.ParseSessionName(sessionName)
if err != nil {
// Fallback for unparseable sessions
return sessionName
}
return identity.Address()
}
// verifyBeadExists checks that the bead exists using bd show.
func verifyBeadExists(beadID string) error {
cmd := exec.Command("bd", "show", beadID, "--json")
if err := cmd.Run(); err != nil {
return fmt.Errorf("bead '%s' not found (bd show failed)", beadID)
}
return nil
}
// beadInfo holds status and assignee for a bead.
type beadInfo struct {
Status string `json:"status"`
Assignee string `json:"assignee"`
}
// getBeadInfo returns status and assignee for a bead.
func getBeadInfo(beadID string) (*beadInfo, error) {
cmd := exec.Command("bd", "show", beadID, "--json")
out, err := cmd.Output()
if err != nil {
return nil, fmt.Errorf("bead '%s' not found", beadID)
}
// bd show --json returns an array (issue + dependents), take first element
var infos []beadInfo
if err := json.Unmarshal(out, &infos); err != nil {
return nil, fmt.Errorf("parsing bead info: %w", err)
}
if len(infos) == 0 {
return nil, fmt.Errorf("bead '%s' not found", beadID)
}
return &infos[0], nil
}
// detectCloneRoot finds the root of the current git clone.
func detectCloneRoot() (string, error) {
cmd := exec.Command("git", "rev-parse", "--show-toplevel")
out, err := cmd.Output()
if err != nil {
return "", fmt.Errorf("not in a git repository")
}
return strings.TrimSpace(string(out)), nil
}
// resolveSelfTarget determines agent identity, pane, and hook root for slinging to self.
func resolveSelfTarget() (agentID string, pane string, hookRoot string, err error) {
roleInfo, err := GetRole()
if err != nil {
return "", "", "", fmt.Errorf("detecting role: %w", err)
}
// Build agent identity from role
switch roleInfo.Role {
case RoleMayor:
agentID = "mayor"
case RoleDeacon:
agentID = "deacon"
case RoleWitness:
agentID = fmt.Sprintf("%s/witness", roleInfo.Rig)
case RoleRefinery:
agentID = fmt.Sprintf("%s/refinery", roleInfo.Rig)
case RolePolecat:
agentID = fmt.Sprintf("%s/polecats/%s", roleInfo.Rig, roleInfo.Polecat)
case RoleCrew:
agentID = fmt.Sprintf("%s/crew/%s", roleInfo.Rig, roleInfo.Polecat)
default:
return "", "", "", fmt.Errorf("cannot determine agent identity (role: %s)", roleInfo.Role)
}
pane = os.Getenv("TMUX_PANE")
hookRoot = roleInfo.Home
if hookRoot == "" {
// Fallback to git root if home not determined
hookRoot, err = detectCloneRoot()
if err != nil {
return "", "", "", fmt.Errorf("detecting clone root: %w", err)
}
}
return agentID, pane, hookRoot, nil
}
// verifyFormulaExists checks that the formula exists using bd formula show.
// Formulas are TOML files (.formula.toml).
func verifyFormulaExists(formulaName string) error {
// Try bd formula show (handles all formula file formats)
cmd := exec.Command("bd", "formula", "show", formulaName)
if err := cmd.Run(); err == nil {
return nil
}
// Try with mol- prefix
cmd = exec.Command("bd", "formula", "show", "mol-"+formulaName)
if err := cmd.Run(); err == nil {
return nil
}
return fmt.Errorf("formula '%s' not found (check 'bd formula list')", 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 err error
if target != "" {
// Check if target is a rig name (auto-spawn polecat)
if rigName, isRig := IsRigName(target); isRig {
if slingDryRun {
// Dry run - just indicate what would happen
fmt.Printf("Would spawn fresh polecat in rig '%s'\n", rigName)
if slingNaked {
fmt.Printf(" --naked: would skip tmux session\n")
}
targetAgent = fmt.Sprintf("%s/polecats/<new>", rigName)
targetPane = "<new-pane>"
} else {
// Spawn a fresh polecat in the rig
fmt.Printf("Target is rig '%s', spawning fresh polecat...\n", rigName)
spawnOpts := SlingSpawnOptions{
Force: slingForce,
Naked: slingNaked,
Account: slingAccount,
Create: slingCreate,
}
spawnInfo, spawnErr := SpawnPolecatForSling(rigName, spawnOpts)
if spawnErr != nil {
return fmt.Errorf("spawning polecat: %w", spawnErr)
}
targetAgent = spawnInfo.AgentID()
targetPane = spawnInfo.Pane
// Wake witness and refinery to monitor the new polecat
wakeRigAgents(rigName)
}
} else {
// Slinging to an existing agent
// Skip pane lookup if --naked (agent may be terminated)
targetAgent, targetPane, _, err = resolveTargetAgent(target, slingNaked)
if err != nil {
return fmt.Errorf("resolving target: %w", err)
}
}
} else {
// Slinging to self
targetAgent, targetPane, _, err = resolveSelfTarget()
if err != nil {
return 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 pin to: %s\n", 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...)
wispCmd.Stderr = os.Stderr // Show wisp errors to user
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, but warn user
fmt.Printf("%s Could not parse wisp output, using formula name as ID\n", style.Dim.Render("Warning:"))
wispResult.RootID = formulaName
}
fmt.Printf("%s Wisp created: %s\n", style.Bold.Render("✓"), wispResult.RootID)
// Step 3: Hook the wisp bead using bd update (discovery-based approach)
hookCmd := exec.Command("bd", "update", wispResult.RootID, "--status=hooked", "--assignee="+targetAgent)
hookCmd.Stderr = os.Stderr
if err := hookCmd.Run(); err != nil {
return fmt.Errorf("hooking wisp bead: %w", err)
}
fmt.Printf("%s Attached to hook (status=hooked)\n", style.Bold.Render("✓"))
// Update agent bead's hook_bead field (ZFC: agents track their current work)
updateAgentHookBead(targetAgent, wispResult.RootID)
// Store args in wisp bead if provided (no-tmux mode: beads as data plane)
if slingArgs != "" {
if err := storeArgsInBead(wispResult.RootID, slingArgs); err != nil {
fmt.Printf("%s Could not store args in bead: %v\n", style.Dim.Render("Warning:"), err)
} else {
fmt.Printf("%s Args stored in bead (durable)\n", style.Bold.Render("✓"))
}
}
// Step 4: Nudge to start (graceful if no tmux)
if targetPane == "" {
fmt.Printf("%s No pane to nudge (agent will discover work via gt prime)\n", style.Dim.Render("○"))
return nil
}
var prompt string
if slingArgs != "" {
prompt = fmt.Sprintf("Formula %s slung. Args: %s. Run `gt mol status` to see your hook, then execute using these args.", formulaName, slingArgs)
} else {
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 {
// Graceful fallback for no-tmux mode
fmt.Printf("%s Could not nudge (no tmux?): %v\n", style.Dim.Render("○"), err)
fmt.Printf(" Agent will discover work via gt prime / bd show\n")
} else {
fmt.Printf("%s Nudged to start\n", style.Bold.Render("▶"))
}
return nil
}
// updateAgentHookBead updates the agent bead's hook_bead field when work is slung.
// This enables the witness to see what each agent is working on.
func updateAgentHookBead(agentID, beadID string) {
// Convert agent ID to agent bead ID
// Format examples (canonical: prefix-rig-role-name):
// gastown/crew/max -> gt-gastown-crew-max
// gastown/polecats/Toast -> gt-gastown-polecat-Toast
// mayor -> gt-mayor
// gastown/witness -> gt-gastown-witness
agentBeadID := agentIDToBeadID(agentID)
if agentBeadID == "" {
return
}
// Find beads directory - try current directory first
workDir, err := os.Getwd()
if err != nil {
return
}
bd := beads.New(workDir)
if err := bd.UpdateAgentState(agentBeadID, "running", &beadID); err != nil {
// Silently ignore - agent bead might not exist yet
return
}
}
// wakeRigAgents wakes the witness and refinery for a rig after polecat dispatch.
// This ensures the patrol agents are ready to monitor and merge.
func wakeRigAgents(rigName string) {
// Boot the rig (idempotent - no-op if already running)
bootCmd := exec.Command("gt", "rig", "boot", rigName)
_ = bootCmd.Run() // Ignore errors - rig might already be running
// Nudge witness and refinery to clear any backoff
t := tmux.NewTmux()
witnessSession := fmt.Sprintf("gt-%s-witness", rigName)
refinerySession := fmt.Sprintf("gt-%s-refinery", rigName)
// Silent nudges - sessions might not exist yet
_ = t.NudgeSession(witnessSession, "Polecat dispatched - check for work")
_ = t.NudgeSession(refinerySession, "Polecat dispatched - check for merge requests")
}
// agentIDToBeadID converts an agent ID to its corresponding agent bead ID.
// Uses canonical naming: prefix-rig-role-name
func agentIDToBeadID(agentID string) string {
// Handle simple cases
if agentID == "mayor" {
return beads.MayorBeadID()
}
if agentID == "deacon" {
return beads.DeaconBeadID()
}
// Parse path-style agent IDs
parts := strings.Split(agentID, "/")
if len(parts) < 2 {
return ""
}
rig := parts[0]
switch {
case len(parts) == 2 && parts[1] == "witness":
return beads.WitnessBeadID(rig)
case len(parts) == 2 && parts[1] == "refinery":
return beads.RefineryBeadID(rig)
case len(parts) == 3 && parts[1] == "crew":
return beads.CrewBeadID(rig, parts[2])
case len(parts) == 3 && parts[1] == "polecats":
return beads.PolecatBeadID(rig, parts[2])
default:
return ""
}
}