feat: Add bd repos multi-repo commands and fix bd ready for in_progress issues
- Add 'bd repos' command for multi-repository management (bd-123)
- bd repos list: show all cached repositories
- bd repos ready: aggregate ready work across repos
- bd repos stats: combined statistics across repos
- bd repos clear-cache: clear repository cache
- Requires global daemon (bd daemon --global)
- Fix bd ready to show in_progress issues (bd-165)
- bd ready now shows both 'open' and 'in_progress' issues with no blockers
- Allows epics/tasks ready to close to appear in ready work
- Critical P0 bug fix for workflow
- Apply code review improvements to repos implementation
- Use strongly typed RPC responses (remove interface{})
- Fix clear-cache lock handling (close connections outside lock)
- Add error collection for per-repo failures
- Add context timeouts (1-2s) to prevent hangs
- Add lock strategy comments
- Update documentation (README.md, AGENTS.md)
- Add comprehensive tests for both features
Amp-Thread-ID: https://ampcode.com/threads/T-1de989a1-1890-492c-9847-a34144259e0f
Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
@@ -10,18 +10,20 @@ import (
|
||||
)
|
||||
|
||||
// GetReadyWork returns issues with no open blockers
|
||||
// By default, shows both 'open' and 'in_progress' issues so epics/tasks
|
||||
// ready to close are visible (bd-165)
|
||||
func (s *SQLiteStorage) GetReadyWork(ctx context.Context, filter types.WorkFilter) ([]*types.Issue, error) {
|
||||
whereClauses := []string{}
|
||||
args := []interface{}{}
|
||||
|
||||
// Default to open status if not specified
|
||||
// Default to open OR in_progress if not specified (bd-165)
|
||||
if filter.Status == "" {
|
||||
filter.Status = types.StatusOpen
|
||||
whereClauses = append(whereClauses, "i.status IN ('open', 'in_progress')")
|
||||
} else {
|
||||
whereClauses = append(whereClauses, "i.status = ?")
|
||||
args = append(args, filter.Status)
|
||||
}
|
||||
|
||||
whereClauses = append(whereClauses, "i.status = ?")
|
||||
args = append(args, filter.Status)
|
||||
|
||||
if filter.Priority != nil {
|
||||
whereClauses = append(whereClauses, "i.priority = ?")
|
||||
args = append(args, *filter.Priority)
|
||||
|
||||
@@ -751,3 +751,74 @@ func TestDeepHierarchyBlocking(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetReadyWorkIncludesInProgress(t *testing.T) {
|
||||
store, cleanup := setupTestDB(t)
|
||||
defer cleanup()
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Create issues:
|
||||
// bd-1: open, no dependencies → READY
|
||||
// bd-2: in_progress, no dependencies → READY (bd-165)
|
||||
// bd-3: in_progress, depends on open issue → BLOCKED
|
||||
// bd-4: closed, no dependencies → NOT READY (closed)
|
||||
|
||||
issue1 := &types.Issue{Title: "Open Ready", Status: types.StatusOpen, Priority: 1, IssueType: types.TypeTask}
|
||||
issue2 := &types.Issue{Title: "In Progress Ready", Status: types.StatusInProgress, Priority: 2, IssueType: types.TypeEpic}
|
||||
issue3 := &types.Issue{Title: "In Progress Blocked", Status: types.StatusInProgress, Priority: 1, IssueType: types.TypeTask}
|
||||
issue4 := &types.Issue{Title: "Blocker", Status: types.StatusOpen, Priority: 1, IssueType: types.TypeTask}
|
||||
issue5 := &types.Issue{Title: "Closed", Status: types.StatusClosed, Priority: 1, IssueType: types.TypeTask}
|
||||
|
||||
store.CreateIssue(ctx, issue1, "test-user")
|
||||
store.CreateIssue(ctx, issue2, "test-user")
|
||||
store.UpdateIssue(ctx, issue2.ID, map[string]interface{}{"status": types.StatusInProgress}, "test-user")
|
||||
store.CreateIssue(ctx, issue3, "test-user")
|
||||
store.UpdateIssue(ctx, issue3.ID, map[string]interface{}{"status": types.StatusInProgress}, "test-user")
|
||||
store.CreateIssue(ctx, issue4, "test-user")
|
||||
store.CreateIssue(ctx, issue5, "test-user")
|
||||
store.CloseIssue(ctx, issue5.ID, "Done", "test-user")
|
||||
|
||||
// Add dependency: issue3 blocks on issue4
|
||||
store.AddDependency(ctx, &types.Dependency{IssueID: issue3.ID, DependsOnID: issue4.ID, Type: types.DepBlocks}, "test-user")
|
||||
|
||||
// Get ready work (default filter - no status specified)
|
||||
ready, err := store.GetReadyWork(ctx, types.WorkFilter{})
|
||||
if err != nil {
|
||||
t.Fatalf("GetReadyWork failed: %v", err)
|
||||
}
|
||||
|
||||
// Should have 3 ready issues:
|
||||
// - issue1 (open, no blockers)
|
||||
// - issue2 (in_progress, no blockers) ← this is the key test case for bd-165
|
||||
// - issue4 (open blocker, but itself has no blockers so it's ready to work on)
|
||||
if len(ready) != 3 {
|
||||
t.Logf("Ready issues:")
|
||||
for _, r := range ready {
|
||||
t.Logf(" - %s: %s (status: %s)", r.ID, r.Title, r.Status)
|
||||
}
|
||||
t.Fatalf("Expected 3 ready issues, got %d", len(ready))
|
||||
}
|
||||
|
||||
// Verify ready issues
|
||||
readyIDs := make(map[string]bool)
|
||||
for _, issue := range ready {
|
||||
readyIDs[issue.ID] = true
|
||||
}
|
||||
|
||||
if !readyIDs[issue1.ID] {
|
||||
t.Errorf("Expected %s (open, no blockers) to be ready", issue1.ID)
|
||||
}
|
||||
if !readyIDs[issue2.ID] {
|
||||
t.Errorf("Expected %s (in_progress, no blockers) to be ready - this is bd-165!", issue2.ID)
|
||||
}
|
||||
if !readyIDs[issue4.ID] {
|
||||
t.Errorf("Expected %s (open blocker, but itself unblocked) to be ready", issue4.ID)
|
||||
}
|
||||
if readyIDs[issue3.ID] {
|
||||
t.Errorf("Expected %s (in_progress, blocked) to NOT be ready", issue3.ID)
|
||||
}
|
||||
if readyIDs[issue5.ID] {
|
||||
t.Errorf("Expected %s (closed) to NOT be ready", issue5.ID)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user