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.
10 lines
5.8 KiB
JSON
10 lines
5.8 KiB
JSON
{
|
|
"title": "Add nbd next subcommand",
|
|
"body": "## Summary\n\nAdd `nbd next` subcommand that selects the single highest-priority ticket that is\nready to work on. Supports `--filter` for additional narrowing within the ready set.\n\nDepends on: `--filter` wired into CLI (ticket 887344). Implicitly depends on the\nTicketFilter module (ticket c2a024) through that.\n\n## Definition of \"ready\"\n\nSame semantics as `nbd ready`:\n- `status \\!= done`\n- Every ID in `ticket.dependencies` belongs to a ticket with `status == done`.\n- Missing dependency IDs are treated conservatively: the ticket is NOT ready.\n\n## Behaviour\n\n1. Load all tickets with `list_tickets(&root)` (already sorted by priority desc).\n2. Build `done_ids` set (same as `cmd_ready`).\n3. Find the first ticket (highest priority) where:\n - status \\!= done\n - all dependencies are in done_ids\n - `filter.matches(t)` (if a filter was provided)\n4. If found: display the single ticket.\n5. If not found: print a \"no ready tickets\" message. Exit 0 (not an error).\n\n## Output\n\nWithout `--json`:\n- Found: print the ticket in the same tabular format as `nbd read` (via `display::print_ticket`).\n- Not found: `No ready tickets.`\n\nWith `--json`:\n- Found: `{\"next\": { ...ticket fields including id... }}`\n- Not found: `{\"next\": null}`\n\nWrapping in `{\"next\": ...}` (rather than bare ticket or bare null) makes the JSON\nunambiguously parseable by consumers — they always get an object with a `next` key.\n\n## Implementation\n\nAdd to `Commands` enum:\n\n```rust\n/// Choose the highest-priority ticket that is ready to work on.\n///\n/// A ticket is ready when its status is not `done` and every ticket it\n/// depends on has status `done`. Returns the single highest-priority\n/// ready ticket, optionally narrowed by `--filter KEY=VALUE`.\n///\n/// Exits 0 even when no ready ticket exists.\nNext {\n /// Filter ready tickets: key=value pairs (repeatable).\n /// AND between different keys, OR within same key.\n #[arg(long = \"filter\", value_name = \"KEY=VALUE\")]\n filter: Vec<String>,\n},\n```\n\nAdd dispatch arm:\n\n```rust\nCommands::Next { filter } => cmd_next(filter, cli.json).await,\n```\n\nImplement `cmd_next`:\n\n```rust\nasync fn cmd_next(filter_args: Vec<String>, json: bool) -> store::Result<()> {\n let root = find_nbd_root()?;\n let all = list_tickets(&root).await?; // sorted by priority desc\n let filter = filter::parse_filters(&filter_args)?;\n\n let done_ids: std::collections::HashSet<&str> = all\n .iter()\n .filter(|t| t.status == Status::Done)\n .map(|t| t.id.as_str())\n .collect();\n\n let next = all.iter().find(|t| {\n t.status \\!= Status::Done\n && t.dependencies.iter().all(|dep| done_ids.contains(dep.as_str()))\n && filter.matches(t)\n });\n\n if json {\n match next {\n Some(ticket) => {\n let value = serde_json::json\\!({\n \"next\": display::ticket_to_json_value(ticket)\n });\n println\\!(\"{}\", serde_json::to_string_pretty(&value)?);\n }\n None => println\\!(\"{}\", serde_json::json\\!({\"next\": null})),\n }\n } else {\n match next {\n Some(ticket) => display::print_ticket(ticket),\n None => println\\!(\"No ready tickets.\"),\n }\n }\n\n Ok(())\n}\n```\n\n## display.rs: make ticket_to_json_value pub(crate)\n\n`ticket_to_json_value` is currently private in `display.rs`. It needs to be accessible\nfrom `cmd_next` in `main.rs`. Change its visibility:\n\n```rust\npub(crate) fn ticket_to_json_value(ticket: &Ticket) -> serde_json::Value { ... }\n```\n\nThis is the cleanest approach — it reuses the existing id-injection logic rather than\nduplicating it.\n\n## README update\n\nAdd a `### Find the next ticket to work on` section:\n\n```markdown\n### Find the next ticket to work on\n\nReturns the single highest-priority ticket that is ready to work on — not done\nand with all dependencies completed.\n\n```sh\nnbd next\nnbd next --json\nnbd next --filter type=bug # highest-priority ready bug\nnbd next --filter priority=9 # highest-priority ready ticket with priority 9\n```\n\nExits 0 even when no ready ticket exists.\n```\n\n## CLAUDE.md update\n\nUpdate the \"Workflow\" section to mention `nbd next` as an alternative to `nbd ready`\nwhen the caller just wants to begin the single most important task:\n\n```\n**To get the single best ticket to work on next:**\n\n```sh\ncargo run -- next --json\n```\n```\n\n## Files touched\n\n- `src/main.rs` — `Next` command variant, `cmd_next` handler, dispatch arm\n- `src/display.rs` — `ticket_to_json_value` changed to `pub(crate)`\n- `tests/integration.rs` — integration tests\n- `README.md` — new section for `nbd next`\n- `CLAUDE.md` — update workflow section\n\n## Integration tests to add (tests/integration.rs)\n\n- Three tickets: A (priority 5, no deps), B (priority 8, dep A), C (priority 7, no deps).\n `nbd next --json` returns C (highest priority ready ticket — B is blocked by A).\n- After marking A done: `nbd next --json` returns B (priority 8, now unblocked).\n- With only done tickets: `nbd next --json` returns `{\"next\": null}`.\n- `nbd next` (no `--json`) with no ready tickets prints \"No ready tickets.\" and exits 0.\n- `nbd next --filter type=bug --json`: create a bug and a task, both ready.\n Returns the bug if it's the highest-priority bug, otherwise the highest-priority bug.\n (Create bug priority 8, task priority 9: with filter, should return the bug.)\n- `nbd next --json` returns a JSON object with a `\"next\"` key containing all ticket fields\n including `\"id\"`.\n- `nbd next --filter priority=99 --json` returns `{\"next\": null}` (no ticket has priority 99).",
|
|
"priority": 7,
|
|
"status": "todo",
|
|
"dependencies": [
|
|
"887344"
|
|
],
|
|
"ticket_type": "feature"
|
|
} |