feat: add parallel step detection for molecules (bd-xo1o.4)
Add --parallel flag to `bd mol show` that analyzes molecule structure and identifies which steps can run in parallel. Also add --mol flag to `bd ready` to list ready steps within a specific molecule. Features: - `bd mol show <id> --parallel`: Shows parallel group annotations - `bd ready --mol <id>`: Lists ready steps with parallel opportunities - Detects steps at same blocking depth that can parallelize - Groups steps without mutual blocking deps into parallel groups - JSON output includes full parallel analysis Algorithm: - Build dependency graph from blocking deps (blocks, conditional-blocks) - Calculate blocking depth (distance from unblocked state) - Group steps at same depth with no mutual blocking into parallel groups - Mark steps as ready if open/in_progress with no open blockers 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
+149
@@ -12,11 +12,26 @@ import (
|
||||
"github.com/steveyegge/beads/internal/types"
|
||||
"github.com/steveyegge/beads/internal/ui"
|
||||
"github.com/steveyegge/beads/internal/util"
|
||||
"github.com/steveyegge/beads/internal/utils"
|
||||
)
|
||||
|
||||
var readyCmd = &cobra.Command{
|
||||
Use: "ready",
|
||||
Short: "Show ready work (no blockers, open or in_progress)",
|
||||
Long: `Show ready work (issues with no blockers that are open or in_progress).
|
||||
|
||||
Use --mol to filter to a specific molecule's steps:
|
||||
bd ready --mol bd-patrol # Show ready steps within molecule
|
||||
|
||||
This is useful for agents executing molecules to see which steps can run next.`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
// Handle molecule-specific ready query
|
||||
molID, _ := cmd.Flags().GetString("mol")
|
||||
if molID != "" {
|
||||
runMoleculeReady(cmd, molID)
|
||||
return
|
||||
}
|
||||
|
||||
limit, _ := cmd.Flags().GetInt("limit")
|
||||
assignee, _ := cmd.Flags().GetString("assignee")
|
||||
unassigned, _ := cmd.Flags().GetBool("unassigned")
|
||||
@@ -252,6 +267,139 @@ var blockedCmd = &cobra.Command{
|
||||
},
|
||||
}
|
||||
|
||||
// runMoleculeReady shows ready steps within a specific molecule
|
||||
func runMoleculeReady(cmd *cobra.Command, molIDArg string) {
|
||||
ctx := rootCtx
|
||||
|
||||
// Molecule-ready requires direct store access for subgraph loading
|
||||
if store == nil {
|
||||
if daemonClient != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: bd ready --mol requires direct database access\n")
|
||||
fmt.Fprintf(os.Stderr, "Hint: use --no-daemon flag: bd --no-daemon ready --mol %s\n", molIDArg)
|
||||
} else {
|
||||
fmt.Fprintf(os.Stderr, "Error: no database connection\n")
|
||||
}
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Resolve molecule ID
|
||||
moleculeID, err := utils.ResolvePartialID(ctx, store, molIDArg)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: molecule '%s' not found\n", molIDArg)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Load molecule subgraph
|
||||
subgraph, err := loadTemplateSubgraph(ctx, store, moleculeID)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error loading molecule: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Get parallel analysis to find ready steps
|
||||
analysis := analyzeMoleculeParallel(subgraph)
|
||||
|
||||
// Collect ready steps
|
||||
var readySteps []*MoleculeReadyStep
|
||||
for _, issue := range subgraph.Issues {
|
||||
info := analysis.Steps[issue.ID]
|
||||
if info != nil && info.IsReady {
|
||||
readySteps = append(readySteps, &MoleculeReadyStep{
|
||||
Issue: issue,
|
||||
ParallelInfo: info,
|
||||
ParallelGroup: info.ParallelGroup,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if jsonOutput {
|
||||
output := MoleculeReadyOutput{
|
||||
MoleculeID: moleculeID,
|
||||
MoleculeTitle: subgraph.Root.Title,
|
||||
TotalSteps: analysis.TotalSteps,
|
||||
ReadySteps: len(readySteps),
|
||||
Steps: readySteps,
|
||||
ParallelGroups: analysis.ParallelGroups,
|
||||
}
|
||||
outputJSON(output)
|
||||
return
|
||||
}
|
||||
|
||||
// Human-readable output
|
||||
fmt.Printf("\n%s Ready steps in molecule: %s\n", ui.RenderAccent("🧪"), subgraph.Root.Title)
|
||||
fmt.Printf(" ID: %s\n", moleculeID)
|
||||
fmt.Printf(" Total: %d steps, %d ready\n", analysis.TotalSteps, len(readySteps))
|
||||
|
||||
if len(readySteps) == 0 {
|
||||
fmt.Printf("\n%s No ready steps (all blocked or completed)\n\n", ui.RenderWarn("✨"))
|
||||
return
|
||||
}
|
||||
|
||||
// Show parallel groups if any
|
||||
if len(analysis.ParallelGroups) > 0 {
|
||||
fmt.Printf("\n%s Parallel Groups:\n", ui.RenderPass("⚡"))
|
||||
for groupName, members := range analysis.ParallelGroups {
|
||||
// Check if any members are ready
|
||||
readyInGroup := 0
|
||||
for _, id := range members {
|
||||
if info := analysis.Steps[id]; info != nil && info.IsReady {
|
||||
readyInGroup++
|
||||
}
|
||||
}
|
||||
if readyInGroup > 0 {
|
||||
fmt.Printf(" %s: %d ready\n", groupName, readyInGroup)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf("\n%s Ready steps:\n\n", ui.RenderPass("📋"))
|
||||
for i, step := range readySteps {
|
||||
// Show parallel group if in one
|
||||
groupAnnotation := ""
|
||||
if step.ParallelGroup != "" {
|
||||
groupAnnotation = fmt.Sprintf(" [%s]", ui.RenderAccent(step.ParallelGroup))
|
||||
}
|
||||
|
||||
fmt.Printf("%d. [%s] [%s] %s: %s%s\n", i+1,
|
||||
ui.RenderPriority(step.Issue.Priority),
|
||||
ui.RenderType(string(step.Issue.IssueType)),
|
||||
ui.RenderID(step.Issue.ID),
|
||||
step.Issue.Title,
|
||||
groupAnnotation)
|
||||
|
||||
// Show what this step can parallelize with
|
||||
if len(step.ParallelInfo.CanParallel) > 0 {
|
||||
readyParallel := []string{}
|
||||
for _, pID := range step.ParallelInfo.CanParallel {
|
||||
if pInfo := analysis.Steps[pID]; pInfo != nil && pInfo.IsReady {
|
||||
readyParallel = append(readyParallel, pID)
|
||||
}
|
||||
}
|
||||
if len(readyParallel) > 0 {
|
||||
fmt.Printf(" Can run with: %v\n", readyParallel)
|
||||
}
|
||||
}
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
// MoleculeReadyStep holds a ready step with its parallel info
|
||||
type MoleculeReadyStep struct {
|
||||
Issue *types.Issue `json:"issue"`
|
||||
ParallelInfo *ParallelInfo `json:"parallel_info"`
|
||||
ParallelGroup string `json:"parallel_group,omitempty"`
|
||||
}
|
||||
|
||||
// MoleculeReadyOutput is the JSON output for bd ready --mol
|
||||
type MoleculeReadyOutput struct {
|
||||
MoleculeID string `json:"molecule_id"`
|
||||
MoleculeTitle string `json:"molecule_title"`
|
||||
TotalSteps int `json:"total_steps"`
|
||||
ReadySteps int `json:"ready_steps"`
|
||||
Steps []*MoleculeReadyStep `json:"steps"`
|
||||
ParallelGroups map[string][]string `json:"parallel_groups"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
readyCmd.Flags().IntP("limit", "n", 10, "Maximum issues to show")
|
||||
readyCmd.Flags().IntP("priority", "p", 0, "Filter by priority")
|
||||
@@ -261,6 +409,7 @@ func init() {
|
||||
readyCmd.Flags().StringSliceP("label", "l", []string{}, "Filter by labels (AND: must have ALL). Can combine with --label-any")
|
||||
readyCmd.Flags().StringSlice("label-any", []string{}, "Filter by labels (OR: must have AT LEAST ONE). Can combine with --label")
|
||||
readyCmd.Flags().StringP("type", "t", "", "Filter by issue type (task, bug, feature, epic, merge-request)")
|
||||
readyCmd.Flags().String("mol", "", "Filter to steps within a specific molecule")
|
||||
rootCmd.AddCommand(readyCmd)
|
||||
rootCmd.AddCommand(blockedCmd)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user