package main import ( "context" "encoding/json" "fmt" "os" "sort" "github.com/spf13/cobra" "github.com/steveyegge/beads/internal/rpc" "github.com/steveyegge/beads/internal/storage" "github.com/steveyegge/beads/internal/types" "github.com/steveyegge/beads/internal/ui" "github.com/steveyegge/beads/internal/utils" ) // MoleculeProgress holds the progress information for a molecule type MoleculeProgress struct { MoleculeID string `json:"molecule_id"` MoleculeTitle string `json:"molecule_title"` Assignee string `json:"assignee,omitempty"` CurrentStep *types.Issue `json:"current_step,omitempty"` NextStep *types.Issue `json:"next_step,omitempty"` Steps []*StepStatus `json:"steps"` Completed int `json:"completed"` Total int `json:"total"` } // StepStatus represents the status of a step in a molecule type StepStatus struct { Issue *types.Issue `json:"issue"` Status string `json:"status"` // "done", "current", "ready", "blocked", "pending" IsCurrent bool `json:"is_current"` // true if this is the in_progress step } var molCurrentCmd = &cobra.Command{ Use: "current [molecule-id]", Short: "Show current position in molecule workflow", Long: `Show where you are in a molecule workflow. If molecule-id is given, show status for that molecule. If not given, infer from in_progress issues assigned to current agent. The output shows all steps with status indicators: [done] - Step is complete (closed) [current] - Step is in_progress (you are here) [ready] - Step is ready to start (unblocked) [blocked] - Step is blocked by dependencies [pending] - Step is waiting`, Args: cobra.MaximumNArgs(1), Run: func(cmd *cobra.Command, args []string) { ctx := rootCtx forAgent, _ := cmd.Flags().GetString("for") // Determine who we're looking for agent := forAgent if agent == "" { agent = actor // Default to current user/agent } // mol current requires direct store access for subgraph loading if store == nil { if daemonClient != nil { fmt.Fprintf(os.Stderr, "Error: mol current requires direct database access\n") fmt.Fprintf(os.Stderr, "Hint: use --no-daemon flag: bd --no-daemon mol current\n") } else { fmt.Fprintf(os.Stderr, "Error: no database connection\n") } os.Exit(1) } var molecules []*MoleculeProgress if len(args) == 1 { // Explicit molecule ID given moleculeID, err := utils.ResolvePartialID(ctx, store, args[0]) if err != nil { fmt.Fprintf(os.Stderr, "Error: molecule '%s' not found\n", args[0]) os.Exit(1) } progress, err := getMoleculeProgress(ctx, store, moleculeID) if err != nil { fmt.Fprintf(os.Stderr, "Error loading molecule: %v\n", err) os.Exit(1) } molecules = append(molecules, progress) } else { // Infer from in_progress issues molecules = findInProgressMolecules(ctx, store, agent) if len(molecules) == 0 { if jsonOutput { outputJSON([]interface{}{}) return } fmt.Printf("No molecules in progress") if agent != "" { fmt.Printf(" for %s", agent) } fmt.Println(".") fmt.Println("\nTo start work on a molecule:") fmt.Println(" bd pour # Instantiate a molecule from template") fmt.Println(" bd update --status in_progress # Claim a step") return } } if jsonOutput { outputJSON(molecules) return } // Human-readable output for i, mol := range molecules { if i > 0 { fmt.Println() } printMoleculeProgress(mol) } }, } // getMoleculeProgress loads a molecule and computes progress func getMoleculeProgress(ctx context.Context, s storage.Storage, moleculeID string) (*MoleculeProgress, error) { subgraph, err := loadTemplateSubgraph(ctx, s, moleculeID) if err != nil { return nil, err } progress := &MoleculeProgress{ MoleculeID: subgraph.Root.ID, MoleculeTitle: subgraph.Root.Title, Assignee: subgraph.Root.Assignee, Total: len(subgraph.Issues) - 1, // Exclude root } // Get ready issues for this molecule readyIDs := make(map[string]bool) readyIssues, err := s.GetReadyWork(ctx, types.WorkFilter{}) if err == nil { for _, issue := range readyIssues { readyIDs[issue.ID] = true } } // Build step status list (exclude root) var steps []*StepStatus for _, issue := range subgraph.Issues { if issue.ID == subgraph.Root.ID { continue // Skip root } step := &StepStatus{ Issue: issue, } switch issue.Status { case types.StatusClosed: step.Status = "done" progress.Completed++ case types.StatusInProgress: step.Status = "current" step.IsCurrent = true progress.CurrentStep = issue case types.StatusBlocked: step.Status = "blocked" default: // Check if ready (unblocked) if readyIDs[issue.ID] { step.Status = "ready" if progress.NextStep == nil { progress.NextStep = issue } } else { step.Status = "pending" } } steps = append(steps, step) } // Sort steps by dependency order sortStepsByDependencyOrder(steps, subgraph) progress.Steps = steps // If no current step but there's a ready step, set it as next if progress.CurrentStep == nil && progress.NextStep == nil { for _, step := range steps { if step.Status == "ready" { progress.NextStep = step.Issue break } } } return progress, nil } // findInProgressMolecules finds molecules with in_progress steps for an agent func findInProgressMolecules(ctx context.Context, s storage.Storage, agent string) []*MoleculeProgress { // Query for in_progress issues var inProgressIssues []*types.Issue if daemonClient != nil { listArgs := &rpc.ListArgs{ Status: "in_progress", Assignee: agent, } resp, err := daemonClient.List(listArgs) if err == nil { _ = json.Unmarshal(resp.Data, &inProgressIssues) } } else { // Direct query - search for in_progress issues status := types.StatusInProgress filter := types.IssueFilter{Status: &status} if agent != "" { filter.Assignee = &agent } allIssues, err := s.SearchIssues(ctx, "", filter) if err == nil { inProgressIssues = allIssues } } if len(inProgressIssues) == 0 { return nil } // For each in_progress issue, find its parent molecule moleculeMap := make(map[string]*MoleculeProgress) for _, issue := range inProgressIssues { moleculeID := findParentMolecule(ctx, s, issue.ID) if moleculeID == "" { // Not part of a molecule, skip continue } if _, exists := moleculeMap[moleculeID]; !exists { progress, err := getMoleculeProgress(ctx, s, moleculeID) if err == nil { moleculeMap[moleculeID] = progress } } } // Convert to slice var molecules []*MoleculeProgress for _, mol := range moleculeMap { molecules = append(molecules, mol) } // Sort by molecule ID for consistent output sort.Slice(molecules, func(i, j int) bool { return molecules[i].MoleculeID < molecules[j].MoleculeID }) return molecules } // findParentMolecule walks up parent-child chain to find the root molecule func findParentMolecule(ctx context.Context, s storage.Storage, issueID string) string { visited := make(map[string]bool) currentID := issueID for !visited[currentID] { visited[currentID] = true // Get dependencies for current issue deps, err := s.GetDependencyRecords(ctx, currentID) if err != nil { return "" } // Find parent-child dependency where current is the child var parentID string for _, dep := range deps { if dep.Type == types.DepParentChild && dep.IssueID == currentID { parentID = dep.DependsOnID break } } if parentID == "" { // No parent - check if current issue is a molecule root issue, err := s.GetIssue(ctx, currentID) if err != nil || issue == nil { return "" } // Check if it has the template label (molecules are spawned from templates) for _, label := range issue.Labels { if label == BeadsTemplateLabel { return currentID } } // Also check if it's an epic with children (ad-hoc molecule) if issue.IssueType == types.TypeEpic { return currentID } return "" } currentID = parentID } return "" } // sortStepsByDependencyOrder sorts steps by their dependency order func sortStepsByDependencyOrder(steps []*StepStatus, subgraph *TemplateSubgraph) { // Build dependency graph depCount := make(map[string]int) // issue ID -> number of deps for _, step := range steps { depCount[step.Issue.ID] = 0 } // Count blocking dependencies within the step set stepIDs := make(map[string]bool) for _, step := range steps { stepIDs[step.Issue.ID] = true } for _, dep := range subgraph.Dependencies { if dep.Type == types.DepBlocks && stepIDs[dep.IssueID] && stepIDs[dep.DependsOnID] { depCount[dep.IssueID]++ } } // Stable sort by dependency count (fewer deps first) sort.SliceStable(steps, func(i, j int) bool { return depCount[steps[i].Issue.ID] < depCount[steps[j].Issue.ID] }) } // printMoleculeProgress prints the progress in human-readable format func printMoleculeProgress(mol *MoleculeProgress) { fmt.Printf("You're working on molecule %s\n", ui.RenderAccent(mol.MoleculeID)) fmt.Printf(" %s\n", mol.MoleculeTitle) if mol.Assignee != "" { fmt.Printf(" Assigned to: %s\n", mol.Assignee) } fmt.Println() for _, step := range mol.Steps { statusIcon := getStatusIcon(step.Status) marker := "" if step.IsCurrent { marker = " <- YOU ARE HERE" } fmt.Printf(" %s %s: %s%s\n", statusIcon, step.Issue.ID, step.Issue.Title, marker) } fmt.Println() fmt.Printf("Progress: %d/%d steps complete\n", mol.Completed, mol.Total) if mol.NextStep != nil && mol.CurrentStep == nil { fmt.Printf("\nNext ready: %s - %s\n", mol.NextStep.ID, mol.NextStep.Title) fmt.Printf(" Start with: bd update %s --status in_progress\n", mol.NextStep.ID) } } // getStatusIcon returns the icon for a step status func getStatusIcon(status string) string { switch status { case "done": return ui.RenderPass("[done]") case "current": return ui.RenderWarn("[current]") case "ready": return ui.RenderAccent("[ready]") case "blocked": return ui.RenderFail("[blocked]") default: return "[pending]" } } // ContinueResult holds the result of advancing to the next molecule step type ContinueResult struct { ClosedStep *types.Issue `json:"closed_step"` NextStep *types.Issue `json:"next_step,omitempty"` AutoAdvanced bool `json:"auto_advanced"` MolComplete bool `json:"molecule_complete"` MoleculeID string `json:"molecule_id,omitempty"` } // AdvanceToNextStep finds the next ready step in a molecule after closing a step // If autoClaim is true, it marks the next step as in_progress // Returns nil if the issue is not part of a molecule func AdvanceToNextStep(ctx context.Context, s storage.Storage, closedStepID string, autoClaim bool, actorName string) (*ContinueResult, error) { if s == nil { return nil, fmt.Errorf("no database connection") } // Get the closed step closedStep, err := s.GetIssue(ctx, closedStepID) if err != nil || closedStep == nil { return nil, fmt.Errorf("could not get closed step: %w", err) } result := &ContinueResult{ ClosedStep: closedStep, } // Find parent molecule moleculeID := findParentMolecule(ctx, s, closedStepID) if moleculeID == "" { // Not part of a molecule - nothing to advance return nil, nil } result.MoleculeID = moleculeID // Load molecule progress progress, err := getMoleculeProgress(ctx, s, moleculeID) if err != nil { return nil, fmt.Errorf("could not load molecule: %w", err) } // Check if molecule is complete if progress.Completed >= progress.Total { result.MolComplete = true return result, nil } // Find next ready step var nextStep *types.Issue for _, step := range progress.Steps { if step.Status == "ready" { nextStep = step.Issue break } } if nextStep == nil { // No ready steps - might be blocked return result, nil } result.NextStep = nextStep // Auto-claim if requested if autoClaim { updates := map[string]interface{}{ "status": types.StatusInProgress, } if err := s.UpdateIssue(ctx, nextStep.ID, updates, actorName); err != nil { return result, fmt.Errorf("could not claim next step: %w", err) } result.AutoAdvanced = true } return result, nil } // PrintContinueResult prints the result of advancing to the next step func PrintContinueResult(result *ContinueResult) { if result == nil { return } if result.MolComplete { fmt.Printf("\n%s Molecule %s complete! All steps closed.\n", ui.RenderPass("✓"), result.MoleculeID) fmt.Println("Consider: bd mol squash " + result.MoleculeID + " --summary '...'") return } if result.NextStep == nil { fmt.Println("\nNo ready steps in molecule (may be blocked).") return } fmt.Printf("\nNext ready in molecule:\n") fmt.Printf(" %s: %s\n", result.NextStep.ID, result.NextStep.Title) if result.AutoAdvanced { fmt.Printf("\n%s Marked in_progress (use --no-auto to skip)\n", ui.RenderWarn("→")) } else { fmt.Printf("\nStart with: bd update %s --status in_progress\n", result.NextStep.ID) } } func init() { molCurrentCmd.Flags().String("for", "", "Show molecules for a specific agent/assignee") molCmd.AddCommand(molCurrentCmd) }