Files
gastown/internal/cmd/mq_next.go
Erik LaBianca 4fa6cfa0da fix(mq): skip closed MRs in list, next, and ready views (#563)
* fix(mq): skip closed MRs in list, next, and ready views (gt-qtb3w)

The gt mq list command with --status=open filter was incorrectly displaying
CLOSED merge requests as 'ready'. This occurred because bd list --status=open
was returning closed issues.

Added manual status filtering in three locations:
- mq_list.go: Filter closed MRs in all list views
- mq_next.go: Skip closed MRs when finding next ready MR
- engineer.go: Skip closed MRs in refinery's ready queue

Also fixed build error in mail_queue.go where QueueConfig struct (non-pointer)
was being compared to nil.

Workaround for upstream bd list status filter bug.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* style: fix gofmt issue in engineer.go comment block

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-16 15:23:28 -08:00

172 lines
4.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 <rig>",
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
}