fix(goals): query epics from all rigs, not just default
gt goals was only querying the default beads location (town-level with hq- prefix), missing epics from rig-level beads (j-, sc-, etc.). Now iterates over all rig directories with .beads/ subdirectories and aggregates epics, deduplicating by ID.
This commit is contained in:
@@ -6,6 +6,7 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -13,6 +14,7 @@ import (
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/steveyegge/gastown/internal/style"
|
||||
"github.com/steveyegge/gastown/internal/workspace"
|
||||
)
|
||||
|
||||
// Goal command flags
|
||||
@@ -169,45 +171,16 @@ func showGoal(goalID string) error {
|
||||
}
|
||||
|
||||
func listGoals() error {
|
||||
// Build list args - bd has its own routing to find the right beads DB
|
||||
listArgs := []string{"list", "--type=epic", "--json"}
|
||||
if goalsStatus != "" && goalsStatus != "open" {
|
||||
if goalsStatus == "all" {
|
||||
listArgs = append(listArgs, "--all")
|
||||
} else {
|
||||
listArgs = append(listArgs, "--status="+goalsStatus)
|
||||
}
|
||||
}
|
||||
|
||||
listCmd := exec.Command("bd", listArgs...)
|
||||
var stdout bytes.Buffer
|
||||
listCmd.Stdout = &stdout
|
||||
|
||||
if err := listCmd.Run(); err != nil {
|
||||
return fmt.Errorf("listing goals: %w", err)
|
||||
}
|
||||
|
||||
var epics []struct {
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Status string `json:"status"`
|
||||
Priority int `json:"priority"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
}
|
||||
if err := json.Unmarshal(stdout.Bytes(), &epics); err != nil {
|
||||
return fmt.Errorf("parsing goals list: %w", err)
|
||||
// Collect epics from all rigs (goals are cross-rig strategic objectives)
|
||||
epics, err := collectEpicsFromAllRigs()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Filter out wisp molecules by default (transient/operational, not strategic goals)
|
||||
// These have IDs like "gt-wisp-*" and are molecule-tracking beads, not human goals
|
||||
if !goalsIncludeWisp {
|
||||
filtered := make([]struct {
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Status string `json:"status"`
|
||||
Priority int `json:"priority"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
}, 0)
|
||||
filtered := make([]epicRecord, 0)
|
||||
for _, e := range epics {
|
||||
if !isWispEpic(e.ID, e.Title) {
|
||||
filtered = append(filtered, e)
|
||||
@@ -219,13 +192,7 @@ func listGoals() error {
|
||||
// Filter by priority if specified
|
||||
if goalsPriority != "" {
|
||||
targetPriority := parsePriority(goalsPriority)
|
||||
filtered := make([]struct {
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Status string `json:"status"`
|
||||
Priority int `json:"priority"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
}, 0)
|
||||
filtered := make([]epicRecord, 0)
|
||||
for _, e := range epics {
|
||||
if e.Priority == targetPriority {
|
||||
filtered = append(filtered, e)
|
||||
@@ -483,3 +450,112 @@ func isWispEpic(id, title string) bool {
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// epicRecord represents an epic from bd list output.
|
||||
type epicRecord struct {
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Status string `json:"status"`
|
||||
Priority int `json:"priority"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
}
|
||||
|
||||
// collectEpicsFromAllRigs queries all rigs for epics and aggregates them.
|
||||
// Goals are cross-rig strategic objectives, so we need to query each rig's beads.
|
||||
func collectEpicsFromAllRigs() ([]epicRecord, error) {
|
||||
var allEpics []epicRecord
|
||||
seen := make(map[string]bool) // Deduplicate by ID
|
||||
|
||||
// Find the town root
|
||||
townRoot, err := workspace.FindFromCwdOrError()
|
||||
if err != nil {
|
||||
// Not in a Gas Town workspace, fall back to single query
|
||||
return queryEpicsInDir("")
|
||||
}
|
||||
|
||||
// Also query town-level beads (for hq- prefixed epics)
|
||||
townBeadsDir := filepath.Join(townRoot, ".beads")
|
||||
if _, err := os.Stat(townBeadsDir); err == nil {
|
||||
epics, err := queryEpicsInDir(townRoot)
|
||||
if err == nil {
|
||||
for _, e := range epics {
|
||||
if !seen[e.ID] {
|
||||
seen[e.ID] = true
|
||||
allEpics = append(allEpics, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Find all rig directories (they have .beads/ subdirectories)
|
||||
entries, err := os.ReadDir(townRoot)
|
||||
if err != nil {
|
||||
return allEpics, nil // Return what we have
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
if !entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
// Skip hidden directories and known non-rig directories
|
||||
name := entry.Name()
|
||||
if strings.HasPrefix(name, ".") || name == "plugins" || name == "docs" {
|
||||
continue
|
||||
}
|
||||
|
||||
rigPath := filepath.Join(townRoot, name)
|
||||
rigBeadsDir := filepath.Join(rigPath, ".beads")
|
||||
|
||||
// Check if this directory has a beads database
|
||||
if _, err := os.Stat(rigBeadsDir); os.IsNotExist(err) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Query this rig for epics
|
||||
epics, err := queryEpicsInDir(rigPath)
|
||||
if err != nil {
|
||||
// Log but continue - one rig failing shouldn't stop the whole query
|
||||
continue
|
||||
}
|
||||
|
||||
for _, e := range epics {
|
||||
if !seen[e.ID] {
|
||||
seen[e.ID] = true
|
||||
allEpics = append(allEpics, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return allEpics, nil
|
||||
}
|
||||
|
||||
// queryEpicsInDir runs bd list --type=epic in the specified directory.
|
||||
// If dir is empty, uses current working directory.
|
||||
func queryEpicsInDir(dir string) ([]epicRecord, error) {
|
||||
listArgs := []string{"list", "--type=epic", "--json"}
|
||||
if goalsStatus != "" && goalsStatus != "open" {
|
||||
if goalsStatus == "all" {
|
||||
listArgs = append(listArgs, "--all")
|
||||
} else {
|
||||
listArgs = append(listArgs, "--status="+goalsStatus)
|
||||
}
|
||||
}
|
||||
|
||||
listCmd := exec.Command("bd", listArgs...)
|
||||
if dir != "" {
|
||||
listCmd.Dir = dir
|
||||
}
|
||||
var stdout bytes.Buffer
|
||||
listCmd.Stdout = &stdout
|
||||
|
||||
if err := listCmd.Run(); err != nil {
|
||||
return nil, fmt.Errorf("listing epics: %w", err)
|
||||
}
|
||||
|
||||
var epics []epicRecord
|
||||
if err := json.Unmarshal(stdout.Bytes(), &epics); err != nil {
|
||||
return nil, fmt.Errorf("parsing epics: %w", err)
|
||||
}
|
||||
|
||||
return epics, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user