feat: add --unassigned flag to bd ready command

Adds the ability to filter ready work for issues with no assignee,
which is useful for the SCAVENGE protocol in Gas Town where polecats
need to query the "Salvage Yard" for unclaimed work.

Changes:
- Add Unassigned bool field to types.WorkFilter
- Add --unassigned/-u flag to bd ready command
- Update SQL query in GetReadyWork to filter for NULL/empty assignee
- Add Unassigned field to RPC ReadyArgs for daemon support
- Add tests for the new functionality

Closes: 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:02 -08:00
parent 08d2a58302
commit 53342732b5
8 changed files with 153 additions and 81 deletions

View File

@@ -16,18 +16,20 @@ var readyCmd = &cobra.Command{
Run: func(cmd *cobra.Command, args []string) {
limit, _ := cmd.Flags().GetInt("limit")
assignee, _ := cmd.Flags().GetString("assignee")
unassigned, _ := cmd.Flags().GetBool("unassigned")
sortPolicy, _ := cmd.Flags().GetString("sort")
labels, _ := cmd.Flags().GetStringSlice("label")
labelsAny, _ := cmd.Flags().GetStringSlice("label-any")
// Use global jsonOutput set by PersistentPreRun (respects config.yaml + env vars)
// Normalize labels: trim, dedupe, remove empty
labels = util.NormalizeLabels(labels)
labelsAny = util.NormalizeLabels(labelsAny)
filter := types.WorkFilter{
// Leave Status empty to get both 'open' and 'in_progress' (bd-165)
Limit: limit,
Unassigned: unassigned,
SortPolicy: types.SortPolicy(sortPolicy),
Labels: labels,
LabelsAny: labelsAny,
@@ -49,6 +51,7 @@ var readyCmd = &cobra.Command{
if daemonClient != nil {
readyArgs := &rpc.ReadyArgs{
Assignee: assignee,
Unassigned: unassigned,
Limit: limit,
SortPolicy: sortPolicy,
Labels: labels,
@@ -293,6 +296,7 @@ func init() {
readyCmd.Flags().IntP("limit", "n", 10, "Maximum issues to show")
readyCmd.Flags().IntP("priority", "p", 0, "Filter by priority")
readyCmd.Flags().StringP("assignee", "a", "", "Filter by assignee")
readyCmd.Flags().BoolP("unassigned", "u", false, "Show only unassigned issues")
readyCmd.Flags().StringP("sort", "s", "hybrid", "Sort policy: hybrid (default), priority, oldest")
readyCmd.Flags().StringSliceP("label", "l", []string{}, "Filter by labels (AND: must have ALL). Can combine with --label-any")
readyCmd.Flags().StringSlice("label-any", []string{}, "Filter by labels (OR: must have AT LEAST ONE). Can combine with --label")

View File

@@ -183,6 +183,35 @@ func TestReadySuite(t *testing.T) {
}
})
t.Run("ReadyWorkUnassigned", func(t *testing.T) {
// Test filtering for unassigned issues
readyUnassigned, err := s.GetReadyWork(ctx, types.WorkFilter{
Unassigned: true,
})
if err != nil {
t.Fatalf("GetReadyWork with unassigned filter failed: %v", err)
}
// All returned issues should have no assignee
for _, issue := range readyUnassigned {
if issue.Assignee != "" {
t.Errorf("Expected empty assignee, got %q for issue %s", issue.Assignee, issue.ID)
}
}
// Should include test-unassigned from previous test
found := false
for _, issue := range readyUnassigned {
if issue.ID == "test-unassigned" {
found = true
break
}
}
if !found {
t.Error("Expected to find test-unassigned in unassigned results")
}
})
t.Run("ReadyWorkInProgress", func(t *testing.T) {
// Create in-progress issue (should be in ready work)
issue := &types.Issue{