feat(sling): implement --convoy flag logic (gt-9o4)

Add --convoy flag to gt sling that allows adding an issue to an existing
convoy instead of creating a new one. When specified:
- Validates the convoy exists and is open
- Adds tracking relation between convoy and issue
- Skips auto-convoy creation

Changes:
- Add slingConvoy variable and --convoy flag registration
- Add addToExistingConvoy() helper function in sling_convoy.go
- Modify auto-convoy logic to check slingConvoy first

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
nux
2026-01-22 17:31:08 -08:00
committed by John Ogle
parent 8dd3bfcd63
commit 0fcbdee98b
2 changed files with 55 additions and 4 deletions

View File

@@ -114,9 +114,9 @@ func init() {
slingCmd.Flags().StringVar(&slingAccount, "account", "", "Claude Code account handle to use")
slingCmd.Flags().StringVar(&slingAgent, "agent", "", "Override agent/runtime for this sling (e.g., claude, gemini, codex, or custom alias)")
slingCmd.Flags().BoolVar(&slingNoConvoy, "no-convoy", false, "Skip auto-convoy creation for single-issue sling")
slingCmd.Flags().BoolVar(&slingHookRawBead, "hook-raw-bead", false, "Hook raw bead without default formula (expert mode)")
slingCmd.Flags().StringVar(&slingEpic, "epic", "", "Link auto-created convoy to parent epic")
slingCmd.Flags().StringVar(&slingConvoy, "convoy", "", "Add to existing convoy instead of creating new")
slingCmd.Flags().BoolVar(&slingHookRawBead, "hook-raw-bead", false, "Hook raw bead without default formula (expert mode)")
rootCmd.AddCommand(slingCmd)
}
@@ -380,9 +380,21 @@ func runSling(cmd *cobra.Command, args []string) error {
}
}
// Auto-convoy: check if issue is already tracked by a convoy
// If not, create one for dashboard visibility (unless --no-convoy is set)
if !slingNoConvoy && formulaName == "" {
// Convoy handling: --convoy adds to existing, otherwise auto-create (unless --no-convoy)
if slingConvoy != "" {
// Use existing convoy specified by --convoy flag
if slingDryRun {
fmt.Printf("Would add to convoy %s\n", slingConvoy)
fmt.Printf("Would add tracking relation to %s\n", beadID)
} else {
if err := addToExistingConvoy(slingConvoy, beadID); err != nil {
return fmt.Errorf("adding to convoy: %w", err)
}
fmt.Printf("%s Added to convoy %s\n", style.Bold.Render("→"), slingConvoy)
}
} else if !slingNoConvoy && formulaName == "" {
// Auto-convoy: check if issue is already tracked by a convoy
// If not, create one for dashboard visibility
existingConvoy := isTrackedByConvoy(beadID)
if existingConvoy == "" {
if slingDryRun {

View File

@@ -109,6 +109,45 @@ func createAutoConvoy(beadID, beadTitle string) (string, error) {
return convoyID, nil
}
// addToExistingConvoy adds a bead to an existing convoy by creating a tracking relation.
// Returns an error if the convoy doesn't exist or the tracking relation fails.
func addToExistingConvoy(convoyID, beadID string) error {
townRoot, err := workspace.FindFromCwd()
if err != nil {
return fmt.Errorf("finding town root: %w", err)
}
townBeads := filepath.Join(townRoot, ".beads")
dbPath := filepath.Join(townBeads, "beads.db")
// Verify convoy exists and is open
query := fmt.Sprintf(`
SELECT id FROM issues
WHERE id = '%s'
AND issue_type = 'convoy'
AND status = 'open'
`, convoyID)
queryCmd := exec.Command("sqlite3", dbPath, query)
out, err := queryCmd.Output()
if err != nil || strings.TrimSpace(string(out)) == "" {
return fmt.Errorf("convoy %s not found or not open", convoyID)
}
// Add tracking relation: convoy tracks the issue
trackBeadID := formatTrackBeadID(beadID)
depArgs := []string{"--no-daemon", "dep", "add", convoyID, trackBeadID, "--type=tracks"}
depCmd := exec.Command("bd", depArgs...)
depCmd.Dir = townBeads
depCmd.Stderr = os.Stderr
if err := depCmd.Run(); err != nil {
return fmt.Errorf("adding tracking relation: %w", err)
}
return nil
}
// formatTrackBeadID formats a bead ID for use in convoy tracking dependencies.
// Cross-rig beads (non-hq- prefixed) are formatted as external references
// so the bd tool can resolve them when running from HQ context.