feat(ready): add --unassigned filter for bd ready

Add -u/--unassigned flag to bd ready command to show only issues
with no assignee. This supports the SCAVENGE protocol where polecats
query the 'Salvage Yard' for unassigned ready work.

Changes:
- Add NoAssignee field to WorkFilter struct
- Update SQLite GetReadyWork to filter by empty/null assignee
- Add --unassigned/-u flag to ready command
- Update RPC protocol and daemon handler
- Add test for NoAssignee filter functionality

Fixes: gt-3rp

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Steve Yegge
2025-11-30 00:37:26 -08:00
parent 19eec843d9
commit 64771a28d2
5 changed files with 169 additions and 80 deletions

View File

@@ -39,7 +39,7 @@ var readyCmd = &cobra.Command{
priority, _ := cmd.Flags().GetInt("priority")
filter.Priority = &priority
}
if assignee != "" {
if assignee != "" && !unassigned {
filter.Assignee = &assignee
}
// Validate sort policy

View File

@@ -260,3 +260,91 @@ func TestReadyCommandInit(t *testing.T) {
t.Error("readyCmd should have Short description")
}
}
func TestReadyWorkUnassigned(t *testing.T) {
tmpDir := t.TempDir()
testDB := filepath.Join(tmpDir, ".beads", "beads.db")
s := newTestStore(t, testDB)
ctx := context.Background()
// Create issues with different assignees
issues := []*types.Issue{
{
ID: "test-unassigned-1",
Title: "Unassigned task 1",
Status: types.StatusOpen,
Priority: 1,
IssueType: types.TypeTask,
Assignee: "",
CreatedAt: time.Now(),
},
{
ID: "test-unassigned-2",
Title: "Unassigned task 2",
Status: types.StatusOpen,
Priority: 2,
IssueType: types.TypeTask,
CreatedAt: time.Now(),
},
{
ID: "test-assigned-alice",
Title: "Alice's task",
Status: types.StatusOpen,
Priority: 1,
IssueType: types.TypeTask,
Assignee: "alice",
CreatedAt: time.Now(),
},
{
ID: "test-assigned-bob",
Title: "Bob's task",
Status: types.StatusOpen,
Priority: 1,
IssueType: types.TypeTask,
Assignee: "bob",
CreatedAt: time.Now(),
},
}
for _, issue := range issues {
if err := s.CreateIssue(ctx, issue, "test"); err != nil {
t.Fatal(err)
}
}
// Test filtering by --unassigned
readyUnassigned, err := s.GetReadyWork(ctx, types.WorkFilter{
Unassigned: true,
})
if err != nil {
t.Fatalf("GetReadyWork with Unassigned filter failed: %v", err)
}
// Should only have unassigned issues
if len(readyUnassigned) != 2 {
t.Errorf("Expected 2 unassigned issues, got %d", len(readyUnassigned))
}
for _, issue := range readyUnassigned {
if issue.Assignee != "" {
t.Errorf("Expected no assignee, got %q for issue %s", issue.Assignee, issue.ID)
}
}
// Test that Unassigned takes precedence over Assignee filter
alice := "alice"
readyConflict, err := s.GetReadyWork(ctx, types.WorkFilter{
Unassigned: true,
Assignee: &alice,
})
if err != nil {
t.Fatalf("GetReadyWork with conflicting filters failed: %v", err)
}
// Unassigned should win, returning only unassigned issues
for _, issue := range readyConflict {
if issue.Assignee != "" {
t.Errorf("Unassigned should override Assignee filter, got %q for issue %s", issue.Assignee, issue.ID)
}
}
}