The gt hook command was using --status=pinned while gt sling and the rest of the codebase use --status=hooked. This inconsistency meant gt hook would not find beads hooked by gt sling, and vice versa. Changes: - hook.go: Use beads.StatusHooked instead of beads.StatusPinned - hook.go: Update bd update command to use --status=hooked - unsling.go: Use beads.StatusHooked when finding hooked beads - Update error messages and comments to say "hooked" instead of "pinned" This fix ensures gt hook works uniformly for all bead types (molecules, issues, mail beads) since they all use the same beads system. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
172 lines
4.6 KiB
Go
172 lines
4.6 KiB
Go
package cmd
|
||
|
||
import (
|
||
"fmt"
|
||
|
||
"github.com/spf13/cobra"
|
||
"github.com/steveyegge/gastown/internal/beads"
|
||
"github.com/steveyegge/gastown/internal/events"
|
||
"github.com/steveyegge/gastown/internal/style"
|
||
)
|
||
|
||
var unslingCmd = &cobra.Command{
|
||
Use: "unsling [bead-id] [target]",
|
||
Aliases: []string{"unhook"},
|
||
GroupID: GroupWork,
|
||
Short: "Remove work from an agent's hook",
|
||
Long: `Remove work from an agent's hook (the inverse of sling/hook).
|
||
|
||
With no arguments, clears your own hook. With a bead ID, only unslings
|
||
if that specific bead is currently hooked. With a target, operates on
|
||
another agent's hook.
|
||
|
||
Examples:
|
||
gt unsling # Clear my hook (whatever's there)
|
||
gt unsling gt-abc # Only unsling if gt-abc is hooked
|
||
gt unsling greenplace/joe # Clear joe's hook
|
||
gt unsling gt-abc greenplace/joe # Unsling gt-abc from joe
|
||
|
||
The bead's status changes from 'hooked' back to 'open'.
|
||
|
||
Related commands:
|
||
gt sling <bead> # Hook + start (inverse of unsling)
|
||
gt hook <bead> # Hook without starting
|
||
gt hook # See what's on your hook`,
|
||
Args: cobra.MaximumNArgs(2),
|
||
RunE: runUnsling,
|
||
}
|
||
|
||
var (
|
||
unslingDryRun bool
|
||
unslingForce bool
|
||
)
|
||
|
||
func init() {
|
||
unslingCmd.Flags().BoolVarP(&unslingDryRun, "dry-run", "n", false, "Show what would be done")
|
||
unslingCmd.Flags().BoolVarP(&unslingForce, "force", "f", false, "Unsling even if work is incomplete")
|
||
rootCmd.AddCommand(unslingCmd)
|
||
}
|
||
|
||
func runUnsling(cmd *cobra.Command, args []string) error {
|
||
var targetBeadID string
|
||
var targetAgent string
|
||
|
||
// Parse args: [bead-id] [target]
|
||
switch len(args) {
|
||
case 0:
|
||
// No args - unsling self, whatever is hooked
|
||
case 1:
|
||
// Could be bead ID or target agent
|
||
// If it contains "/" or is a known role, treat as target
|
||
if isAgentTarget(args[0]) {
|
||
targetAgent = args[0]
|
||
} else {
|
||
targetBeadID = args[0]
|
||
}
|
||
case 2:
|
||
targetBeadID = args[0]
|
||
targetAgent = args[1]
|
||
}
|
||
|
||
// Resolve target agent (default: self)
|
||
var agentID string
|
||
var err error
|
||
if targetAgent != "" {
|
||
// Skip pane lookup - unsling only needs agent ID, not tmux session
|
||
agentID, _, _, err = resolveTargetAgent(targetAgent, true)
|
||
if err != nil {
|
||
return fmt.Errorf("resolving target agent: %w", err)
|
||
}
|
||
} else {
|
||
agentID, _, _, err = resolveSelfTarget()
|
||
if err != nil {
|
||
return fmt.Errorf("detecting agent identity: %w", err)
|
||
}
|
||
}
|
||
|
||
// Find beads directory
|
||
workDir, err := findLocalBeadsDir()
|
||
if err != nil {
|
||
return fmt.Errorf("not in a beads workspace: %w", err)
|
||
}
|
||
|
||
b := beads.New(workDir)
|
||
|
||
// Find hooked bead for this agent
|
||
pinnedBeads, err := b.List(beads.ListOptions{
|
||
Status: beads.StatusHooked,
|
||
Assignee: agentID,
|
||
Priority: -1,
|
||
})
|
||
if err != nil {
|
||
return fmt.Errorf("checking hooked beads: %w", err)
|
||
}
|
||
|
||
if len(pinnedBeads) == 0 {
|
||
if targetAgent != "" {
|
||
fmt.Printf("%s No work hooked for %s\n", style.Dim.Render("ℹ"), agentID)
|
||
} else {
|
||
fmt.Printf("%s Nothing on your hook\n", style.Dim.Render("ℹ"))
|
||
}
|
||
return nil
|
||
}
|
||
|
||
pinned := pinnedBeads[0]
|
||
|
||
// If specific bead requested, verify it matches
|
||
if targetBeadID != "" && pinned.ID != targetBeadID {
|
||
return fmt.Errorf("bead %s is not hooked (current hook: %s)", targetBeadID, pinned.ID)
|
||
}
|
||
|
||
// Check if work is complete (warn if not, unless --force)
|
||
isComplete, _ := checkPinnedBeadComplete(b, pinned)
|
||
if !isComplete && !unslingForce {
|
||
return fmt.Errorf("hooked work %s is incomplete (%s)\n Use --force to unsling anyway",
|
||
pinned.ID, pinned.Title)
|
||
}
|
||
|
||
if targetAgent != "" {
|
||
fmt.Printf("%s Unslinging %s from %s...\n", style.Bold.Render("🪝"), pinned.ID, agentID)
|
||
} else {
|
||
fmt.Printf("%s Unslinging %s...\n", style.Bold.Render("🪝"), pinned.ID)
|
||
}
|
||
|
||
if unslingDryRun {
|
||
fmt.Printf("Would run: bd update %s --status=open\n", pinned.ID)
|
||
return nil
|
||
}
|
||
|
||
// Unpin by setting status back to open
|
||
status := "open"
|
||
if err := b.Update(pinned.ID, beads.UpdateOptions{Status: &status}); err != nil {
|
||
return fmt.Errorf("unhooking bead %s: %w", pinned.ID, err)
|
||
}
|
||
|
||
// Log unhook event
|
||
_ = events.LogFeed(events.TypeUnhook, agentID, events.UnhookPayload(pinned.ID))
|
||
|
||
fmt.Printf("%s Work removed from hook\n", style.Bold.Render("✓"))
|
||
fmt.Printf(" Bead %s is now status=open\n", pinned.ID)
|
||
|
||
return nil
|
||
}
|
||
|
||
// isAgentTarget checks if a string looks like an agent target rather than a bead ID.
|
||
// Agent targets contain "/" or are known role names.
|
||
func isAgentTarget(s string) bool {
|
||
// Contains "/" means it's a path like "greenplace/joe"
|
||
for _, c := range s {
|
||
if c == '/' {
|
||
return true
|
||
}
|
||
}
|
||
|
||
// Known role names
|
||
switch s {
|
||
case "mayor", "deacon", "witness", "refinery", "crew":
|
||
return true
|
||
}
|
||
|
||
return false
|
||
}
|