fix(sling): check hooked status and send LIFECYCLE:Shutdown on --force (#828)

* fix(sling): check hooked status and send LIFECYCLE:Shutdown on --force

- Change sling validation to check both pinned and hooked status (was only
  checking pinned, likely a bug)
- Add --force handling that sends LIFECYCLE:Shutdown message to witness when
  forcibly reassigning work from an already-hooked bead
- Use existing LIFECYCLE:Shutdown protocol instead of new KILL_POLECAT -
  witness will auto-nuke if clean, or create cleanup wisp if dirty
- Use agent.Self() to identify the requester (falls back to "unknown"
  for CLI users without GT_ROLE env vars)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: use env vars instead of undefined agent.Self()

The agent.Self() function does not exist in the agent package.
Replace with direct env var lookups for GT_POLECAT (when running
as a polecat) or USER as fallback.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: julianknutsen <julianknutsen@users.noreply.github>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: beads/crew/lizzy <steve.yegge@gmail.com>
This commit is contained in:
Julian Knutsen
2026-01-20 19:57:28 -08:00
committed by GitHub
parent 9a91a1b94f
commit 126ec84bb3

View File

@@ -11,6 +11,7 @@ import (
"github.com/spf13/cobra"
"github.com/steveyegge/gastown/internal/beads"
"github.com/steveyegge/gastown/internal/events"
"github.com/steveyegge/gastown/internal/mail"
"github.com/steveyegge/gastown/internal/style"
"github.com/steveyegge/gastown/internal/workspace"
)
@@ -312,17 +313,63 @@ func runSling(cmd *cobra.Command, args []string) error {
fmt.Printf("%s Slinging %s to %s...\n", style.Bold.Render("🎯"), beadID, targetAgent)
}
// Check if bead is already pinned (guard against accidental re-sling)
// Check if bead is already assigned (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 {
if (info.Status == "pinned" || info.Status == "hooked") && !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)
return fmt.Errorf("bead %s is already %s to %s\nUse --force to re-sling", beadID, info.Status, assignee)
}
// Handle --force when bead is already hooked: send shutdown to old polecat and unhook
if info.Status == "hooked" && slingForce && info.Assignee != "" {
fmt.Printf("%s Bead already hooked to %s, forcing reassignment...\n", style.Warning.Render("⚠"), info.Assignee)
// Determine requester identity from env vars, fall back to "gt-sling"
requester := "gt-sling"
if polecat := os.Getenv("GT_POLECAT"); polecat != "" {
requester = polecat
} else if user := os.Getenv("USER"); user != "" {
requester = user
}
// Extract rig name from assignee (e.g., "gastown/polecats/Toast" -> "gastown")
assigneeParts := strings.Split(info.Assignee, "/")
if len(assigneeParts) >= 3 && assigneeParts[1] == "polecats" {
oldRigName := assigneeParts[0]
oldPolecatName := assigneeParts[2]
// Send LIFECYCLE:Shutdown to witness - will auto-nuke if clean,
// otherwise create cleanup wisp for manual intervention
if townRoot != "" {
router := mail.NewRouter(townRoot)
shutdownMsg := &mail.Message{
From: "gt-sling",
To: fmt.Sprintf("%s/witness", oldRigName),
Subject: fmt.Sprintf("LIFECYCLE:Shutdown %s", oldPolecatName),
Body: fmt.Sprintf("Reason: work_reassigned\nRequestedBy: %s\nBead: %s\nNewAssignee: %s", requester, beadID, targetAgent),
Type: mail.TypeTask,
Priority: mail.PriorityHigh,
}
if err := router.Send(shutdownMsg); err != nil {
fmt.Printf("%s Could not send shutdown to witness: %v\n", style.Dim.Render("Warning:"), err)
} else {
fmt.Printf("%s Sent LIFECYCLE:Shutdown to %s/witness for %s\n", style.Bold.Render("→"), oldRigName, oldPolecatName)
}
}
}
// Unhook the bead from old owner (set status back to open)
unhookCmd := exec.Command("bd", "--no-daemon", "update", beadID, "--status=open", "--assignee=")
unhookCmd.Dir = beads.ResolveHookDir(townRoot, beadID, "")
if err := unhookCmd.Run(); err != nil {
fmt.Printf("%s Could not unhook bead from old owner: %v\n", style.Dim.Render("Warning:"), err)
}
}
// Auto-convoy: check if issue is already tracked by a convoy