From 2066d29d02945665b85bc79e54b24a552b64860f Mon Sep 17 00:00:00 2001
From: Taksh <takshkothari09@gmail.com>
Date: Fri, 31 Jul 2026 15:15:58 +0300
Subject: [PATCH 1/4] fix(cli): make repos create --channel required

Optional binding left new repos on an opaque git 404 until a later bind.
Require the UUID up front so announcements always carry buzz-channel (#3539).

Signed-off-by: Taksh <takshkothari09@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
---
 crates/buzz-cli/src/lib.rs | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/crates/buzz-cli/src/lib.rs b/crates/buzz-cli/src/lib.rs
index 0726406d29..ff356f27fd 100644
--- a/crates/buzz-cli/src/lib.rs
+++ b/crates/buzz-cli/src/lib.rs
@@ -1132,11 +1132,11 @@ pub enum ReposCmd {
         /// Preferred Nostr relay(s) for repo discovery — can be specified multiple times
         #[arg(long = "nostr-relay")]
         relays: Vec<String>,
-        /// Channel UUID to bind the repo to. The `buzz-channel` tag is the
-        /// git ACL: without it the relay 404s every clone/fetch/push until
-        /// the author runs `buzz repos bind` (issue #3527).
+        /// Channel UUID that binds this repo for the relay git read gate
+        /// (`buzz-channel` tag). Required — without it every clone/fetch/push
+        /// returns an opaque 404 until `buzz repos bind` (issue #3539 / #3527).
         #[arg(long)]
-        channel: Option<String>,
+        channel: String,
     },
     /// Get a repository announcement
     Get {

From c6c5b3a710383029d046c4eedb40c8a4a25fd7af Mon Sep 17 00:00:00 2001
From: Taksh <takshkothari09@gmail.com>
Date: Fri, 31 Jul 2026 15:15:58 +0300
Subject: [PATCH 2/4] fix(cli): always emit buzz-channel from repos create

build_create_announcement now takes a required UUID and attaches exactly
one binding tag; drop the optional no-binding path.

Signed-off-by: Taksh <takshkothari09@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
---
 crates/buzz-cli/src/commands/repos.rs | 44 ++++++++-------------------
 1 file changed, 13 insertions(+), 31 deletions(-)

diff --git a/crates/buzz-cli/src/commands/repos.rs b/crates/buzz-cli/src/commands/repos.rs
index 608d495055..9ecb057e95 100644
--- a/crates/buzz-cli/src/commands/repos.rs
+++ b/crates/buzz-cli/src/commands/repos.rs
@@ -214,14 +214,14 @@ async fn submit_repo_update(client: &BuzzClient, builder: EventBuilder) -> Resul
     Ok(())
 }
 
-/// Build the kind:30617 announcement for `repos create`, including the
-/// `buzz-channel` binding when requested.
+/// Build the kind:30617 announcement for `repos create` with a required
+/// `buzz-channel` binding.
 ///
 /// Pure (no I/O) so the emitted tags are unit-testable. Exactly one
 /// validated `buzz-channel` tag is appended — the tag is the git ACL
-/// (issue #3527: without it the relay 404s every clone/fetch/push), so the
-/// UUID is shape-validated here and its existence/membership is the relay's
-/// authority at git-access time, same posture as `repos bind`.
+/// (issue #3539 / #3527: without it the relay 404s every clone/fetch/push),
+/// so the UUID is shape-validated here and its existence/membership is the
+/// relay's authority at git-access time, same posture as `repos bind`.
 #[allow(clippy::too_many_arguments)]
 fn build_create_announcement(
     repo_id: &str,
@@ -230,9 +230,10 @@ fn build_create_announcement(
     clone_urls: &[String],
     web_url: Option<&str>,
     relays: &[String],
-    channel: Option<&str>,
+    channel: &str,
 ) -> Result<EventBuilder, CliError> {
     validate_repo_id(repo_id)?;
+    crate::validate::validate_uuid(channel)?;
 
     let clone_refs: Vec<&str> = clone_urls.iter().map(|s| s.as_str()).collect();
     let relay_refs: Vec<&str> = relays.iter().map(|s| s.as_str()).collect();
@@ -247,10 +248,7 @@ fn build_create_announcement(
     )
     .map_err(|e| CliError::Other(format!("build_repo_announcement failed: {e}")))?;
 
-    if let Some(channel) = channel {
-        crate::validate::validate_uuid(channel)?;
-        builder = builder.tag(Tag::parse(["buzz-channel", channel]).map_err(tag_error)?);
-    }
+    builder = builder.tag(Tag::parse(["buzz-channel", channel]).map_err(tag_error)?);
     Ok(builder)
 }
 
@@ -263,7 +261,7 @@ pub async fn cmd_create_repo(
     clone_urls: &[String],
     web_url: Option<&str>,
     relays: &[String],
-    channel: Option<&str>,
+    channel: &str,
 ) -> Result<(), CliError> {
     let builder = build_create_announcement(
         repo_id,
@@ -437,7 +435,7 @@ pub async fn dispatch(cmd: crate::ReposCmd, client: &BuzzClient) -> Result<(), C
                 &clone_urls,
                 web.as_deref(),
                 &relays,
-                channel.as_deref(),
+                &channel,
             )
             .await
         }
@@ -770,7 +768,7 @@ mod tests {
         assert!(matches!(error, crate::error::CliError::Usage(_)));
     }
 
-    /// Issue #3527: `repos create --channel` must emit exactly one
+    /// Issue #3539 / #3527: `repos create --channel` must emit exactly one
     /// `buzz-channel` tag so the primary create command stops producing
     /// repos the relay 404s forever.
     #[test]
@@ -783,7 +781,7 @@ mod tests {
             &["https://relay.example/git/owner/demo".to_string()],
             None,
             &[],
-            Some(&channel),
+            &channel,
         )
         .expect("build create announcement")
         .sign_with_keys(&Keys::generate())
@@ -805,25 +803,9 @@ mod tests {
             .any(|tag| tag.as_slice() == ["name", "Demo"]));
     }
 
-    #[test]
-    fn create_without_channel_emits_no_binding_tag() {
-        let event = build_create_announcement("demo", None, None, &[], None, &[], None)
-            .expect("build create announcement")
-            .sign_with_keys(&Keys::generate())
-            .expect("sign create announcement");
-
-        assert!(
-            !event
-                .tags
-                .iter()
-                .any(|tag| tag.as_slice().first().map(String::as_str) == Some("buzz-channel")),
-            "no --channel means no binding tag (vanilla NIP-34 stays possible)"
-        );
-    }
-
     #[test]
     fn create_rejects_malformed_channel_uuid() {
-        let error = build_create_announcement("demo", None, None, &[], None, &[], Some("nope"))
+        let error = build_create_announcement("demo", None, None, &[], None, &[], "nope")
             .expect_err("malformed channel id must not build an announcement");
         assert!(matches!(error, crate::error::CliError::Usage(_)));
     }

From d7904767bceb388896ffac1bef9ef59daadb3fb4 Mon Sep 17 00:00:00 2001
From: Taksh <takshkothari09@gmail.com>
Date: Fri, 31 Jul 2026 15:15:58 +0300
Subject: [PATCH 3/4] docs(agents): include --channel in nest repos create
 example

Keep the managed-agent skill aligned with the required clap flag.

Signed-off-by: Taksh <takshkothari09@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
---
 desktop/src-tauri/src/managed_agents/nest_skill.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/desktop/src-tauri/src/managed_agents/nest_skill.md b/desktop/src-tauri/src/managed_agents/nest_skill.md
index 79a5ea301d..a5d29cf916 100644
--- a/desktop/src-tauri/src/managed_agents/nest_skill.md
+++ b/desktop/src-tauri/src/managed_agents/nest_skill.md
@@ -43,7 +43,7 @@ Run `buzz agents draft-update --help` for optional runtime, provider, model, ren
 
 ## Git Repositories
 
-Buzz hosts real git repos, and **you can own one yourself** — no human key needed. `repos create` signs the announcement with *your* key, so the repo is owned by whoever runs it; the owner segment in the clone URL is your own pubkey (hex, not a username). Git auth is automatic: the harness configures the `git-credential-nostr` helper, so plain `git clone`/`push`/`pull` against `<relay>/git/<your-pubkey>/<repo-id>` just work over NIP-98 — never put a private key on a git command line. Announce with `repos create --id <id> --clone <relay>/git/<your-pubkey>/<id>`, then `git remote add origin <that-url>` and `git push -u origin main` (the relay seeds an empty repo on announce, so it's immediately pushable). Requires git 2.46+ for the credential protocol.
+Buzz hosts real git repos, and **you can own one yourself** — no human key needed. `repos create` signs the announcement with *your* key, so the repo is owned by whoever runs it; the owner segment in the clone URL is your own pubkey (hex, not a username). Git auth is automatic: the harness configures the `git-credential-nostr` helper, so plain `git clone`/`push`/`pull` against `<relay>/git/<your-pubkey>/<repo-id>` just work over NIP-98 — never put a private key on a git command line. Announce with `repos create --id <id> --channel <UUID> --clone <relay>/git/<your-pubkey>/<id>`, then `git remote add origin <that-url>` and `git push -u origin main` (the relay seeds an empty repo on announce, so it's immediately pushable). Requires git 2.46+ for the credential protocol.
 
 Manage your repository's enforced branch and tag rules with `repos protect list|set|remove`. Ref patterns must use full Git names such as `refs/heads/main` or `refs/tags/*`; supported rules are `--push owner|admin|member`, `--no-force-push`, `--no-delete`, and `--require-patch`. `protect set` replaces the complete rule for that exact pattern, so omitted constraints are removed. Protection updates preserve every unrelated metadata tag and return exit code 5 when a newer NIP-33 head wins a concurrent write.
 

From 8d3f063f5e7abc115c76583592a9d6e063449422 Mon Sep 17 00:00:00 2001
From: Taksh <takshkothari09@gmail.com>
Date: Fri, 31 Jul 2026 15:16:13 +0300
Subject: [PATCH 4/4] docs(cli): show required --channel in repos create help

Surface the git ACL requirement in clap after_help so operators see it
before hitting a relay 404.

Signed-off-by: Taksh <takshkothari09@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
---
 crates/buzz-cli/src/lib.rs | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/crates/buzz-cli/src/lib.rs b/crates/buzz-cli/src/lib.rs
index ff356f27fd..aa81a953a7 100644
--- a/crates/buzz-cli/src/lib.rs
+++ b/crates/buzz-cli/src/lib.rs
@@ -1113,6 +1113,11 @@ pub enum NotesCmd {
 #[derive(Subcommand)]
 pub enum ReposCmd {
     /// Announce a git repository (NIP-34)
+    #[command(
+        after_help = "The relay git read gate requires a buzz-channel binding. Always pass --channel.\n\n\
+Examples:\n  \
+buzz repos create --id demo --channel <UUID> --clone https://<relay>/git/<pubkey>/demo"
+    )]
     Create {
         /// Repository identifier: [a-zA-Z0-9._-]{1,64}
         #[arg(long)]
