From 58daa4b79bd76b26811c5c3af9362f608a0cb239 Mon Sep 17 00:00:00 2001 From: Jake Deaton <181679476+RMSTrucks@users.noreply.github.com> Date: Mon, 27 Jul 2026 17:47:24 -0600 Subject: [PATCH] feat(buzz-cli): add 'patches status-list' to read NIP-34 status events `buzz patches` could write status events (`patches status`) but not read them: `patches list` returns only kind:1617 and `patches get` fetches a patch by id. Patch/PR status (kind:1630-1633: open/merged/closed/draft) was therefore write-only from the CLI, so agent sign-off/review state could be set but never queried or audited. Add `patches status-list --repo-owner --repo-id [--patch ] [--limit N]`, mirroring cmd_list_patches but selecting the status kinds and reusing the existing client.query() path (NIP-98 auth, pagination, retries). Update the subcommand-inventory guard tests for the new command. Signed-off-by: Jake Deaton <181679476+RMSTrucks@users.noreply.github.com> Co-Authored-By: Claude Opus 4.8 (1M context) --- crates/buzz-cli/src/commands/patches.rs | 38 +++++++++++++++++++++++++ crates/buzz-cli/src/lib.rs | 19 +++++++++++-- 2 files changed, 55 insertions(+), 2 deletions(-) diff --git a/crates/buzz-cli/src/commands/patches.rs b/crates/buzz-cli/src/commands/patches.rs index 13f1714d06..1ec378129f 100644 --- a/crates/buzz-cli/src/commands/patches.rs +++ b/crates/buzz-cli/src/commands/patches.rs @@ -110,6 +110,38 @@ pub async fn cmd_list_patches( Ok(()) } +/// Read patch status events (kind:1630-1633) for a repo — the read side of sign-off. +/// Mirrors `cmd_list_patches` but selects status kinds instead of the patch kind, so +/// operator sign-off (a signed `merged` status event) is queryable, not write-only. +pub async fn cmd_list_status( + client: &BuzzClient, + repo_owner: &str, + repo_id: &str, + patch: Option<&str>, + limit: Option, +) -> Result<(), CliError> { + validate_hex64(repo_owner)?; + validate_repo_id(repo_id)?; + + let a_value = format!("30617:{repo_owner}:{repo_id}"); + let mut filter = serde_json::json!({ + "kinds": [1630, 1631, 1632, 1633], + "#a": [a_value] + }); + + if let Some(root) = patch { + validate_hex64(root)?; + filter["#e"] = serde_json::json!([root]); + } + if let Some(n) = limit { + filter["limit"] = serde_json::json!(n); + } + + let resp = client.query(&filter).await?; + println!("{resp}"); + Ok(()) +} + #[allow(clippy::too_many_arguments)] pub async fn cmd_patch_status( client: &BuzzClient, @@ -244,6 +276,12 @@ pub async fn dispatch(cmd: crate::PatchesCmd, client: &BuzzClient) -> Result<(), author, limit, } => cmd_list_patches(client, &repo_owner, &repo_id, author.as_deref(), limit).await, + PatchesCmd::StatusList { + repo_owner, + repo_id, + patch, + limit, + } => cmd_list_status(client, &repo_owner, &repo_id, patch.as_deref(), limit).await, PatchesCmd::Status { root, status, diff --git a/crates/buzz-cli/src/lib.rs b/crates/buzz-cli/src/lib.rs index 6ab81a082d..32d5a8d49b 100644 --- a/crates/buzz-cli/src/lib.rs +++ b/crates/buzz-cli/src/lib.rs @@ -1254,6 +1254,21 @@ pub enum PatchesCmd { #[arg(long)] limit: Option, }, + /// Read patch status events (NIP-34 kind:1630-1633) for a repo — the read side of sign-off + StatusList { + /// Repo owner pubkey (64-char hex) + #[arg(long)] + repo_owner: String, + /// Repo identifier (d-tag) + #[arg(long)] + repo_id: String, + /// Only status events referencing this patch root event id + #[arg(long)] + patch: Option, + /// Maximum number of results + #[arg(long)] + limit: Option, + }, /// Set status on a patch (open/merged/closed/draft — NIP-34 kind:1630-1633) Status { /// Root patch event id (first patch of the series/revision) @@ -1968,7 +1983,7 @@ mod tests { ); assert_eq!( names(&cmd, "patches"), - vec!["get", "list", "send", "status"] + vec!["get", "list", "send", "status", "status-list"] ); assert_eq!( names(&cmd, "issues"), @@ -2005,7 +2020,7 @@ mod tests { ("media", 1), ("messages", 8), ("pack", 2), - ("patches", 4), + ("patches", 5), ("pr", 5), ("reactions", 3), ("repos", 4),