feat(convoy): add close command for manual convoy closure
Add `gt convoy close` command to manually close convoys regardless of tracked issue status. This addresses the desire path identified in convoy-lifecycle.md. Features: - Close convoy with optional --reason flag - Send notification with optional --notify flag - Idempotent: closing already-closed convoy is a no-op - Validates convoy type before closing Closes hq-2i8yw Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
committed by
beads/crew/emma
parent
6b2a7438e1
commit
e442212c05
@@ -69,6 +69,8 @@ var (
|
|||||||
convoyListTree bool
|
convoyListTree bool
|
||||||
convoyInteractive bool
|
convoyInteractive bool
|
||||||
convoyStrandedJSON bool
|
convoyStrandedJSON bool
|
||||||
|
convoyCloseReason string
|
||||||
|
convoyCloseNotify string
|
||||||
)
|
)
|
||||||
|
|
||||||
var convoyCmd = &cobra.Command{
|
var convoyCmd = &cobra.Command{
|
||||||
@@ -106,6 +108,7 @@ TRACKING SEMANTICS:
|
|||||||
COMMANDS:
|
COMMANDS:
|
||||||
create Create a convoy tracking specified issues
|
create Create a convoy tracking specified issues
|
||||||
add Add issues to an existing convoy (reopens if closed)
|
add Add issues to an existing convoy (reopens if closed)
|
||||||
|
close Close a convoy (manually, regardless of tracked issue status)
|
||||||
status Show convoy progress, tracked issues, and active workers
|
status Show convoy progress, tracked issues, and active workers
|
||||||
list List convoys (the dashboard view)`,
|
list List convoys (the dashboard view)`,
|
||||||
}
|
}
|
||||||
@@ -199,6 +202,26 @@ Examples:
|
|||||||
RunE: runConvoyStranded,
|
RunE: runConvoyStranded,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var convoyCloseCmd = &cobra.Command{
|
||||||
|
Use: "close <convoy-id>",
|
||||||
|
Short: "Close a convoy",
|
||||||
|
Long: `Close a convoy, optionally with a reason.
|
||||||
|
|
||||||
|
Closes the convoy regardless of tracked issue status. Use this to:
|
||||||
|
- Force-close abandoned convoys no longer relevant
|
||||||
|
- Close convoys where work completed outside the tracked path
|
||||||
|
- Manually close stuck convoys
|
||||||
|
|
||||||
|
The close is idempotent - closing an already-closed convoy is a no-op.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
gt convoy close hq-cv-abc
|
||||||
|
gt convoy close hq-cv-abc --reason="work done differently"
|
||||||
|
gt convoy close hq-cv-xyz --notify mayor/`,
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
RunE: runConvoyClose,
|
||||||
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
// Create flags
|
// Create flags
|
||||||
convoyCreateCmd.Flags().StringVar(&convoyMolecule, "molecule", "", "Associated molecule ID")
|
convoyCreateCmd.Flags().StringVar(&convoyMolecule, "molecule", "", "Associated molecule ID")
|
||||||
@@ -220,6 +243,10 @@ func init() {
|
|||||||
// Stranded flags
|
// Stranded flags
|
||||||
convoyStrandedCmd.Flags().BoolVar(&convoyStrandedJSON, "json", false, "Output as JSON")
|
convoyStrandedCmd.Flags().BoolVar(&convoyStrandedJSON, "json", false, "Output as JSON")
|
||||||
|
|
||||||
|
// Close flags
|
||||||
|
convoyCloseCmd.Flags().StringVar(&convoyCloseReason, "reason", "", "Reason for closing the convoy")
|
||||||
|
convoyCloseCmd.Flags().StringVar(&convoyCloseNotify, "notify", "", "Agent to notify on close (e.g., mayor/)")
|
||||||
|
|
||||||
// Add subcommands
|
// Add subcommands
|
||||||
convoyCmd.AddCommand(convoyCreateCmd)
|
convoyCmd.AddCommand(convoyCreateCmd)
|
||||||
convoyCmd.AddCommand(convoyStatusCmd)
|
convoyCmd.AddCommand(convoyStatusCmd)
|
||||||
@@ -227,6 +254,7 @@ func init() {
|
|||||||
convoyCmd.AddCommand(convoyAddCmd)
|
convoyCmd.AddCommand(convoyAddCmd)
|
||||||
convoyCmd.AddCommand(convoyCheckCmd)
|
convoyCmd.AddCommand(convoyCheckCmd)
|
||||||
convoyCmd.AddCommand(convoyStrandedCmd)
|
convoyCmd.AddCommand(convoyStrandedCmd)
|
||||||
|
convoyCmd.AddCommand(convoyCloseCmd)
|
||||||
|
|
||||||
rootCmd.AddCommand(convoyCmd)
|
rootCmd.AddCommand(convoyCmd)
|
||||||
}
|
}
|
||||||
@@ -432,6 +460,98 @@ func runConvoyCheck(cmd *cobra.Command, args []string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func runConvoyClose(cmd *cobra.Command, args []string) error {
|
||||||
|
convoyID := args[0]
|
||||||
|
|
||||||
|
townBeads, err := getTownBeadsDir()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get convoy details
|
||||||
|
showArgs := []string{"show", convoyID, "--json"}
|
||||||
|
showCmd := exec.Command("bd", showArgs...)
|
||||||
|
showCmd.Dir = townBeads
|
||||||
|
var stdout bytes.Buffer
|
||||||
|
showCmd.Stdout = &stdout
|
||||||
|
|
||||||
|
if err := showCmd.Run(); err != nil {
|
||||||
|
return fmt.Errorf("convoy '%s' not found", convoyID)
|
||||||
|
}
|
||||||
|
|
||||||
|
var convoys []struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Type string `json:"issue_type"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(stdout.Bytes(), &convoys); err != nil {
|
||||||
|
return fmt.Errorf("parsing convoy data: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(convoys) == 0 {
|
||||||
|
return fmt.Errorf("convoy '%s' not found", convoyID)
|
||||||
|
}
|
||||||
|
|
||||||
|
convoy := convoys[0]
|
||||||
|
|
||||||
|
// Verify it's actually a convoy type
|
||||||
|
if convoy.Type != "convoy" {
|
||||||
|
return fmt.Errorf("'%s' is not a convoy (type: %s)", convoyID, convoy.Type)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Idempotent: if already closed, just report it
|
||||||
|
if convoy.Status == "closed" {
|
||||||
|
fmt.Printf("%s Convoy %s is already closed\n", style.Dim.Render("○"), convoyID)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build close reason
|
||||||
|
reason := convoyCloseReason
|
||||||
|
if reason == "" {
|
||||||
|
reason = "Manually closed"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close the convoy
|
||||||
|
closeArgs := []string{"close", convoyID, "-r", reason}
|
||||||
|
closeCmd := exec.Command("bd", closeArgs...)
|
||||||
|
closeCmd.Dir = townBeads
|
||||||
|
|
||||||
|
if err := closeCmd.Run(); err != nil {
|
||||||
|
return fmt.Errorf("closing convoy: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("%s Closed convoy 🚚 %s: %s\n", style.Bold.Render("✓"), convoyID, convoy.Title)
|
||||||
|
if convoyCloseReason != "" {
|
||||||
|
fmt.Printf(" Reason: %s\n", convoyCloseReason)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send notification if --notify flag provided
|
||||||
|
if convoyCloseNotify != "" {
|
||||||
|
sendCloseNotification(convoyCloseNotify, convoyID, convoy.Title, reason)
|
||||||
|
} else {
|
||||||
|
// Check if convoy has a notify address in description
|
||||||
|
notifyConvoyCompletion(townBeads, convoyID, convoy.Title)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// sendCloseNotification sends a notification about convoy closure.
|
||||||
|
func sendCloseNotification(addr, convoyID, title, reason string) {
|
||||||
|
subject := fmt.Sprintf("🚚 Convoy closed: %s", title)
|
||||||
|
body := fmt.Sprintf("Convoy %s has been closed.\n\nReason: %s", convoyID, reason)
|
||||||
|
|
||||||
|
mailArgs := []string{"mail", "send", addr, "-s", subject, "-m", body}
|
||||||
|
mailCmd := exec.Command("gt", mailArgs...)
|
||||||
|
if err := mailCmd.Run(); err != nil {
|
||||||
|
style.PrintWarning("couldn't send notification: %v", err)
|
||||||
|
} else {
|
||||||
|
fmt.Printf(" Notified: %s\n", addr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// strandedConvoyInfo holds info about a stranded convoy.
|
// strandedConvoyInfo holds info about a stranded convoy.
|
||||||
type strandedConvoyInfo struct {
|
type strandedConvoyInfo struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
|
|||||||
Reference in New Issue
Block a user