You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

5.6 KiB

+++ title = "Add nbd next subcommand" priority = 7 status = "done" ticket_type = "feature" dependencies = ["887344"] +++

Summary

Add nbd next subcommand that selects the single highest-priority ticket that is ready to work on. Supports --filter for additional narrowing within the ready set.

Depends on: --filter wired into CLI (ticket 887344). Implicitly depends on the TicketFilter module (ticket c2a024) through that.

Definition of "ready"

Same semantics as nbd ready:

  • status \!= done
  • Every ID in ticket.dependencies belongs to a ticket with status == done.
  • Missing dependency IDs are treated conservatively: the ticket is NOT ready.

Behaviour

  1. Load all tickets with list_tickets(&root) (already sorted by priority desc).
  2. Build done_ids set (same as cmd_ready).
  3. Find the first ticket (highest priority) where:
    • status != done
    • all dependencies are in done_ids
    • filter.matches(t) (if a filter was provided)
  4. If found: display the single ticket.
  5. If not found: print a "no ready tickets" message. Exit 0 (not an error).

Output

Without --json:

  • Found: print the ticket in the same tabular format as nbd read (via display::print_ticket).
  • Not found: No ready tickets.

With --json:

  • Found: {"next": { ...ticket fields including id... }}
  • Not found: {"next": null}

Wrapping in {"next": ...} (rather than bare ticket or bare null) makes the JSON unambiguously parseable by consumers — they always get an object with a next key.

Implementation

Add to Commands enum:

/// Choose the highest-priority ticket that is ready to work on.
///
/// A ticket is ready when its status is not `done` and every ticket it
/// depends on has status `done`. Returns the single highest-priority
/// ready ticket, optionally narrowed by `--filter KEY=VALUE`.
///
/// Exits 0 even when no ready ticket exists.
Next {
    /// Filter ready tickets: key=value pairs (repeatable).
    /// AND between different keys, OR within same key.
    #[arg(long = "filter", value_name = "KEY=VALUE")]
    filter: Vec<String>,
},

Add dispatch arm:

Commands::Next { filter } => cmd_next(filter, cli.json).await,

Implement cmd_next:

async fn cmd_next(filter_args: Vec<String>, json: bool) -> store::Result<()> {
    let root = find_nbd_root()?;
    let all = list_tickets(&root).await?;  // sorted by priority desc
    let filter = filter::parse_filters(&filter_args)?;

    let done_ids: std::collections::HashSet<&str> = all
        .iter()
        .filter(|t| t.status == Status::Done)
        .map(|t| t.id.as_str())
        .collect();

    let next = all.iter().find(|t| {
        t.status \!= Status::Done
            && t.dependencies.iter().all(|dep| done_ids.contains(dep.as_str()))
            && filter.matches(t)
    });

    if json {
        match next {
            Some(ticket) => {
                let value = serde_json::json\!({
                    "next": display::ticket_to_json_value(ticket)
                });
                println\!("{}", serde_json::to_string_pretty(&value)?);
            }
            None => println\!("{}", serde_json::json\!({"next": null})),
        }
    } else {
        match next {
            Some(ticket) => display::print_ticket(ticket),
            None => println\!("No ready tickets."),
        }
    }

    Ok(())
}

display.rs: make ticket_to_json_value pub(crate)

ticket_to_json_value is currently private in display.rs. It needs to be accessible from cmd_next in main.rs. Change its visibility:

pub(crate) fn ticket_to_json_value(ticket: &Ticket) -> serde_json::Value { ... }

This is the cleanest approach — it reuses the existing id-injection logic rather than duplicating it.

README update

Add a ### Find the next ticket to work on section:

### Find the next ticket to work on

Returns the single highest-priority ticket that is ready to work on — not done
and with all dependencies completed.

```sh
nbd next
nbd next --json
nbd next --filter type=bug      # highest-priority ready bug
nbd next --filter priority=9    # highest-priority ready ticket with priority 9

Exits 0 even when no ready ticket exists.


## CLAUDE.md update

Update the "Workflow" section to mention `nbd next` as an alternative to `nbd ready`
when the caller just wants to begin the single most important task:

To get the single best ticket to work on next:

cargo run -- next --json

## Files touched

- `src/main.rs` — `Next` command variant, `cmd_next` handler, dispatch arm
- `src/display.rs` — `ticket_to_json_value` changed to `pub(crate)`
- `tests/integration.rs` — integration tests
- `README.md` — new section for `nbd next`
- `CLAUDE.md` — update workflow section

## Integration tests to add (tests/integration.rs)

- Three tickets: A (priority 5, no deps), B (priority 8, dep A), C (priority 7, no deps).
  `nbd next --json` returns C (highest priority ready ticket — B is blocked by A).
- After marking A done: `nbd next --json` returns B (priority 8, now unblocked).
- With only done tickets: `nbd next --json` returns `{"next": null}`.
- `nbd next` (no `--json`) with no ready tickets prints "No ready tickets." and exits 0.
- `nbd next --filter type=bug --json`: create a bug and a task, both ready.
  Returns the bug if it's the highest-priority bug, otherwise the highest-priority bug.
  (Create bug priority 8, task priority 9: with filter, should return the bug.)
- `nbd next --json` returns a JSON object with a `"next"` key containing all ticket fields
  including `"id"`.
- `nbd next --filter priority=99 --json` returns `{"next": null}` (no ticket has priority 99).