feat(nbd): add claude-md command [fc444f]

Adds `nbd claude-md` subcommand that prints a ready-to-paste CLAUDE.md
snippet for adopting nbd in any project. The snippet is embedded at
compile time via `include_str!` from `src/claude_md_snippet.md`, so it
stays in sync with the installed binary automatically.

- `nbd claude-md` prints raw markdown (suitable for `>> CLAUDE.md`)
- `nbd claude-md --json` outputs `{"snippet": "..."}` for programmatic use
- Works without a `.nbd/` store present (no find_nbd_root call)
- 4 new integration tests covering all specified behaviours

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
quotesdb
Elijah Voigt 3 months ago
parent deecdba592
commit ba0efeb622

@ -1,9 +1,8 @@
{ {
"id": "fc444f",
"title": "nbd claude-md command", "title": "nbd claude-md command",
"body": "Add `nbd claude-md` subcommand that prints a ready-to-paste CLAUDE.md snippet for adopting `nbd` in any project. The snippet content is maintained as a source file and baked into the binary at compile time via `include_str!`.\n\n## Motivation\n\nEvery project that wants to use `nbd` needs the same boilerplate in its CLAUDE.md: what `nbd` is, how to invoke it, the create/update/done workflow, and the key guidelines. Without this command, that block has to be written by hand and tends to drift out of date as `nbd` evolves. By owning the canonical snippet in the `nbd` source tree and embedding it into the binary, the snippet stays in sync with the tool automatically — projects just run `nbd claude-md >> CLAUDE.md` when they adopt or upgrade `nbd`.\n\n## Snippet file\n\nCreate `src/claude_md_snippet.md`. This is a plain markdown file, checked into the repository, that contains the CLAUDE.md section any adopting project should paste in. It should cover:\n\n- One-sentence description of `nbd`.\n- Initialisation (`nbd init`).\n- The four core commands with examples (`create`, `list`, `read`, `update`).\n- The `nbd ready` command for finding unblocked tickets.\n- The workflow (create before starting → set in_progress → set done).\n- Guidelines: always `--json`, priority scale, type choices, dep usage.\n\nThe file is written for a project where `nbd` is installed in PATH (i.e. not via `cargo run`). It should be self-contained — no references to the `nbd` crate internals.\n\n## Binary embedding\n\nIn `main.rs`, embed the snippet at compile time:\n\n```rust\nconst CLAUDE_MD_SNIPPET: &str = include_str!(\"claude_md_snippet.md\");\n```\n\n`include_str!` resolves paths relative to the source file (`src/`), so this looks for `src/claude_md_snippet.md`. Cargo rebuilds the binary automatically when the file changes.\n\n## Command implementation\n\nAdd `ClaudeMd` variant to `Commands` in `main.rs`:\n\n```rust\n/// Print a CLAUDE.md snippet for adopting nbd in a project.\nClaudeMd,\n```\n\nThe `--json` global flag applies:\n- Without `--json`: `print!(\"{CLAUDE_MD_SNIPPET}\")` — raw markdown, suitable for redirect (`nbd claude-md >> CLAUDE.md`).\n- With `--json`: output `{\"snippet\": \"<escaped content>\"}` — for programmatic consumption.\n\nNo store access needed — this command is pure output from the embedded constant. It does not call `find_nbd_root()`.\n\n## No display.rs changes needed\n\nThe output is a single `print!` call in the command handler. No tabular formatting.\n\n## Tests\n\nIntegration tests (`tests/integration.rs`):\n- `nbd claude-md` exits zero and stdout is non-empty.\n- stdout contains key strings (`\"nbd\"`, `\"CLAUDE.md\"` or similar section header, `\"--json\"`).\n- `nbd claude-md --json` exits zero and stdout is valid JSON with a `\"snippet\"` key whose value is a non-empty string.\n- `nbd claude-md` works even when run from a directory with no `.nbd/` (no `find_nbd_root` call).\n\n## Files touched\n- `src/claude_md_snippet.md` — new file; the canonical snippet content\n- `src/main.rs` — `include_str!` constant, `ClaudeMd` command variant and handler\n- `tests/integration.rs` — integration tests\n- `README.md` — mention `nbd claude-md` in the Usage section", "body": "Add `nbd claude-md` subcommand that prints a ready-to-paste CLAUDE.md snippet for adopting `nbd` in any project. The snippet content is maintained as a source file and baked into the binary at compile time via `include_str!`.\n\n## Motivation\n\nEvery project that wants to use `nbd` needs the same boilerplate in its CLAUDE.md: what `nbd` is, how to invoke it, the create/update/done workflow, and the key guidelines. Without this command, that block has to be written by hand and tends to drift out of date as `nbd` evolves. By owning the canonical snippet in the `nbd` source tree and embedding it into the binary, the snippet stays in sync with the tool automatically — projects just run `nbd claude-md >> CLAUDE.md` when they adopt or upgrade `nbd`.\n\n## Snippet file\n\nCreate `src/claude_md_snippet.md`. This is a plain markdown file, checked into the repository, that contains the CLAUDE.md section any adopting project should paste in. It should cover:\n\n- One-sentence description of `nbd`.\n- Initialisation (`nbd init`).\n- The four core commands with examples (`create`, `list`, `read`, `update`).\n- The `nbd ready` command for finding unblocked tickets.\n- The workflow (create before starting → set in_progress → set done).\n- Guidelines: always `--json`, priority scale, type choices, dep usage.\n\nThe file is written for a project where `nbd` is installed in PATH (i.e. not via `cargo run`). It should be self-contained — no references to the `nbd` crate internals.\n\n## Binary embedding\n\nIn `main.rs`, embed the snippet at compile time:\n\n```rust\nconst CLAUDE_MD_SNIPPET: &str = include_str!(\"claude_md_snippet.md\");\n```\n\n`include_str!` resolves paths relative to the source file (`src/`), so this looks for `src/claude_md_snippet.md`. Cargo rebuilds the binary automatically when the file changes.\n\n## Command implementation\n\nAdd `ClaudeMd` variant to `Commands` in `main.rs`:\n\n```rust\n/// Print a CLAUDE.md snippet for adopting nbd in a project.\nClaudeMd,\n```\n\nThe `--json` global flag applies:\n- Without `--json`: `print!(\"{CLAUDE_MD_SNIPPET}\")` — raw markdown, suitable for redirect (`nbd claude-md >> CLAUDE.md`).\n- With `--json`: output `{\"snippet\": \"<escaped content>\"}` — for programmatic consumption.\n\nNo store access needed — this command is pure output from the embedded constant. It does not call `find_nbd_root()`.\n\n## No display.rs changes needed\n\nThe output is a single `print!` call in the command handler. No tabular formatting.\n\n## Tests\n\nIntegration tests (`tests/integration.rs`):\n- `nbd claude-md` exits zero and stdout is non-empty.\n- stdout contains key strings (`\"nbd\"`, `\"CLAUDE.md\"` or similar section header, `\"--json\"`).\n- `nbd claude-md --json` exits zero and stdout is valid JSON with a `\"snippet\"` key whose value is a non-empty string.\n- `nbd claude-md` works even when run from a directory with no `.nbd/` (no `find_nbd_root` call).\n\n## Files touched\n- `src/claude_md_snippet.md` — new file; the canonical snippet content\n- `src/main.rs` — `include_str!` constant, `ClaudeMd` command variant and handler\n- `tests/integration.rs` — integration tests\n- `README.md` — mention `nbd claude-md` in the Usage section",
"priority": 6, "priority": 6,
"status": "todo", "status": "done",
"dependencies": [], "dependencies": [],
"ticket_type": "feature" "ticket_type": "feature"
} }

