package cmd import ( "fmt" "sort" "time" "github.com/spf13/cobra" "github.com/steveyegge/gastown/internal/beads" "github.com/steveyegge/gastown/internal/style" ) // MQ next command flags var ( mqNextStrategy string // "priority" (default) or "fifo" mqNextJSON bool mqNextQuiet bool ) var mqNextCmd = &cobra.Command{ Use: "next ", Short: "Show the highest-priority merge request", Long: `Show the next merge request to process based on priority score. The priority scoring function considers: - Convoy age: Older convoys get higher priority (starvation prevention) - Issue priority: P0 > P1 > P2 > P3 > P4 - Retry count: MRs that fail repeatedly get deprioritized - MR age: FIFO tiebreaker for same priority/convoy Use --strategy=fifo for first-in-first-out ordering instead. Examples: gt mq next gastown # Show highest-priority MR gt mq next gastown --strategy=fifo # Show oldest MR instead gt mq next gastown --quiet # Just print the MR ID gt mq next gastown --json # Output as JSON`, Args: cobra.ExactArgs(1), RunE: runMQNext, } func init() { mqNextCmd.Flags().StringVar(&mqNextStrategy, "strategy", "priority", "Ordering strategy: 'priority' or 'fifo'") mqNextCmd.Flags().BoolVar(&mqNextJSON, "json", false, "Output as JSON") mqNextCmd.Flags().BoolVarP(&mqNextQuiet, "quiet", "q", false, "Just print the MR ID") mqCmd.AddCommand(mqNextCmd) } func runMQNext(cmd *cobra.Command, args []string) error { rigName := args[0] _, r, _, err := getRefineryManager(rigName) if err != nil { return err } // Create beads wrapper for the rig b := beads.New(r.BeadsPath()) // Query for open merge-requests (ready to process) opts := beads.ListOptions{ Type: "merge-request", Status: "open", Priority: -1, // No priority filter } issues, err := b.List(opts) if err != nil { return fmt.Errorf("querying merge queue: %w", err) } // Filter to only ready MRs (no blockers) var ready []*beads.Issue for _, issue := range issues { // Skip closed MRs (workaround for bd list not respecting --status filter) if issue.Status != "open" { continue } if len(issue.BlockedBy) == 0 && issue.BlockedByCount == 0 { ready = append(ready, issue) } } if len(ready) == 0 { if mqNextQuiet { return nil // Silent exit } fmt.Printf("%s No ready merge requests in queue\n", style.Dim.Render("ℹ")) return nil } now := time.Now() // Sort based on strategy if mqNextStrategy == "fifo" { // FIFO: oldest first by creation time sort.Slice(ready, func(i, j int) bool { ti, _ := time.Parse(time.RFC3339, ready[i].CreatedAt) tj, _ := time.Parse(time.RFC3339, ready[j].CreatedAt) return ti.Before(tj) }) } else { // Priority: highest score first type scoredIssue struct { issue *beads.Issue score float64 } scored := make([]scoredIssue, len(ready)) for i, issue := range ready { fields := beads.ParseMRFields(issue) score := calculateMRScore(issue, fields, now) scored[i] = scoredIssue{issue: issue, score: score} } sort.Slice(scored, func(i, j int) bool { return scored[i].score > scored[j].score }) // Rebuild ready slice in sorted order for i, s := range scored { ready[i] = s.issue } } // Get the top MR next := ready[0] fields := beads.ParseMRFields(next) // Output based on format flags if mqNextQuiet { fmt.Println(next.ID) return nil } if mqNextJSON { return outputJSON(next) } // Human-readable output fmt.Printf("%s Next MR to process:\n\n", style.Bold.Render("🎯")) score := calculateMRScore(next, fields, now) fmt.Printf(" ID: %s\n", next.ID) fmt.Printf(" Score: %.1f\n", score) fmt.Printf(" Priority: P%d\n", next.Priority) if fields != nil { if fields.Branch != "" { fmt.Printf(" Branch: %s\n", fields.Branch) } if fields.Worker != "" { fmt.Printf(" Worker: %s\n", fields.Worker) } if fields.ConvoyID != "" { fmt.Printf(" Convoy: %s\n", fields.ConvoyID) } if fields.RetryCount > 0 { fmt.Printf(" Retries: %d\n", fields.RetryCount) } } fmt.Printf(" Age: %s\n", formatMRAge(next.CreatedAt)) if len(ready) > 1 { fmt.Printf("\n %s\n", style.Dim.Render(fmt.Sprintf("(%d more in queue)", len(ready)-1))) } return nil }