feat(close): Add --suggest-next flag to show newly unblocked issues (GH#679)
When closing an issue, the new --suggest-next flag returns a list of
issues that became unblocked (ready to work on) as a result of the close.
This helps agents and users quickly identify what work is now available
after completing a blocker.
Example:
$ bd close bd-5 --suggest-next
✓ Closed bd-5: Completed
Newly unblocked:
• bd-7 "Implement feature X" (P1)
• bd-8 "Write tests for X" (P2)
Implementation:
- Added GetNewlyUnblockedByClose to storage interface
- Implemented efficient single-query for SQLite using blocked_issues_cache
- Added SuggestNext field to CloseArgs in RPC protocol
- Added CloseResult type for structured response
- CLI handles both daemon and direct modes
Thanks to @kraitsura for the detailed feature request and design.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1512,3 +1512,56 @@ func TestCheckExternalDepInvalidFormats(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestGetNewlyUnblockedByClose tests the --suggest-next functionality (GH#679)
|
||||
func TestGetNewlyUnblockedByClose(t *testing.T) {
|
||||
env := newTestEnv(t)
|
||||
|
||||
// Create a blocker issue
|
||||
blocker := env.CreateIssueWith("Blocker", types.StatusOpen, 1, types.TypeTask)
|
||||
|
||||
// Create two issues blocked by the blocker
|
||||
blocked1 := env.CreateIssueWith("Blocked 1", types.StatusOpen, 2, types.TypeTask)
|
||||
blocked2 := env.CreateIssueWith("Blocked 2", types.StatusOpen, 3, types.TypeTask)
|
||||
|
||||
// Create one issue blocked by multiple issues (blocker + another)
|
||||
otherBlocker := env.CreateIssueWith("Other Blocker", types.StatusOpen, 1, types.TypeTask)
|
||||
multiBlocked := env.CreateIssueWith("Multi Blocked", types.StatusOpen, 2, types.TypeTask)
|
||||
|
||||
// Add dependencies (issue depends on blocker)
|
||||
env.AddDep(blocked1, blocker)
|
||||
env.AddDep(blocked2, blocker)
|
||||
env.AddDep(multiBlocked, blocker)
|
||||
env.AddDep(multiBlocked, otherBlocker)
|
||||
|
||||
// Close the blocker
|
||||
env.Close(blocker, "Done")
|
||||
|
||||
// Get newly unblocked issues
|
||||
ctx := context.Background()
|
||||
unblocked, err := env.Store.GetNewlyUnblockedByClose(ctx, blocker.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("GetNewlyUnblockedByClose failed: %v", err)
|
||||
}
|
||||
|
||||
// Should return blocked1 and blocked2 (but not multiBlocked, which is still blocked by otherBlocker)
|
||||
if len(unblocked) != 2 {
|
||||
t.Errorf("Expected 2 unblocked issues, got %d", len(unblocked))
|
||||
}
|
||||
|
||||
// Check that the right issues are unblocked
|
||||
unblockedIDs := make(map[string]bool)
|
||||
for _, issue := range unblocked {
|
||||
unblockedIDs[issue.ID] = true
|
||||
}
|
||||
|
||||
if !unblockedIDs[blocked1.ID] {
|
||||
t.Errorf("Expected %s to be unblocked", blocked1.ID)
|
||||
}
|
||||
if !unblockedIDs[blocked2.ID] {
|
||||
t.Errorf("Expected %s to be unblocked", blocked2.ID)
|
||||
}
|
||||
if unblockedIDs[multiBlocked.ID] {
|
||||
t.Errorf("Expected %s to still be blocked (has another blocker)", multiBlocked.ID)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user