@ -111,6 +111,19 @@ nbd ready
nbd ready --json nbd ready --json
``` ```
### Print a CLAUDE.md snippet
Print a ready-to-paste section for adopting `nbd` in any project's CLAUDE.md:
```sh
nbd claude-md # raw markdown, suitable for redirect
nbd claude-md >> CLAUDE.md # append to your project's CLAUDE.md
nbd claude-md --json # machine-readable {"snippet": "..."}
```
The snippet is embedded in the binary at compile time, so it is always in sync
with the installed version of `nbd`.
### Migrate ticket files ### Migrate ticket files
Re-serialise all ticket files through the current schema. Use this after Re-serialise all ticket files through the current schema. Use this after
@ -136,6 +149,8 @@ cargo run -- read <id>
cargo run -- update <id> --status in_progress cargo run -- update <id> --status in_progress
cargo run -- archive <id> cargo run -- archive <id>
cargo run -- list --json cargo run -- list --json
cargo run -- claude-md
cargo run -- claude-md --json
``` ```
## Testing ## Testing

@ -0,0 +1,53 @@
## Task Tracking with nbd
`nbd` is a CLI tool for managing work tickets, designed for agent workflows.
### Initialisation
```sh
nbd init
```
Run once in the project root. Creates `.nbd/tickets/`. Safe to run multiple times.
### Core commands
```sh
# Create a new ticket
nbd create --title "Add OAuth login" --type feature --priority 7
# List all open tickets (sorted by priority)
nbd list
# Read a specific ticket
nbd read <id>
# Update a ticket
nbd update <id> --status in_progress
nbd update <id> --status done
```
### Finding what to work on
```sh
# All tickets that are unblocked and ready to start
nbd ready
# The single highest-priority unblocked ticket
nbd next
```
### Workflow
1. **Before starting** — create a ticket: `nbd create --title "..." --json`
2. **When starting** — mark it in progress: `nbd update <id> --status in_progress`
3. **When done** — mark it complete: `nbd update <id> --status done`
### Guidelines
- **Always pass `--json`** to every command for structured, unambiguous output.
- Use `jq` to parse and transform JSON output when needed.
- Priority scale 010: use **79** for bugs, **5** for normal tasks, **3** for nice-to-haves.
- `--type` choices: `project`, `feature`, `task`, `bug`.
- Use `--deps id1,id2` to express blockers — tickets that must be done first.
- Create tickets *before* starting non-trivial tasks, not after.

