Files
gastown/internal/cmd/mq_next.go
nux 8517ff0650 feat(mq): Add priority-ordered queue display and processing (gt-si8rq.6)
Implements priority scoring for merge queue ordering:

## Changes to gt mq list
- Add SCORE column showing priority score (higher = process first)
- Sort MRs by score descending instead of simple priority
- Add CONVOY column showing convoy ID if tracked

## New gt mq next command
- Returns highest-score MR ready for processing
- Supports --strategy=fifo for FIFO ordering fallback
- Supports --quiet for just printing MR ID
- Supports --json for programmatic access

## Changes to Refinery
- Queue() now sorts by priority score instead of simple priority
- Uses ScoreMR from mrqueue package for consistent scoring

## MR Fields Extended
- Added retry_count, last_conflict_sha, conflict_task_id
- Added convoy_id, convoy_created_at for convoy tracking
- These fields feed into priority scoring function

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-02 17:18:27 -08:00

168 lines
4.1 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 {
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
}