feat: add overseer experience commands (gt focus, gt attention)
Some checks failed
CI / Check for .beads changes (push) Has been skipped
CI / Check embedded formulas (push) Failing after 15s
CI / Test (push) Failing after 1m29s
CI / Lint (push) Failing after 19s
CI / Integration Tests (push) Successful in 1m14s
CI / Coverage Report (push) Has been skipped
Windows CI / Windows Build and Unit Tests (push) Has been cancelled
Some checks failed
CI / Check for .beads changes (push) Has been skipped
CI / Check embedded formulas (push) Failing after 15s
CI / Test (push) Failing after 1m29s
CI / Lint (push) Failing after 19s
CI / Integration Tests (push) Successful in 1m14s
CI / Coverage Report (push) Has been skipped
Windows CI / Windows Build and Unit Tests (push) Has been cancelled
Implements the Overseer Experience epic (gt-k0kn): - gt focus: Shows stalest high-priority goals, sorted by priority × staleness - gt attention: Shows blocked items, PRs awaiting review, stuck workers - gt status: Now includes GOALS and ATTENTION summary sections - gt convoy list: Added --orphans, --epic, --by-epic flags These commands reduce Mayor bottleneck by giving the overseer direct visibility into system state without needing to ask Mayor. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -9,6 +9,7 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -69,6 +70,9 @@ var (
|
||||
convoyListStatus string
|
||||
convoyListAll bool
|
||||
convoyListTree bool
|
||||
convoyListOrphans bool
|
||||
convoyListEpic string
|
||||
convoyListByEpic bool
|
||||
convoyInteractive bool
|
||||
convoyStrandedJSON bool
|
||||
convoyCloseReason string
|
||||
@@ -159,6 +163,9 @@ Examples:
|
||||
gt convoy list --all # All convoys (open + closed)
|
||||
gt convoy list --status=closed # Recently landed
|
||||
gt convoy list --tree # Show convoy + child status tree
|
||||
gt convoy list --orphans # Convoys with no parent epic
|
||||
gt convoy list --epic gt-abc # Convoys linked to specific epic
|
||||
gt convoy list --by-epic # Group convoys by parent epic
|
||||
gt convoy list --json`,
|
||||
RunE: runConvoyList,
|
||||
}
|
||||
@@ -253,6 +260,9 @@ func init() {
|
||||
convoyListCmd.Flags().StringVar(&convoyListStatus, "status", "", "Filter by status (open, closed)")
|
||||
convoyListCmd.Flags().BoolVar(&convoyListAll, "all", false, "Show all convoys (open and closed)")
|
||||
convoyListCmd.Flags().BoolVar(&convoyListTree, "tree", false, "Show convoy + child status tree")
|
||||
convoyListCmd.Flags().BoolVar(&convoyListOrphans, "orphans", false, "Show only orphan convoys (no parent epic)")
|
||||
convoyListCmd.Flags().StringVar(&convoyListEpic, "epic", "", "Show convoys for a specific epic")
|
||||
convoyListCmd.Flags().BoolVar(&convoyListByEpic, "by-epic", false, "Group convoys by parent epic")
|
||||
|
||||
// Interactive TUI flag (on parent command)
|
||||
convoyCmd.Flags().BoolVarP(&convoyInteractive, "interactive", "i", false, "Interactive tree view")
|
||||
@@ -1163,6 +1173,16 @@ func showAllConvoyStatus(townBeads string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// convoyListItem holds convoy info for list display.
|
||||
type convoyListItem struct {
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Status string `json:"status"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
ParentEpic string `json:"parent_epic,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
}
|
||||
|
||||
func runConvoyList(cmd *cobra.Command, args []string) error {
|
||||
townBeads, err := getTownBeadsDir()
|
||||
if err != nil {
|
||||
@@ -1187,16 +1207,58 @@ func runConvoyList(cmd *cobra.Command, args []string) error {
|
||||
return fmt.Errorf("listing convoys: %w", err)
|
||||
}
|
||||
|
||||
var convoys []struct {
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Status string `json:"status"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
var rawConvoys []struct {
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Status string `json:"status"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
if err := json.Unmarshal(stdout.Bytes(), &convoys); err != nil {
|
||||
if err := json.Unmarshal(stdout.Bytes(), &rawConvoys); err != nil {
|
||||
return fmt.Errorf("parsing convoy list: %w", err)
|
||||
}
|
||||
|
||||
// Convert to convoyListItem and extract parent_epic from description
|
||||
convoys := make([]convoyListItem, 0, len(rawConvoys))
|
||||
for _, rc := range rawConvoys {
|
||||
item := convoyListItem{
|
||||
ID: rc.ID,
|
||||
Title: rc.Title,
|
||||
Status: rc.Status,
|
||||
CreatedAt: rc.CreatedAt,
|
||||
Description: rc.Description,
|
||||
}
|
||||
// Extract parent_epic from description (format: "Parent-Epic: xxx")
|
||||
for _, line := range strings.Split(rc.Description, "\n") {
|
||||
if strings.HasPrefix(line, "Parent-Epic: ") {
|
||||
item.ParentEpic = strings.TrimPrefix(line, "Parent-Epic: ")
|
||||
break
|
||||
}
|
||||
}
|
||||
convoys = append(convoys, item)
|
||||
}
|
||||
|
||||
// Apply filtering based on new flags
|
||||
if convoyListOrphans {
|
||||
// Filter to only orphan convoys (no parent epic)
|
||||
filtered := make([]convoyListItem, 0)
|
||||
for _, c := range convoys {
|
||||
if c.ParentEpic == "" {
|
||||
filtered = append(filtered, c)
|
||||
}
|
||||
}
|
||||
convoys = filtered
|
||||
} else if convoyListEpic != "" {
|
||||
// Filter to convoys linked to specific epic
|
||||
filtered := make([]convoyListItem, 0)
|
||||
for _, c := range convoys {
|
||||
if c.ParentEpic == convoyListEpic {
|
||||
filtered = append(filtered, c)
|
||||
}
|
||||
}
|
||||
convoys = filtered
|
||||
}
|
||||
|
||||
if convoyListJSON {
|
||||
enc := json.NewEncoder(os.Stdout)
|
||||
enc.SetIndent("", " ")
|
||||
@@ -1204,26 +1266,133 @@ func runConvoyList(cmd *cobra.Command, args []string) error {
|
||||
}
|
||||
|
||||
if len(convoys) == 0 {
|
||||
fmt.Println("No convoys found.")
|
||||
if convoyListOrphans {
|
||||
fmt.Println("No orphan convoys found.")
|
||||
} else if convoyListEpic != "" {
|
||||
fmt.Printf("No convoys found for epic %s.\n", convoyListEpic)
|
||||
} else {
|
||||
fmt.Println("No convoys found.")
|
||||
}
|
||||
fmt.Println("Create a convoy with: gt convoy create <name> [issues...]")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Group by epic view
|
||||
if convoyListByEpic {
|
||||
return printConvoysByEpic(townBeads, convoys)
|
||||
}
|
||||
|
||||
// Tree view: show convoys with their child issues
|
||||
if convoyListTree {
|
||||
return printConvoyTree(townBeads, convoys)
|
||||
return printConvoyTreeFromItems(townBeads, convoys)
|
||||
}
|
||||
|
||||
fmt.Printf("%s\n\n", style.Bold.Render("Convoys"))
|
||||
for i, c := range convoys {
|
||||
status := formatConvoyStatus(c.Status)
|
||||
fmt.Printf(" %d. 🚚 %s: %s %s\n", i+1, c.ID, c.Title, status)
|
||||
epicSuffix := ""
|
||||
if c.ParentEpic != "" {
|
||||
epicSuffix = style.Dim.Render(fmt.Sprintf(" [%s]", c.ParentEpic))
|
||||
}
|
||||
fmt.Printf(" %d. 🚚 %s: %s %s%s\n", i+1, c.ID, c.Title, status, epicSuffix)
|
||||
}
|
||||
fmt.Printf("\nUse 'gt convoy status <id>' or 'gt convoy status <n>' for detailed view.\n")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// printConvoysByEpic groups and displays convoys by their parent epic.
|
||||
func printConvoysByEpic(townBeads string, convoys []convoyListItem) error {
|
||||
// Group convoys by parent epic
|
||||
byEpic := make(map[string][]convoyListItem)
|
||||
for _, c := range convoys {
|
||||
epic := c.ParentEpic
|
||||
if epic == "" {
|
||||
epic = "(No Epic)"
|
||||
}
|
||||
byEpic[epic] = append(byEpic[epic], c)
|
||||
}
|
||||
|
||||
// Get sorted epic keys (No Epic last)
|
||||
var epics []string
|
||||
for epic := range byEpic {
|
||||
if epic != "(No Epic)" {
|
||||
epics = append(epics, epic)
|
||||
}
|
||||
}
|
||||
sort.Strings(epics)
|
||||
if _, ok := byEpic["(No Epic)"]; ok {
|
||||
epics = append(epics, "(No Epic)")
|
||||
}
|
||||
|
||||
// Print grouped output
|
||||
for _, epic := range epics {
|
||||
convoys := byEpic[epic]
|
||||
fmt.Printf("%s (%d convoys)\n", style.Bold.Render(epic), len(convoys))
|
||||
for _, c := range convoys {
|
||||
status := formatConvoyStatus(c.Status)
|
||||
fmt.Printf(" 🚚 %s: %s %s\n", c.ID, c.Title, status)
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// printConvoyTreeFromItems displays convoys with their child issues in a tree format.
|
||||
func printConvoyTreeFromItems(townBeads string, convoys []convoyListItem) error {
|
||||
for _, c := range convoys {
|
||||
// Get tracked issues for this convoy
|
||||
tracked := getTrackedIssues(townBeads, c.ID)
|
||||
|
||||
// Count completed
|
||||
completed := 0
|
||||
for _, t := range tracked {
|
||||
if t.Status == "closed" {
|
||||
completed++
|
||||
}
|
||||
}
|
||||
|
||||
// Print convoy header with progress
|
||||
total := len(tracked)
|
||||
progress := ""
|
||||
if total > 0 {
|
||||
progress = fmt.Sprintf(" (%d/%d)", completed, total)
|
||||
}
|
||||
epicSuffix := ""
|
||||
if c.ParentEpic != "" {
|
||||
epicSuffix = style.Dim.Render(fmt.Sprintf(" [%s]", c.ParentEpic))
|
||||
}
|
||||
fmt.Printf("🚚 %s: %s%s%s\n", c.ID, c.Title, progress, epicSuffix)
|
||||
|
||||
// Print tracked issues as tree children
|
||||
for i, t := range tracked {
|
||||
// Determine tree connector
|
||||
isLast := i == len(tracked)-1
|
||||
connector := "├──"
|
||||
if isLast {
|
||||
connector = "└──"
|
||||
}
|
||||
|
||||
// Status symbol: ✓ closed, ▶ in_progress/hooked, ○ other
|
||||
status := "○"
|
||||
switch t.Status {
|
||||
case "closed":
|
||||
status = "✓"
|
||||
case "in_progress", "hooked":
|
||||
status = "▶"
|
||||
}
|
||||
|
||||
fmt.Printf("%s %s %s: %s\n", connector, status, t.ID, t.Title)
|
||||
}
|
||||
|
||||
// Add blank line between convoys
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// printConvoyTree displays convoys with their child issues in a tree format.
|
||||
func printConvoyTree(townBeads string, convoys []struct {
|
||||
ID string `json:"id"`
|
||||
|
||||
Reference in New Issue
Block a user