+++ 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: ```rust /// 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, }, ``` Add dispatch arm: ```rust Commands::Next { filter } => cmd_next(filter, cli.json).await, ``` Implement `cmd_next`: ```rust async fn cmd_next(filter_args: Vec, 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: ```rust 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: ```markdown ### 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:** ```sh 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).