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 <noreply@anthropic.com>
This commit is contained in:
@@ -137,6 +137,12 @@ Use --limit or --range to view specific steps:
|
|||||||
} else {
|
} else {
|
||||||
// Infer from in_progress issues
|
// Infer from in_progress issues
|
||||||
molecules = findInProgressMolecules(ctx, store, agent)
|
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 len(molecules) == 0 {
|
||||||
if jsonOutput {
|
if jsonOutput {
|
||||||
outputJSON([]interface{}{})
|
outputJSON([]interface{}{})
|
||||||
@@ -308,6 +314,76 @@ func findInProgressMolecules(ctx context.Context, s storage.Storage, agent strin
|
|||||||
return molecules
|
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
|
// findParentMolecule walks up parent-child chain to find the root molecule
|
||||||
func findParentMolecule(ctx context.Context, s storage.Storage, issueID string) string {
|
func findParentMolecule(ctx context.Context, s storage.Storage, issueID string) string {
|
||||||
visited := make(map[string]bool)
|
visited := make(map[string]bool)
|
||||||
|
|||||||
@@ -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
|
// TestAdvanceToNextStep tests auto-advancing to next step
|
||||||
func TestAdvanceToNextStep(t *testing.T) {
|
func TestAdvanceToNextStep(t *testing.T) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|||||||
Reference in New Issue
Block a user