@ -9,6 +9,15 @@ mod filter;
mod store; mod store;
mod ticket; mod ticket;
/// The CLAUDE.md snippet embedded at compile time from `src/claude_md_snippet.md`.
///
/// Projects adopting `nbd` can append this to their own CLAUDE.md:
///
/// ```sh
/// nbd claude-md >> CLAUDE.md
/// ```
const CLAUDE_MD_SNIPPET: &str = include_str!("claude_md_snippet.md");
#[cfg(test)] #[cfg(test)]
mod tests; mod tests;
@ -163,6 +172,21 @@ enum Commands {
id: String, id: String,
}, },
/// Print a CLAUDE.md snippet for adopting nbd in a project.
///
/// The snippet is baked into the binary at compile time and covers
/// the nbd workflow, core commands, and usage guidelines.
///
/// Append the snippet to your project's CLAUDE.md:
///
/// ```sh
/// nbd claude-md >> CLAUDE.md
/// ```
///
/// With `--json`, outputs `{"snippet": "..."}` for programmatic use.
/// Does not require a `.nbd/` store to be present.
ClaudeMd,
/// Update fields of an existing ticket and print the result. /// Update fields of an existing ticket and print the result.
/// ///
/// Only the flags you supply are changed; all other fields retain their /// Only the flags you supply are changed; all other fields retain their
@ -230,6 +254,8 @@ async fn dispatch(cli: Cli) -> store::Result<()> {
Commands::Archive { id } => cmd_archive(id, cli.json).await, Commands::Archive { id } => cmd_archive(id, cli.json).await,
Commands::ClaudeMd => cmd_claude_md(cli.json),
Commands::Read { id } => cmd_read(id, cli.json).await, Commands::Read { id } => cmd_read(id, cli.json).await,
Commands::List { filter, all } => cmd_list(filter, all, cli.json).await, Commands::List { filter, all } => cmd_list(filter, all, cli.json).await,
@ -590,6 +616,24 @@ async fn cmd_archive(id: String, json: bool) -> store::Result<()> {
Ok(()) Ok(())
} }
/// Print the embedded CLAUDE.md snippet to stdout.
///
/// With `json = true`, wraps the snippet in `{"snippet": "..."}` for
/// programmatic consumption. Without `--json`, emits the raw markdown so the
/// caller can redirect it directly into a CLAUDE.md file.
///
/// This command never calls [`find_nbd_root`] — it works even when no `.nbd/`
/// store exists.
fn cmd_claude_md(json: bool) -> store::Result<()> {
if json {
let value = serde_json::json!({ "snippet": CLAUDE_MD_SNIPPET });
println!("{}", serde_json::to_string_pretty(&value)?);
} else {
print!("{CLAUDE_MD_SNIPPET}");
}
Ok(())
}
/// Update the specified fields of an existing ticket, persist it, and print it. /// Update the specified fields of an existing ticket, persist it, and print it.
/// ///
/// Only the flags explicitly passed on the command line are applied; all other /// Only the flags explicitly passed on the command line are applied; all other

