feat(convoy): add --owner flag for targeted completion notifications

Add --owner flag to gt convoy create to track who requested a convoy.
Owner receives completion notification when convoy closes (in addition
to any --notify subscribers). Notifications are de-duplicated if owner
and notify are the same address.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
rictus
2026-01-12 19:47:14 -08:00
committed by beads/crew/emma
parent 58207a00ec
commit 392ff1d31b

View File

@@ -62,6 +62,7 @@ func looksLikeIssueID(s string) bool {
var (
convoyMolecule string
convoyNotify string
convoyOwner string
convoyStatusJSON bool
convoyListJSON bool
convoyListStatus string
@@ -121,10 +122,15 @@ var convoyCreateCmd = &cobra.Command{
The convoy is created in town-level beads (hq-* prefix) and can track
issues across any rig.
The --owner flag specifies who requested the convoy (receives completion
notification by default). If not specified, defaults to created_by.
The --notify flag adds additional subscribers beyond the owner.
Examples:
gt convoy create "Deploy v2.0" gt-abc bd-xyz
gt convoy create "Release prep" gt-abc --notify # defaults to mayor/
gt convoy create "Release prep" gt-abc --notify ops/ # notify ops/
gt convoy create "Feature rollout" gt-a gt-b --owner mayor/ --notify ops/
gt convoy create "Feature rollout" gt-a gt-b gt-c --molecule mol-release`,
Args: cobra.MinimumNArgs(1),
RunE: runConvoyCreate,
@@ -225,7 +231,8 @@ Examples:
func init() {
// Create flags
convoyCreateCmd.Flags().StringVar(&convoyMolecule, "molecule", "", "Associated molecule ID")
convoyCreateCmd.Flags().StringVar(&convoyNotify, "notify", "", "Address to notify on completion (default: mayor/ if flag used without value)")
convoyCreateCmd.Flags().StringVar(&convoyOwner, "owner", "", "Owner who requested convoy (gets completion notification)")
convoyCreateCmd.Flags().StringVar(&convoyNotify, "notify", "", "Additional address to notify on completion (default: mayor/ if flag used without value)")
convoyCreateCmd.Flags().Lookup("notify").NoOptDefVal = "mayor/"
// Status flags
@@ -291,6 +298,9 @@ func runConvoyCreate(cmd *cobra.Command, args []string) error {
// Create convoy issue in town beads
description := fmt.Sprintf("Convoy tracking %d issues", len(trackedIssues))
if convoyOwner != "" {
description += fmt.Sprintf("\nOwner: %s", convoyOwner)
}
if convoyNotify != "" {
description += fmt.Sprintf("\nNotify: %s", convoyNotify)
}
@@ -345,6 +355,9 @@ func runConvoyCreate(cmd *cobra.Command, args []string) error {
if len(trackedIssues) > 0 {
fmt.Printf(" Issues: %s\n", strings.Join(trackedIssues, ", "))
}
if convoyOwner != "" {
fmt.Printf(" Owner: %s\n", convoyOwner)
}
if convoyNotify != "" {
fmt.Printf(" Notify: %s\n", convoyNotify)
}
@@ -786,9 +799,9 @@ func checkAndCloseCompletedConvoys(townBeads string) ([]struct{ ID, Title string
return closed, nil
}
// notifyConvoyCompletion sends a notification if the convoy has a notify address.
// notifyConvoyCompletion sends notifications to owner and any notify addresses.
func notifyConvoyCompletion(townBeads, convoyID, title string) {
// Get convoy description to find notify address
// Get convoy description to find owner and notify addresses
showArgs := []string{"show", convoyID, "--json"}
showCmd := exec.Command("bd", showArgs...)
showCmd.Dir = townBeads
@@ -806,20 +819,26 @@ func notifyConvoyCompletion(townBeads, convoyID, title string) {
return
}
// Parse notify address from description
// Parse owner and notify addresses from description
desc := convoys[0].Description
notified := make(map[string]bool) // Track who we've notified to avoid duplicates
for _, line := range strings.Split(desc, "\n") {
if strings.HasPrefix(line, "Notify: ") {
addr := strings.TrimPrefix(line, "Notify: ")
if addr != "" {
// Send notification via gt mail
mailArgs := []string{"mail", "send", addr,
"-s", fmt.Sprintf("🚚 Convoy landed: %s", title),
"-m", fmt.Sprintf("Convoy %s has completed.\n\nAll tracked issues are now closed.", convoyID)}
mailCmd := exec.Command("gt", mailArgs...)
_ = mailCmd.Run() // Best effort, ignore errors
}
break
var addr string
if strings.HasPrefix(line, "Owner: ") {
addr = strings.TrimPrefix(line, "Owner: ")
} else if strings.HasPrefix(line, "Notify: ") {
addr = strings.TrimPrefix(line, "Notify: ")
}
if addr != "" && !notified[addr] {
// Send notification via gt mail
mailArgs := []string{"mail", "send", addr,
"-s", fmt.Sprintf("🚚 Convoy landed: %s", title),
"-m", fmt.Sprintf("Convoy %s has completed.\n\nAll tracked issues are now closed.", convoyID)}
mailCmd := exec.Command("gt", mailArgs...)
_ = mailCmd.Run() // Best effort, ignore errors
notified[addr] = true
}
}
}