- Add convoy-lifecycle.md design doc - Add formula-resolution.md design doc - Add mol-mall-design.md design doc - Add ready.go command implementation - Move dog-pool-architecture.md to docs/design/ - Update .gitignore for beads sync files Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
292 lines
7.3 KiB
Go
292 lines
7.3 KiB
Go
package cmd
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/spf13/cobra"
|
|
"github.com/steveyegge/gastown/internal/beads"
|
|
"github.com/steveyegge/gastown/internal/config"
|
|
"github.com/steveyegge/gastown/internal/constants"
|
|
"github.com/steveyegge/gastown/internal/git"
|
|
"github.com/steveyegge/gastown/internal/rig"
|
|
"github.com/steveyegge/gastown/internal/style"
|
|
"github.com/steveyegge/gastown/internal/workspace"
|
|
)
|
|
|
|
var readyJSON bool
|
|
var readyRig string
|
|
|
|
var readyCmd = &cobra.Command{
|
|
Use: "ready",
|
|
GroupID: GroupWork,
|
|
Short: "Show work ready across town",
|
|
Long: `Display all ready work items across the town and all rigs.
|
|
|
|
Aggregates ready issues from:
|
|
- Town beads (hq-* items: convoys, cross-rig coordination)
|
|
- Each rig's beads (project-level issues, MRs)
|
|
|
|
Ready items have no blockers and can be worked immediately.
|
|
Results are sorted by priority (highest first) then by source.
|
|
|
|
Examples:
|
|
gt ready # Show all ready work
|
|
gt ready --json # Output as JSON
|
|
gt ready --rig=gastown # Show only one rig`,
|
|
RunE: runReady,
|
|
}
|
|
|
|
func init() {
|
|
readyCmd.Flags().BoolVar(&readyJSON, "json", false, "Output as JSON")
|
|
readyCmd.Flags().StringVar(&readyRig, "rig", "", "Filter to a specific rig")
|
|
rootCmd.AddCommand(readyCmd)
|
|
}
|
|
|
|
// ReadySource represents ready items from a single source (town or rig).
|
|
type ReadySource struct {
|
|
Name string `json:"name"` // "town" or rig name
|
|
Issues []*beads.Issue `json:"issues"` // Ready issues from this source
|
|
Error string `json:"error,omitempty"`
|
|
}
|
|
|
|
// ReadyResult is the aggregated result of gt ready.
|
|
type ReadyResult struct {
|
|
Sources []ReadySource `json:"sources"`
|
|
Summary ReadySummary `json:"summary"`
|
|
TownRoot string `json:"town_root,omitempty"`
|
|
}
|
|
|
|
// ReadySummary provides counts for the ready report.
|
|
type ReadySummary struct {
|
|
Total int `json:"total"`
|
|
BySource map[string]int `json:"by_source"`
|
|
P0Count int `json:"p0_count"`
|
|
P1Count int `json:"p1_count"`
|
|
P2Count int `json:"p2_count"`
|
|
P3Count int `json:"p3_count"`
|
|
P4Count int `json:"p4_count"`
|
|
}
|
|
|
|
func runReady(cmd *cobra.Command, args []string) error {
|
|
// Find town root
|
|
townRoot, err := workspace.FindFromCwdOrError()
|
|
if err != nil {
|
|
return fmt.Errorf("not in a Gas Town workspace: %w", err)
|
|
}
|
|
|
|
// Load rigs config
|
|
rigsConfigPath := constants.MayorRigsPath(townRoot)
|
|
rigsConfig, err := config.LoadRigsConfig(rigsConfigPath)
|
|
if err != nil {
|
|
rigsConfig = &config.RigsConfig{Rigs: make(map[string]config.RigEntry)}
|
|
}
|
|
|
|
// Create rig manager and discover rigs
|
|
g := git.NewGit(townRoot)
|
|
mgr := rig.NewManager(townRoot, rigsConfig, g)
|
|
rigs, err := mgr.DiscoverRigs()
|
|
if err != nil {
|
|
return fmt.Errorf("discovering rigs: %w", err)
|
|
}
|
|
|
|
// Filter rigs if --rig flag provided
|
|
if readyRig != "" {
|
|
var filtered []*rig.Rig
|
|
for _, r := range rigs {
|
|
if r.Name == readyRig {
|
|
filtered = append(filtered, r)
|
|
break
|
|
}
|
|
}
|
|
if len(filtered) == 0 {
|
|
return fmt.Errorf("rig not found: %s", readyRig)
|
|
}
|
|
rigs = filtered
|
|
}
|
|
|
|
// Collect results from all sources in parallel
|
|
var wg sync.WaitGroup
|
|
var mu sync.Mutex
|
|
sources := make([]ReadySource, 0, len(rigs)+1)
|
|
|
|
// Fetch town beads (only if not filtering to a specific rig)
|
|
if readyRig == "" {
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
townBeadsPath := beads.GetTownBeadsPath(townRoot)
|
|
townBeads := beads.New(townBeadsPath)
|
|
issues, err := townBeads.Ready()
|
|
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
src := ReadySource{Name: "town"}
|
|
if err != nil {
|
|
src.Error = err.Error()
|
|
} else {
|
|
src.Issues = issues
|
|
}
|
|
sources = append(sources, src)
|
|
}()
|
|
}
|
|
|
|
// Fetch from each rig in parallel
|
|
for _, r := range rigs {
|
|
wg.Add(1)
|
|
go func(r *rig.Rig) {
|
|
defer wg.Done()
|
|
// Use mayor/rig path where rig-level beads are stored
|
|
rigBeadsPath := constants.RigMayorPath(r.Path)
|
|
rigBeads := beads.New(rigBeadsPath)
|
|
issues, err := rigBeads.Ready()
|
|
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
src := ReadySource{Name: r.Name}
|
|
if err != nil {
|
|
src.Error = err.Error()
|
|
} else {
|
|
src.Issues = issues
|
|
}
|
|
sources = append(sources, src)
|
|
}(r)
|
|
}
|
|
|
|
wg.Wait()
|
|
|
|
// Sort sources: town first, then rigs alphabetically
|
|
sort.Slice(sources, func(i, j int) bool {
|
|
if sources[i].Name == "town" {
|
|
return true
|
|
}
|
|
if sources[j].Name == "town" {
|
|
return false
|
|
}
|
|
return sources[i].Name < sources[j].Name
|
|
})
|
|
|
|
// Sort issues within each source by priority (lower number = higher priority)
|
|
for i := range sources {
|
|
sort.Slice(sources[i].Issues, func(a, b int) bool {
|
|
return sources[i].Issues[a].Priority < sources[i].Issues[b].Priority
|
|
})
|
|
}
|
|
|
|
// Build summary
|
|
summary := ReadySummary{
|
|
BySource: make(map[string]int),
|
|
}
|
|
for _, src := range sources {
|
|
count := len(src.Issues)
|
|
summary.Total += count
|
|
summary.BySource[src.Name] = count
|
|
for _, issue := range src.Issues {
|
|
switch issue.Priority {
|
|
case 0:
|
|
summary.P0Count++
|
|
case 1:
|
|
summary.P1Count++
|
|
case 2:
|
|
summary.P2Count++
|
|
case 3:
|
|
summary.P3Count++
|
|
case 4:
|
|
summary.P4Count++
|
|
}
|
|
}
|
|
}
|
|
|
|
result := ReadyResult{
|
|
Sources: sources,
|
|
Summary: summary,
|
|
TownRoot: townRoot,
|
|
}
|
|
|
|
// Output
|
|
if readyJSON {
|
|
enc := json.NewEncoder(os.Stdout)
|
|
enc.SetIndent("", " ")
|
|
return enc.Encode(result)
|
|
}
|
|
|
|
return printReadyHuman(result)
|
|
}
|
|
|
|
func printReadyHuman(result ReadyResult) error {
|
|
if result.Summary.Total == 0 {
|
|
fmt.Println("No ready work across town.")
|
|
return nil
|
|
}
|
|
|
|
fmt.Printf("%s Ready work across town:\n\n", style.Bold.Render("📋"))
|
|
|
|
for _, src := range result.Sources {
|
|
if src.Error != "" {
|
|
fmt.Printf("%s %s\n", style.Dim.Render(src.Name+"/"), style.Warning.Render("(error: "+src.Error+")"))
|
|
continue
|
|
}
|
|
|
|
count := len(src.Issues)
|
|
if count == 0 {
|
|
fmt.Printf("%s %s\n", style.Dim.Render(src.Name+"/"), style.Dim.Render("(none)"))
|
|
continue
|
|
}
|
|
|
|
fmt.Printf("%s (%d items)\n", style.Bold.Render(src.Name+"/"), count)
|
|
for _, issue := range src.Issues {
|
|
priorityStr := fmt.Sprintf("P%d", issue.Priority)
|
|
var priorityStyled string
|
|
switch issue.Priority {
|
|
case 0:
|
|
priorityStyled = style.Error.Render(priorityStr) // P0 is critical
|
|
case 1:
|
|
priorityStyled = style.Error.Render(priorityStr)
|
|
case 2:
|
|
priorityStyled = style.Warning.Render(priorityStr)
|
|
default:
|
|
priorityStyled = style.Dim.Render(priorityStr)
|
|
}
|
|
|
|
// Truncate title if too long
|
|
title := issue.Title
|
|
if len(title) > 60 {
|
|
title = title[:57] + "..."
|
|
}
|
|
|
|
fmt.Printf(" [%s] %s %s\n", priorityStyled, style.Dim.Render(issue.ID), title)
|
|
}
|
|
fmt.Println()
|
|
}
|
|
|
|
// Summary line
|
|
parts := []string{}
|
|
if result.Summary.P0Count > 0 {
|
|
parts = append(parts, fmt.Sprintf("%d P0", result.Summary.P0Count))
|
|
}
|
|
if result.Summary.P1Count > 0 {
|
|
parts = append(parts, fmt.Sprintf("%d P1", result.Summary.P1Count))
|
|
}
|
|
if result.Summary.P2Count > 0 {
|
|
parts = append(parts, fmt.Sprintf("%d P2", result.Summary.P2Count))
|
|
}
|
|
if result.Summary.P3Count > 0 {
|
|
parts = append(parts, fmt.Sprintf("%d P3", result.Summary.P3Count))
|
|
}
|
|
if result.Summary.P4Count > 0 {
|
|
parts = append(parts, fmt.Sprintf("%d P4", result.Summary.P4Count))
|
|
}
|
|
|
|
if len(parts) > 0 {
|
|
fmt.Printf("Total: %d items ready (%s)\n", result.Summary.Total, strings.Join(parts, ", "))
|
|
} else {
|
|
fmt.Printf("Total: %d items ready\n", result.Summary.Total)
|
|
}
|
|
|
|
return nil
|
|
}
|