package main import ( "context" "fmt" "os" "sort" "github.com/spf13/cobra" "github.com/steveyegge/beads/internal/storage" "github.com/steveyegge/beads/internal/types" "github.com/steveyegge/beads/internal/ui" ) // GatedMolecule represents a molecule ready for gate-resume dispatch type GatedMolecule struct { MoleculeID string `json:"molecule_id"` MoleculeTitle string `json:"molecule_title"` ClosedGate *types.Issue `json:"closed_gate"` ReadyStep *types.Issue `json:"ready_step"` } // GatedReadyOutput is the JSON output for bd mol ready --gated type GatedReadyOutput struct { Molecules []*GatedMolecule `json:"molecules"` Count int `json:"count"` } var molReadyGatedCmd = &cobra.Command{ Use: "ready --gated", Short: "Find molecules ready for gate-resume dispatch", Long: `Find molecules where a gate has closed and the workflow is ready to resume. This command discovers molecules waiting at a gate step where: 1. The molecule has a gate bead that blocks a step 2. The gate bead is now closed (condition satisfied) 3. The blocked step is now ready to proceed 4. No agent currently has this molecule hooked This enables discovery-based resume without explicit waiter tracking. The Deacon patrol uses this to find and dispatch gate-ready molecules. Examples: bd mol ready --gated # Find all gate-ready molecules bd mol ready --gated --json # JSON output for automation`, Run: runMolReadyGated, } func runMolReadyGated(cmd *cobra.Command, args []string) { ctx := rootCtx // mol ready --gated requires direct store access if store == nil { if daemonClient != nil { fmt.Fprintf(os.Stderr, "Error: mol ready --gated requires direct database access\n") fmt.Fprintf(os.Stderr, "Hint: use --no-daemon flag: bd --no-daemon mol ready --gated\n") } else { fmt.Fprintf(os.Stderr, "Error: no database connection\n") } os.Exit(1) } // Find gate-ready molecules molecules, err := findGateReadyMolecules(ctx, store) if err != nil { fmt.Fprintf(os.Stderr, "Error: %v\n", err) os.Exit(1) } if jsonOutput { output := GatedReadyOutput{ Molecules: molecules, Count: len(molecules), } if output.Molecules == nil { output.Molecules = []*GatedMolecule{} } outputJSON(output) return } // Human-readable output if len(molecules) == 0 { fmt.Printf("\n%s No molecules ready for gate-resume dispatch\n\n", ui.RenderWarn("")) return } fmt.Printf("\n%s Molecules ready for gate-resume dispatch (%d):\n\n", ui.RenderAccent(""), len(molecules)) for i, mol := range molecules { fmt.Printf("%d. %s: %s\n", i+1, ui.RenderID(mol.MoleculeID), mol.MoleculeTitle) if mol.ClosedGate != nil { fmt.Printf(" Gate closed: %s (%s)\n", mol.ClosedGate.ID, mol.ClosedGate.AwaitType) } if mol.ReadyStep != nil { fmt.Printf(" Ready step: %s - %s\n", mol.ReadyStep.ID, mol.ReadyStep.Title) } fmt.Println() } fmt.Println("To dispatch a molecule:") fmt.Println(" gt sling --mol ") } // findGateReadyMolecules finds molecules where a gate has closed and work can resume. // // Logic: // 1. Find all closed gate beads // 2. For each closed gate, find what step it was blocking // 3. Check if that step is now ready (unblocked) // 4. Find the parent molecule // 5. Filter out molecules that are already hooked by someone func findGateReadyMolecules(ctx context.Context, s storage.Storage) ([]*GatedMolecule, error) { // Step 1: Find all closed gate beads gateType := types.TypeGate closedStatus := types.StatusClosed gateFilter := types.IssueFilter{ IssueType: &gateType, Status: &closedStatus, Limit: 100, } closedGates, err := s.SearchIssues(ctx, "", gateFilter) if err != nil { return nil, fmt.Errorf("searching closed gates: %w", err) } if len(closedGates) == 0 { return nil, nil } // Step 2: Get ready work to check which steps are ready readyIssues, err := s.GetReadyWork(ctx, types.WorkFilter{Limit: 500}) if err != nil { return nil, fmt.Errorf("getting ready work: %w", err) } readyIDs := make(map[string]bool) for _, issue := range readyIssues { readyIDs[issue.ID] = true } // Step 3: Get hooked molecules to filter out hookedStatus := types.StatusHooked hookedFilter := types.IssueFilter{ Status: &hookedStatus, Limit: 100, } hookedIssues, err := s.SearchIssues(ctx, "", hookedFilter) if err != nil { // Non-fatal: just continue without filtering hookedIssues = nil } hookedMolecules := make(map[string]bool) for _, issue := range hookedIssues { // If the hooked issue is a molecule root, mark it hookedMolecules[issue.ID] = true // Also find parent molecule for hooked steps if parentMol := findParentMolecule(ctx, s, issue.ID); parentMol != "" { hookedMolecules[parentMol] = true } } // Step 4: For each closed gate, find issues that depend on it (were blocked) moleculeMap := make(map[string]*GatedMolecule) for _, gate := range closedGates { // Find issues that depend on this gate (GetDependents returns issues where depends_on_id = gate.ID) dependents, err := s.GetDependents(ctx, gate.ID) if err != nil { continue } for _, dependent := range dependents { // Check if the previously blocked step is now ready if !readyIDs[dependent.ID] { continue } // Find the parent molecule moleculeID := findParentMolecule(ctx, s, dependent.ID) if moleculeID == "" { continue } // Skip if already hooked if hookedMolecules[moleculeID] { continue } // Get molecule details moleculeIssue, err := s.GetIssue(ctx, moleculeID) if err != nil || moleculeIssue == nil { continue } // Add to results (dedupe by molecule ID) if _, exists := moleculeMap[moleculeID]; !exists { moleculeMap[moleculeID] = &GatedMolecule{ MoleculeID: moleculeID, MoleculeTitle: moleculeIssue.Title, ClosedGate: gate, ReadyStep: dependent, } } } } // Convert to slice and sort var molecules []*GatedMolecule for _, mol := range moleculeMap { molecules = append(molecules, mol) } sort.Slice(molecules, func(i, j int) bool { return molecules[i].MoleculeID < molecules[j].MoleculeID }) return molecules, nil } func init() { // Note: --gated flag is registered in ready.go // Also add as a subcommand under mol for discoverability molCmd.AddCommand(molReadyGatedCmd) }