feat(cmd): add desire-path commands for agent ergonomics
- gt hook --clear: alias for 'gt unhook' (gt-eod2iv) - gt close: wrapper for 'bd close' (gt-msak6o) - gt bead move: move beads between repos (gt-dzdbr7) These commands were natural guesses that agents tried but didn't exist. Following the desire-paths approach to improve agent ergonomics. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
committed by
Steve Yegge
parent
9b34b6bfec
commit
22a24c5648
148
internal/cmd/bead.go
Normal file
148
internal/cmd/bead.go
Normal file
@@ -0,0 +1,148 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/steveyegge/gastown/internal/style"
|
||||
)
|
||||
|
||||
var beadCmd = &cobra.Command{
|
||||
Use: "bead",
|
||||
GroupID: GroupWork,
|
||||
Short: "Bead management utilities",
|
||||
Long: `Utilities for managing beads across repositories.`,
|
||||
}
|
||||
|
||||
var beadMoveCmd = &cobra.Command{
|
||||
Use: "move <bead-id> <target-prefix>",
|
||||
Short: "Move a bead to a different repository",
|
||||
Long: `Move a bead from one repository to another.
|
||||
|
||||
This creates a copy of the bead in the target repository (with the new prefix)
|
||||
and closes the source bead with a reference to the new location.
|
||||
|
||||
The target prefix determines which repository receives the bead.
|
||||
Common prefixes: gt- (gastown), bd- (beads), hq- (headquarters)
|
||||
|
||||
Examples:
|
||||
gt bead move gt-abc123 bd- # Move gt-abc123 to beads repo as bd-*
|
||||
gt bead move hq-xyz bd- # Move hq-xyz to beads repo
|
||||
gt bead move bd-123 gt- # Move bd-123 to gastown repo`,
|
||||
Args: cobra.ExactArgs(2),
|
||||
RunE: runBeadMove,
|
||||
}
|
||||
|
||||
var beadMoveDryRun bool
|
||||
|
||||
func init() {
|
||||
beadMoveCmd.Flags().BoolVarP(&beadMoveDryRun, "dry-run", "n", false, "Show what would be done")
|
||||
beadCmd.AddCommand(beadMoveCmd)
|
||||
rootCmd.AddCommand(beadCmd)
|
||||
}
|
||||
|
||||
// moveBeadInfo holds the essential fields we need to copy when moving beads
|
||||
type moveBeadInfo struct {
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Type string `json:"issue_type"`
|
||||
Priority int `json:"priority"`
|
||||
Description string `json:"description"`
|
||||
Labels []string `json:"labels"`
|
||||
Assignee string `json:"assignee"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
func runBeadMove(cmd *cobra.Command, args []string) error {
|
||||
sourceID := args[0]
|
||||
targetPrefix := args[1]
|
||||
|
||||
// Normalize prefix (ensure it ends with -)
|
||||
if !strings.HasSuffix(targetPrefix, "-") {
|
||||
targetPrefix = targetPrefix + "-"
|
||||
}
|
||||
|
||||
// Get source bead details
|
||||
showCmd := exec.Command("bd", "show", sourceID, "--json")
|
||||
output, err := showCmd.Output()
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting bead %s: %w", sourceID, err)
|
||||
}
|
||||
|
||||
// bd show --json returns an array
|
||||
var sources []moveBeadInfo
|
||||
if err := json.Unmarshal(output, &sources); err != nil {
|
||||
return fmt.Errorf("parsing bead data: %w", err)
|
||||
}
|
||||
if len(sources) == 0 {
|
||||
return fmt.Errorf("bead %s not found", sourceID)
|
||||
}
|
||||
source := sources[0]
|
||||
|
||||
// Don't move closed beads
|
||||
if source.Status == "closed" {
|
||||
return fmt.Errorf("cannot move closed bead %s", sourceID)
|
||||
}
|
||||
|
||||
fmt.Printf("%s Moving %s to %s...\n", style.Bold.Render("→"), sourceID, targetPrefix)
|
||||
fmt.Printf(" Title: %s\n", source.Title)
|
||||
fmt.Printf(" Type: %s\n", source.Type)
|
||||
|
||||
if beadMoveDryRun {
|
||||
fmt.Printf("\nDry run - would:\n")
|
||||
fmt.Printf(" 1. Create new bead with prefix %s\n", targetPrefix)
|
||||
fmt.Printf(" 2. Close %s with reference to new bead\n", sourceID)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Build create command for target
|
||||
createArgs := []string{
|
||||
"create",
|
||||
"--prefix", targetPrefix,
|
||||
"--title", source.Title,
|
||||
"--type", source.Type,
|
||||
"--priority", fmt.Sprintf("%d", source.Priority),
|
||||
"--silent", // Only output the ID
|
||||
}
|
||||
|
||||
if source.Description != "" {
|
||||
createArgs = append(createArgs, "--description", source.Description)
|
||||
}
|
||||
if source.Assignee != "" {
|
||||
createArgs = append(createArgs, "--assignee", source.Assignee)
|
||||
}
|
||||
for _, label := range source.Labels {
|
||||
createArgs = append(createArgs, "--label", label)
|
||||
}
|
||||
|
||||
// Create the new bead
|
||||
createCmd := exec.Command("bd", createArgs...)
|
||||
createCmd.Stderr = os.Stderr
|
||||
newIDBytes, err := createCmd.Output()
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating new bead: %w", err)
|
||||
}
|
||||
newID := strings.TrimSpace(string(newIDBytes))
|
||||
|
||||
fmt.Printf("%s Created %s\n", style.Bold.Render("✓"), newID)
|
||||
|
||||
// Close the source bead with reference
|
||||
closeReason := fmt.Sprintf("Moved to %s", newID)
|
||||
closeCmd := exec.Command("bd", "close", sourceID, "--reason", closeReason)
|
||||
closeCmd.Stderr = os.Stderr
|
||||
if err := closeCmd.Run(); err != nil {
|
||||
// Try to clean up the new bead if close fails
|
||||
fmt.Fprintf(os.Stderr, "Warning: failed to close source bead: %v\n", err)
|
||||
fmt.Fprintf(os.Stderr, "New bead %s was created but source %s remains open\n", newID, sourceID)
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("%s Closed %s (moved to %s)\n", style.Bold.Render("✓"), sourceID, newID)
|
||||
fmt.Printf("\nBead moved: %s → %s\n", sourceID, newID)
|
||||
|
||||
return nil
|
||||
}
|
||||
40
internal/cmd/close.go
Normal file
40
internal/cmd/close.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var closeCmd = &cobra.Command{
|
||||
Use: "close [bead-id...]",
|
||||
GroupID: GroupWork,
|
||||
Short: "Close one or more beads",
|
||||
Long: `Close one or more beads (wrapper for 'bd close').
|
||||
|
||||
This is a convenience command that passes through to 'bd close' with
|
||||
all arguments and flags preserved.
|
||||
|
||||
Examples:
|
||||
gt close gt-abc # Close bead gt-abc
|
||||
gt close gt-abc gt-def # Close multiple beads
|
||||
gt close --reason "Done" # Close with reason
|
||||
gt close --force # Force close pinned beads`,
|
||||
DisableFlagParsing: true, // Pass all flags through to bd close
|
||||
RunE: runClose,
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(closeCmd)
|
||||
}
|
||||
|
||||
func runClose(cmd *cobra.Command, args []string) error {
|
||||
// Build bd close command with all args passed through
|
||||
bdArgs := append([]string{"close"}, args...)
|
||||
bdCmd := exec.Command("bd", bdArgs...)
|
||||
bdCmd.Stdin = os.Stdin
|
||||
bdCmd.Stdout = os.Stdout
|
||||
bdCmd.Stderr = os.Stderr
|
||||
return bdCmd.Run()
|
||||
}
|
||||
@@ -89,6 +89,7 @@ var (
|
||||
hookMessage string
|
||||
hookDryRun bool
|
||||
hookForce bool
|
||||
hookClear bool
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -97,6 +98,7 @@ func init() {
|
||||
hookCmd.Flags().StringVarP(&hookMessage, "message", "m", "", "Message for handoff mail (optional)")
|
||||
hookCmd.Flags().BoolVarP(&hookDryRun, "dry-run", "n", false, "Show what would be done")
|
||||
hookCmd.Flags().BoolVarP(&hookForce, "force", "f", false, "Replace existing incomplete hooked bead")
|
||||
hookCmd.Flags().BoolVar(&hookClear, "clear", false, "Clear your hook (alias for 'gt unhook')")
|
||||
|
||||
// --json flag for status output (used when no args, i.e., gt hook --json)
|
||||
hookCmd.Flags().BoolVar(&moleculeJSON, "json", false, "Output as JSON (for status)")
|
||||
@@ -108,8 +110,15 @@ func init() {
|
||||
rootCmd.AddCommand(hookCmd)
|
||||
}
|
||||
|
||||
// runHookOrStatus dispatches to status or hook based on args
|
||||
// runHookOrStatus dispatches to status, clear, or hook based on args/flags
|
||||
func runHookOrStatus(cmd *cobra.Command, args []string) error {
|
||||
// --clear flag is alias for 'gt unhook'
|
||||
if hookClear {
|
||||
// Pass through dry-run and force flags
|
||||
unslingDryRun = hookDryRun
|
||||
unslingForce = hookForce
|
||||
return runUnsling(cmd, args)
|
||||
}
|
||||
if len(args) == 0 {
|
||||
// No args - show status
|
||||
return runMoleculeStatus(cmd, args)
|
||||
|
||||
Reference in New Issue
Block a user