From 1472d115f1a11442ced0e1467967401ffc817397 Mon Sep 17 00:00:00 2001 From: aleiby Date: Sat, 24 Jan 2026 17:11:44 -0800 Subject: [PATCH] fix(mol): find molecules attached to hooked issues (bd-o0mp) (#1302) When no steps are in_progress, bd mol current now checks for molecules bonded to hooked issues via blocks dependencies. This fixes the case where a molecule is attached to an agent's hook but no steps have been claimed yet. The fix adds findHookedMolecules() as a fallback after findInProgressMolecules() returns empty. It queries for hooked issues assigned to the agent and checks for blocks dependencies pointing to molecules (epics or template-labeled issues). Co-authored-by: Claude Opus 4.5 --- cmd/bd/mol_current.go | 76 +++++++++++++++++++++++++++++++++++++++ cmd/bd/mol_test.go | 83 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 159 insertions(+) diff --git a/cmd/bd/mol_current.go b/cmd/bd/mol_current.go index 12778836..ba11ab4c 100644 --- a/cmd/bd/mol_current.go +++ b/cmd/bd/mol_current.go @@ -137,6 +137,12 @@ Use --limit or --range to view specific steps: } else { // Infer from in_progress issues molecules = findInProgressMolecules(ctx, store, agent) + + // Fallback: check for hooked issues with bonded molecules + if len(molecules) == 0 { + molecules = findHookedMolecules(ctx, store, agent) + } + if len(molecules) == 0 { if jsonOutput { outputJSON([]interface{}{}) @@ -308,6 +314,76 @@ func findInProgressMolecules(ctx context.Context, s storage.Storage, agent strin return molecules } +// findHookedMolecules finds molecules bonded to hooked issues for an agent. +// This is a fallback when no in_progress steps exist but a molecule is attached +// to the agent's hooked work via a "blocks" dependency. +func findHookedMolecules(ctx context.Context, s storage.Storage, agent string) []*MoleculeProgress { + // Query for hooked issues assigned to the agent + status := types.StatusHooked + filter := types.IssueFilter{Status: &status} + if agent != "" { + filter.Assignee = &agent + } + hookedIssues, err := s.SearchIssues(ctx, "", filter) + if err != nil || len(hookedIssues) == 0 { + return nil + } + + // For each hooked issue, check for blocks dependencies on molecules + moleculeMap := make(map[string]*MoleculeProgress) + for _, issue := range hookedIssues { + deps, err := s.GetDependencyRecords(ctx, issue.ID) + if err != nil { + continue + } + + // Look for a blocks dependency pointing to a molecule (epic or template) + for _, dep := range deps { + if dep.Type != types.DepBlocks { + continue + } + // The issue depends on (is blocked by) dep.DependsOnID + candidate, err := s.GetIssue(ctx, dep.DependsOnID) + if err != nil || candidate == nil { + continue + } + + // Check if candidate is a molecule (epic or has template label) + isMolecule := candidate.IssueType == types.TypeEpic + if !isMolecule { + for _, label := range candidate.Labels { + if label == BeadsTemplateLabel { + isMolecule = true + break + } + } + } + + if isMolecule { + if _, exists := moleculeMap[candidate.ID]; !exists { + progress, err := getMoleculeProgress(ctx, s, candidate.ID) + if err == nil { + moleculeMap[candidate.ID] = 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) diff --git a/cmd/bd/mol_test.go b/cmd/bd/mol_test.go index f98f5937..2228cf48 100644 --- a/cmd/bd/mol_test.go +++ b/cmd/bd/mol_test.go @@ -1550,6 +1550,89 @@ func TestFindParentMolecule(t *testing.T) { } } +// TestFindHookedMolecules tests finding molecules bonded to hooked issues +func TestFindHookedMolecules(t *testing.T) { + ctx := context.Background() + dbPath := t.TempDir() + "/test.db" + s, err := sqlite.New(ctx, dbPath) + if err != nil { + t.Fatalf("Failed to create store: %v", err) + } + defer s.Close() + if err := s.SetConfig(ctx, "issue_prefix", "test"); err != nil { + t.Fatalf("Failed to set config: %v", err) + } + + // Create molecule root (epic) + molecule := &types.Issue{ + Title: "Test Molecule", + Status: types.StatusOpen, + Priority: 1, + IssueType: types.TypeEpic, + } + if err := s.CreateIssue(ctx, molecule, "test"); err != nil { + t.Fatalf("Failed to create molecule: %v", err) + } + + // Create step as child of molecule + step := &types.Issue{ + Title: "Step 1", + Status: types.StatusOpen, + Priority: 2, + IssueType: types.TypeTask, + } + if err := s.CreateIssue(ctx, step, "test"); err != nil { + t.Fatalf("Failed to create step: %v", err) + } + if err := s.AddDependency(ctx, &types.Dependency{ + IssueID: step.ID, + DependsOnID: molecule.ID, + Type: types.DepParentChild, + }, "test"); err != nil { + t.Fatalf("Failed to add parent-child: %v", err) + } + + // Create hooked issue with blocks dependency on molecule + hookedIssue := &types.Issue{ + Title: "Hooked Work", + Status: types.StatusHooked, + Priority: 2, + IssueType: types.TypeTask, + Assignee: "test-agent", + } + if err := s.CreateIssue(ctx, hookedIssue, "test"); err != nil { + t.Fatalf("Failed to create hooked issue: %v", err) + } + if err := s.AddDependency(ctx, &types.Dependency{ + IssueID: hookedIssue.ID, + DependsOnID: molecule.ID, + Type: types.DepBlocks, + }, "test"); err != nil { + t.Fatalf("Failed to add blocks dependency: %v", err) + } + + // Test: findHookedMolecules should find the molecule for this agent + molecules := findHookedMolecules(ctx, s, "test-agent") + if len(molecules) != 1 { + t.Fatalf("findHookedMolecules() got %d molecules, want 1", len(molecules)) + } + if molecules[0].MoleculeID != molecule.ID { + t.Errorf("findHookedMolecules() got molecule %q, want %q", molecules[0].MoleculeID, molecule.ID) + } + + // Test: different agent should not find the molecule + molecules = findHookedMolecules(ctx, s, "other-agent") + if len(molecules) != 0 { + t.Errorf("findHookedMolecules(other-agent) got %d molecules, want 0", len(molecules)) + } + + // Test: no agent filter should find the molecule + molecules = findHookedMolecules(ctx, s, "") + if len(molecules) != 1 { + t.Errorf("findHookedMolecules('') got %d molecules, want 1", len(molecules)) + } +} + // TestAdvanceToNextStep tests auto-advancing to next step func TestAdvanceToNextStep(t *testing.T) { ctx := context.Background()