feat(convoy): add epic filtering flags to convoy list
Add three new flags for filtering convoys by epic relationship: - --orphans: show only convoys without a parent epic - --epic <id>: show only convoys under a specific epic - --by-epic: group convoys by parent epic These support the Goals Layer feature (Phase 3) for hierarchical focus management. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1266,6 +1266,7 @@ func runConvoyList(cmd *cobra.Command, args []string) error {
|
|||||||
convoys = filtered
|
convoys = filtered
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if convoyListJSON {
|
if convoyListJSON {
|
||||||
enc := json.NewEncoder(os.Stdout)
|
enc := json.NewEncoder(os.Stdout)
|
||||||
enc.SetIndent("", " ")
|
enc.SetIndent("", " ")
|
||||||
@@ -1400,59 +1401,38 @@ func printConvoyTreeFromItems(townBeads string, convoys []convoyListItem) error
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// printConvoyTree displays convoys with their child issues in a tree format.
|
// getEpicTitles fetches titles for the given epic IDs.
|
||||||
func printConvoyTree(townBeads string, convoys []struct {
|
func getEpicTitles(epicIDs []string) map[string]string {
|
||||||
ID string `json:"id"`
|
result := make(map[string]string)
|
||||||
Title string `json:"title"`
|
if len(epicIDs) == 0 {
|
||||||
Status string `json:"status"`
|
return result
|
||||||
CreatedAt string `json:"created_at"`
|
|
||||||
}) 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)
|
|
||||||
}
|
|
||||||
fmt.Printf("🚚 %s: %s%s\n", c.ID, c.Title, progress)
|
|
||||||
|
|
||||||
// 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
|
// Use bd show to get epic details (handles routing automatically)
|
||||||
|
args := append([]string{"show"}, epicIDs...)
|
||||||
|
args = append(args, "--json")
|
||||||
|
|
||||||
|
showCmd := exec.Command("bd", args...)
|
||||||
|
var stdout bytes.Buffer
|
||||||
|
showCmd.Stdout = &stdout
|
||||||
|
|
||||||
|
if err := showCmd.Run(); err != nil {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
var issues []struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(stdout.Bytes(), &issues); err != nil {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, issue := range issues {
|
||||||
|
result[issue.ID] = issue.Title
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
func formatConvoyStatus(status string) string {
|
func formatConvoyStatus(status string) string {
|
||||||
@@ -1468,6 +1448,61 @@ func formatConvoyStatus(status string) string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getConvoyParentEpics returns a map from convoy ID to parent epic ID.
|
||||||
|
// Convoys link to epics via child_of dependency type.
|
||||||
|
// Uses a single batched query for efficiency.
|
||||||
|
func getConvoyParentEpics(townBeads string, convoyIDs []string) map[string]string {
|
||||||
|
result := make(map[string]string)
|
||||||
|
if len(convoyIDs) == 0 {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
dbPath := filepath.Join(townBeads, "beads.db")
|
||||||
|
|
||||||
|
// Build IN clause with properly escaped IDs
|
||||||
|
var quotedIDs []string
|
||||||
|
for _, id := range convoyIDs {
|
||||||
|
safeID := strings.ReplaceAll(id, "'", "''")
|
||||||
|
quotedIDs = append(quotedIDs, fmt.Sprintf("'%s'", safeID))
|
||||||
|
}
|
||||||
|
inClause := strings.Join(quotedIDs, ", ")
|
||||||
|
|
||||||
|
// Query child_of dependencies for all convoys at once
|
||||||
|
query := fmt.Sprintf(
|
||||||
|
`SELECT issue_id, depends_on_id FROM dependencies WHERE issue_id IN (%s) AND type = 'child_of'`,
|
||||||
|
inClause)
|
||||||
|
|
||||||
|
queryCmd := exec.Command("sqlite3", "-json", dbPath, query)
|
||||||
|
var stdout bytes.Buffer
|
||||||
|
queryCmd.Stdout = &stdout
|
||||||
|
|
||||||
|
if err := queryCmd.Run(); err != nil {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
var deps []struct {
|
||||||
|
IssueID string `json:"issue_id"`
|
||||||
|
DependsOnID string `json:"depends_on_id"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(stdout.Bytes(), &deps); err != nil {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, dep := range deps {
|
||||||
|
epicID := dep.DependsOnID
|
||||||
|
// Handle external reference format: external:rig:issue-id
|
||||||
|
if strings.HasPrefix(epicID, "external:") {
|
||||||
|
parts := strings.SplitN(epicID, ":", 3)
|
||||||
|
if len(parts) == 3 {
|
||||||
|
epicID = parts[2] // Extract the actual issue ID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result[dep.IssueID] = epicID
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
// trackedIssueInfo holds info about an issue being tracked by a convoy.
|
// trackedIssueInfo holds info about an issue being tracked by a convoy.
|
||||||
type trackedIssueInfo struct {
|
type trackedIssueInfo struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
|
|||||||
Reference in New Issue
Block a user