feat: Add tracks relation type for convoy tracking (bd-3roq)

Adds non-blocking tracks dependency type for convoy to issue relationships:
- Non-blocking: does not affect ready work calculation
- Cross-prefix capable: convoys in hq-* can track issues in gt-*, bd-*
- Reverse lookup: bd dep list <id> --direction=up -t tracks

Also adds bd dep list command with direction and type filtering for
querying dependencies/dependents.

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Steve Yegge
2025-12-29 21:04:28 -08:00
parent 2b90f51d0c
commit b8a5ee162b
5 changed files with 189 additions and 4 deletions

View File

@@ -221,6 +221,138 @@ Examples:
},
}
var depListCmd = &cobra.Command{
Use: "list [issue-id]",
Short: "List dependencies or dependents of an issue",
Long: `List dependencies or dependents of an issue with optional type filtering.
By default shows dependencies (what this issue depends on). Use --direction to control:
- down: Show dependencies (what this issue depends on) - default
- up: Show dependents (what depends on this issue)
Use --type to filter by dependency type (e.g., tracks, blocks, parent-child).
Examples:
bd dep list gt-abc # Show what gt-abc depends on
bd dep list gt-abc --direction=up # Show what depends on gt-abc
bd dep list gt-abc --direction=up -t tracks # Show what tracks gt-abc (convoy tracking)`,
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
ctx := rootCtx
// Resolve partial ID first
var fullID string
if daemonClient != nil {
resolveArgs := &rpc.ResolveIDArgs{ID: args[0]}
resp, err := daemonClient.ResolveID(resolveArgs)
if err != nil {
FatalErrorRespectJSON("resolving issue ID %s: %v", args[0], err)
}
if err := json.Unmarshal(resp.Data, &fullID); err != nil {
FatalErrorRespectJSON("unmarshaling resolved ID: %v", err)
}
} else {
var err error
fullID, err = utils.ResolvePartialID(ctx, store, args[0])
if err != nil {
FatalErrorRespectJSON("resolving %s: %v", args[0], err)
}
}
// If daemon is running but doesn't support this command, use direct storage
if daemonClient != nil && store == nil {
var err error
store, err = sqlite.New(rootCtx, dbPath)
if err != nil {
FatalErrorRespectJSON("failed to open database: %v", err)
}
defer func() { _ = store.Close() }()
}
direction, _ := cmd.Flags().GetString("direction")
typeFilter, _ := cmd.Flags().GetString("type")
if direction == "" {
direction = "down"
}
var issues []*types.IssueWithDependencyMetadata
var err error
if direction == "up" {
issues, err = store.GetDependentsWithMetadata(ctx, fullID)
} else {
issues, err = store.GetDependenciesWithMetadata(ctx, fullID)
}
if err != nil {
FatalErrorRespectJSON("%v", err)
}
// Apply type filter if specified
if typeFilter != "" {
var filtered []*types.IssueWithDependencyMetadata
for _, iss := range issues {
if string(iss.DependencyType) == typeFilter {
filtered = append(filtered, iss)
}
}
issues = filtered
}
if jsonOutput {
if issues == nil {
issues = []*types.IssueWithDependencyMetadata{}
}
outputJSON(issues)
return
}
if len(issues) == 0 {
if typeFilter != "" {
if direction == "up" {
fmt.Printf("\nNo issues depend on %s with type '%s'\n", fullID, typeFilter)
} else {
fmt.Printf("\n%s has no dependencies of type '%s'\n", fullID, typeFilter)
}
} else {
if direction == "up" {
fmt.Printf("\nNo issues depend on %s\n", fullID)
} else {
fmt.Printf("\n%s has no dependencies\n", fullID)
}
}
return
}
if direction == "up" {
fmt.Printf("\n%s Issues that depend on %s:\n\n", ui.RenderAccent("📋"), fullID)
} else {
fmt.Printf("\n%s %s depends on:\n\n", ui.RenderAccent("📋"), fullID)
}
for _, iss := range issues {
// Color the ID based on status
var idStr string
switch iss.Status {
case types.StatusOpen:
idStr = ui.StatusOpenStyle.Render(iss.ID)
case types.StatusInProgress:
idStr = ui.StatusInProgressStyle.Render(iss.ID)
case types.StatusBlocked:
idStr = ui.StatusBlockedStyle.Render(iss.ID)
case types.StatusClosed:
idStr = ui.StatusClosedStyle.Render(iss.ID)
default:
idStr = iss.ID
}
fmt.Printf(" %s: %s [P%d] (%s) via %s\n",
idStr, iss.Title, iss.Priority, iss.Status, iss.DependencyType)
}
fmt.Println()
},
}
var depRemoveCmd = &cobra.Command{
Use: "remove [issue-id] [depends-on-id]",
Aliases: []string{"rm"},
@@ -842,7 +974,7 @@ func ParseExternalRef(ref string) (project, capability string) {
}
func init() {
depAddCmd.Flags().StringP("type", "t", "blocks", "Dependency type (blocks|related|parent-child|discovered-from)")
depAddCmd.Flags().StringP("type", "t", "blocks", "Dependency type (blocks|tracks|related|parent-child|discovered-from)")
// Note: --json flag is defined as a persistent flag in main.go, not here
// Note: --json flag is defined as a persistent flag in main.go, not here
@@ -853,12 +985,17 @@ func init() {
depTreeCmd.Flags().String("direction", "", "Tree direction: 'down' (dependencies), 'up' (dependents), or 'both'")
depTreeCmd.Flags().String("status", "", "Filter to only show issues with this status (open, in_progress, blocked, deferred, closed)")
depTreeCmd.Flags().String("format", "", "Output format: 'mermaid' for Mermaid.js flowchart")
depTreeCmd.Flags().StringP("type", "t", "", "Filter to only show dependencies of this type (e.g., tracks, blocks, parent-child)")
// Note: --json flag is defined as a persistent flag in main.go, not here
// Note: --json flag is defined as a persistent flag in main.go, not here
depListCmd.Flags().String("direction", "down", "Direction: 'down' (dependencies), 'up' (dependents)")
depListCmd.Flags().StringP("type", "t", "", "Filter by dependency type (e.g., tracks, blocks, parent-child)")
depCmd.AddCommand(depAddCmd)
depCmd.AddCommand(depRemoveCmd)
depCmd.AddCommand(depListCmd)
depCmd.AddCommand(depTreeCmd)
depCmd.AddCommand(depCyclesCmd)
rootCmd.AddCommand(depCmd)