@ -1255,6 +1255,99 @@ fn next_json_includes_id_field() {
assert_eq!(id.len(), 6, "id should be 6 characters"); assert_eq!(id.len(), 6, "id should be 6 characters");
} }
// ── nbd claude-md tests ───────────────────────────────────────────────────────
/// `nbd claude-md` exits zero and stdout is non-empty.
#[test]
fn claude_md_exits_zero_with_output() {
let tmp = tempfile::tempdir().expect("failed to create tempdir");
let output = std::process::Command::new(env!("CARGO_BIN_EXE_nbd"))
.args(["claude-md"])
.current_dir(tmp.path())
.output()
.expect("failed to spawn nbd");
assert!(
output.status.success(),
"claude-md should exit zero, stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
assert!(
!output.stdout.is_empty(),
"claude-md stdout should be non-empty"
);
}
/// `nbd claude-md` stdout contains key strings (`nbd`, `--json`).
#[test]
fn claude_md_contains_key_content() {
let tmp = tempfile::tempdir().expect("failed to create tempdir");
let output = std::process::Command::new(env!("CARGO_BIN_EXE_nbd"))
.args(["claude-md"])
.current_dir(tmp.path())
.output()
.expect("failed to spawn nbd");
assert!(output.status.success());
let stdout = String::from_utf8(output.stdout).unwrap();
assert!(
stdout.contains("nbd"),
"output should mention 'nbd', got: {stdout}"
);
assert!(
stdout.contains("--json"),
"output should mention '--json', got: {stdout}"
);
}
/// `nbd claude-md --json` exits zero and stdout is valid JSON with a `snippet` key.
#[test]
fn claude_md_json_flag_produces_valid_json() {
let tmp = tempfile::tempdir().expect("failed to create tempdir");
let output = std::process::Command::new(env!("CARGO_BIN_EXE_nbd"))
.args(["claude-md", "--json"])
.current_dir(tmp.path())
.output()
.expect("failed to spawn nbd");
assert!(
output.status.success(),
"claude-md --json should exit zero, stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
let stdout = String::from_utf8(output.stdout).unwrap();
let parsed: serde_json::Value =
serde_json::from_str(&stdout).expect("claude-md --json output should be valid JSON");
assert!(
parsed.get("snippet").is_some(),
"JSON should have a 'snippet' key, got: {stdout}"
);
let snippet = parsed["snippet"].as_str().unwrap();
assert!(!snippet.is_empty(), "snippet value should be non-empty");
}
/// `nbd claude-md` works even from a directory with no `.nbd/` store.
#[test]
fn claude_md_works_without_nbd_store() {
let tmp = tempfile::tempdir().expect("failed to create tempdir");
// Deliberately do NOT create a .nbd/ directory.
let output = std::process::Command::new(env!("CARGO_BIN_EXE_nbd"))
.args(["claude-md"])
.current_dir(tmp.path())
.output()
.expect("failed to spawn nbd");
assert!(
output.status.success(),
"claude-md should succeed even without a .nbd/ store, stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
assert!(
!output.stdout.is_empty(),
"claude-md should produce output even without .nbd/"
);
}
/// `nbd next --filter priority=99 --json` returns `{"next": null}` when no /// `nbd next --filter priority=99 --json` returns `{"next": null}` when no
/// ticket has the requested priority. /// ticket has the requested priority.
#[test] #[test]

Loading…
Cancel
Save