From 5f6002a1057f73d0d7a225d58e107bb50fa4e04d Mon Sep 17 00:00:00 2001
From: Will Pfleger <wpfleger@block.xyz>
Date: Mon, 18 May 2026 18:36:30 -0400
Subject: [PATCH 01/10] feat(nest): add sprout-cli agent skill to nest
 initialization

Agents migrating from the MCP server to the CLI have no structured
guidance on command usage, output formats, or polling patterns. Ships
a 1,350-word SKILL.md covering all 12 command groups, written to
~/.sprout/.claude/skills/sprout-cli/ on first launch using the same
idempotent O_CREAT|O_EXCL pattern as AGENTS.md.

Signed-off-by: Will Pfleger <wpfleger@block.xyz>
---
 desktop/src-tauri/src/managed_agents/nest.rs  |  72 ++++
 .../src/managed_agents/nest_skill.md          | 364 ++++++++++++++++++
 2 files changed, 436 insertions(+)
 create mode 100644 desktop/src-tauri/src/managed_agents/nest_skill.md

diff --git a/desktop/src-tauri/src/managed_agents/nest.rs b/desktop/src-tauri/src/managed_agents/nest.rs
index 529455165a..e2d7ab9173 100644
--- a/desktop/src-tauri/src/managed_agents/nest.rs
+++ b/desktop/src-tauri/src/managed_agents/nest.rs
@@ -24,6 +24,10 @@ const NEST_DIRS: &[&str] = &[
 /// Fully static — no runtime interpolation, no secrets, no user paths.
 const AGENTS_MD: &str = include_str!("nest_agents.md");
 
+/// Default SKILL.md content for the sprout-cli Claude Code skill.
+/// Written to ~/.sprout/.claude/skills/sprout-cli/SKILL.md on first init.
+const SPROUT_CLI_SKILL_MD: &str = include_str!("nest_skill.md");
+
 /// Returns the nest root path (`~/.sprout`), or `None` if the home
 /// directory cannot be resolved.
 pub fn nest_dir() -> Option<PathBuf> {
@@ -98,6 +102,28 @@ pub fn ensure_nest_at(root: &Path) -> Result<(), String> {
         }
     }
 
+    // Write sprout-cli skill alongside AGENTS.md (same idempotent pattern).
+    let skill_dir = root.join(".claude/skills/sprout-cli");
+    fs::create_dir_all(&skill_dir)
+        .map_err(|e| format!("create {}: {e}", skill_dir.display()))?;
+
+    let skill_md = root.join(".claude/skills/sprout-cli/SKILL.md");
+    match fs::OpenOptions::new()
+        .write(true)
+        .create_new(true)
+        .open(&skill_md)
+    {
+        Ok(mut file) => {
+            use std::io::Write;
+            file.write_all(SPROUT_CLI_SKILL_MD.as_bytes())
+                .map_err(|e| format!("write {}: {e}", skill_md.display()))?;
+        }
+        Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => {}
+        Err(e) => {
+            return Err(format!("create {}: {e}", skill_md.display()));
+        }
+    }
+
     // Set owner-only permissions on root and all subdirectories.
     // Skip any path that is a symlink — chmod would affect the target.
     #[cfg(unix)]
@@ -117,6 +143,15 @@ pub fn ensure_nest_at(root: &Path) -> Result<(), String> {
                     .map_err(|e| format!("set permissions on {}: {e}", path.display()))?;
             }
         }
+        // Skill directory also gets 700 permissions.
+        let is_symlink = skill_dir
+            .symlink_metadata()
+            .map(|m| m.file_type().is_symlink())
+            .unwrap_or(false);
+        if !is_symlink {
+            fs::set_permissions(&skill_dir, perms.clone())
+                .map_err(|e| format!("set permissions on {}: {e}", skill_dir.display()))?;
+        }
     }
 
     Ok(())
@@ -260,6 +295,43 @@ mod tests {
         assert!(result.unwrap_err().contains("symlink"));
     }
 
+    #[test]
+    fn ensure_nest_creates_skill_file() {
+        let tmp = tempfile::tempdir().unwrap();
+        let root = tmp.path().join(".sprout");
+        ensure_nest_at(&root).unwrap();
+        let skill = root.join(".claude/skills/sprout-cli/SKILL.md");
+        assert!(skill.exists(), "SKILL.md should exist");
+        let content = fs::read_to_string(&skill).unwrap();
+        assert_eq!(content, SPROUT_CLI_SKILL_MD);
+    }
+
+    #[test]
+    fn ensure_nest_does_not_overwrite_skill_file() {
+        let tmp = tempfile::tempdir().unwrap();
+        let root = tmp.path().join(".sprout");
+        ensure_nest_at(&root).unwrap();
+        let skill = root.join(".claude/skills/sprout-cli/SKILL.md");
+        fs::write(&skill, "custom skill content").unwrap();
+        ensure_nest_at(&root).unwrap();
+        assert_eq!(
+            fs::read_to_string(&skill).unwrap(),
+            "custom skill content"
+        );
+    }
+
+    #[cfg(unix)]
+    #[test]
+    fn ensure_nest_skill_dir_has_700_permissions() {
+        use std::os::unix::fs::PermissionsExt;
+        let tmp = tempfile::tempdir().unwrap();
+        let root = tmp.path().join(".sprout");
+        ensure_nest_at(&root).unwrap();
+        let skill_dir = root.join(".claude/skills/sprout-cli");
+        let mode = fs::metadata(&skill_dir).unwrap().permissions().mode() & 0o777;
+        assert_eq!(mode, 0o700, "skill dir should be 700");
+    }
+
     #[cfg(unix)]
     #[test]
     fn ensure_nest_skips_permissions_on_symlinked_child() {
diff --git a/desktop/src-tauri/src/managed_agents/nest_skill.md b/desktop/src-tauri/src/managed_agents/nest_skill.md
new file mode 100644
index 0000000000..9499dbc73f
--- /dev/null
+++ b/desktop/src-tauri/src/managed_agents/nest_skill.md
@@ -0,0 +1,364 @@
+---
+name: sprout-cli
+description: >
+  Use the Sprout CLI (`sprout` command) to interact with a Sprout relay: send
+  and read messages in channels, post threaded replies, manage channels
+  (create, list, join, leave, archive), set canvas documents, add reactions,
+  open direct messages, query user profiles and presence, trigger and approve
+  workflows, search messages, post code diffs, and manage repositories.
+  Activate when the task involves messaging, channels, feeds, DMs, reactions,
+  workflows, or any Sprout relay operation via the `sprout` command.
+version: 1
+---
+
+# Sprout CLI Skill
+
+## Environment
+
+`SPROUT_PRIVATE_KEY` is pre-set by the harness. Never prompt for it, never read it, never echo it. All authentication is handled automatically via NIP-98 Schnorr signatures derived from this key.
+
+`SPROUT_RELAY_URL` defaults to `http://localhost:3000`. Override only if explicitly instructed.
+
+All output is JSON on stdout. Commands that return lists return JSON arrays; commands that return a single resource return a JSON object.
+
+Errors go to stderr as `{"error": "category", "message": "detail"}`. Exit codes: 0=ok, 1=input error, 2=network/relay error, 3=auth error, 4=other error. On non-zero exit, parse stderr for the error message before retrying or escalating.
+
+## Parameter Conventions
+
+- `--channel` accepts UUID format (e.g., `550e8400-e29b-41d4-a716-446655440000`)
+- `--event` accepts 64-character lowercase hex (e.g., `a3f2...`). Do not pass Bech32-encoded `note1...` identifiers — convert first if needed.
+- `--pubkey` accepts 64-character lowercase hex. Do not pass `npub...` identifiers.
+- `--content -` reads content from stdin, enabling pipe-friendly workflows.
+- Content max 65,536 bytes. Larger content will be rejected with exit code 1.
+- Diffs max 61,440 bytes; the CLI auto-truncates at a hunk boundary if the diff exceeds this limit.
+
+## Messaging
+
+Send a message to a channel:
+
+```bash
+sprout messages send --channel <UUID> --content "text"
+```
+
+Send a threaded reply:
+
+```bash
+sprout messages send --channel <UUID> --content "reply text" --reply-to <event-id>
+```
+
+Read recent messages (returns array, newest messages at the end when no `--since`):
+
+```bash
+sprout messages get --channel <UUID> --limit 20
+sprout messages get --channel <UUID> --limit 50 --since 1716000000
+sprout messages get --channel <UUID> --limit 50 --before 1716050000
+```
+
+Get a thread rooted at a specific event:
+
+```bash
+sprout messages thread --channel <UUID> --event <event-id>
+```
+
+Full-text search across all channels you can access:
+
+```bash
+sprout messages search --query "architecture decision"
+```
+
+Send a code diff with repository metadata (pipe the diff via stdin):
+
+```bash
+git diff HEAD~1 | sprout messages send-diff --channel <UUID> --diff - --repo https://github.com/org/repo --commit abc123def456
+```
+
+Edit a message you sent:
+
+```bash
+sprout messages edit --event <event-id> --content "updated text"
+```
+
+Delete a message:
+
+```bash
+sprout messages delete --event <event-id>
+```
+
+Vote on a forum post:
+
+```bash
+sprout messages vote --event <event-id> --direction up
+sprout messages vote --event <event-id> --direction down
+```
+
+## Channels
+
+List all visible channels:
+
+```bash
+sprout channels list
+```
+
+Returns `[{channel_id, name, description, created_at}]`.
+
+Filter channel lists:
+
+```bash
+sprout channels list --member              # only channels you've joined
+sprout channels list --visibility open     # open or private
+```
+
+Create a channel:
+
+```bash
+sprout channels create --name "eng-backend" --type stream --visibility open
+```
+
+Returns `{event_id, channel_id, accepted, message}`. Use `channel_id` for subsequent operations.
+
+Get channel details:
+
+```bash
+sprout channels get --channel <UUID>
+```
+
+Join or leave:
+
+```bash
+sprout channels join --channel <UUID>
+sprout channels leave --channel <UUID>
+```
+
+Set topic or purpose:
+
+```bash
+sprout channels topic --channel <UUID> --topic "Sprint 42 coordination"
+sprout channels purpose --channel <UUID> --purpose "Backend team daily sync"
+```
+
+List members:
+
+```bash
+sprout channels members --channel <UUID>
+```
+
+Returns `[{pubkey, role}]`.
+
+Admin operations (require `admin:channels` scope):
+
+```bash
+sprout channels archive --channel <UUID>
+sprout channels unarchive --channel <UUID>
+sprout channels delete --channel <UUID>
+```
+
+## Canvas
+
+Get the canvas document for a channel (returns the markdown content string directly, not JSON-wrapped):
+
+```bash
+sprout canvas get --channel <UUID>
+```
+
+Set the canvas (inline or via stdin):
+
+```bash
+sprout canvas set --channel <UUID> --content "# Project Brief\n\nObjectives..."
+echo "# Doc" | sprout canvas set --channel <UUID> --content -
+```
+
+## Reactions
+
+Add a reaction to any event (message, note, etc.):
+
+```bash
+sprout reactions add --event <hex-event-id> --emoji "👍"
+```
+
+Remove a reaction:
+
+```bash
+sprout reactions remove --event <hex-event-id> --emoji "👍"
+```
+
+Get all reactions on an event:
+
+```bash
+sprout reactions get --event <hex-event-id>
+```
+
+Returns `{"reactions": [{emoji, count, pubkeys}]}`.
+
+## DMs
+
+List existing DM conversations:
+
+```bash
+sprout dms list
+```
+
+Returns `[{dm_id, participants, created_at}]`.
+
+Open a new DM (creates a group DM conversation):
+
+```bash
+sprout dms open --pubkey <hex-pubkey>
+```
+
+Returns `{event_id, dm_id, accepted, message}`. Use `dm_id` as the `--channel` value for subsequent `messages` commands.
+
+Add a member to a DM group:
+
+```bash
+sprout dms add-member --channel <UUID> --pubkey <hex-pubkey>
+```
+
+## Users
+
+Get your own profile:
+
+```bash
+sprout users get
+```
+
+Returns a flat profile object: `{display_name, about, picture, pubkey, ...}`.
+
+Get a specific user's profile:
+
+```bash
+sprout users get --pubkey <hex-pubkey>
+```
+
+Batch lookup (up to 200 pubkeys):
+
+```bash
+sprout users get --pubkey <hex1> --pubkey <hex2> --pubkey <hex3>
+```
+
+Search by display name:
+
+```bash
+sprout users get --name "alice"
+```
+
+Update your profile:
+
+```bash
+sprout users set-profile --name "Alice" --avatar "https://example.com/avatar.png" --about "Backend engineer" --nip05 "alice@example.com"
+```
+
+Get presence for one or more users:
+
+```bash
+sprout users presence --pubkeys <hex1>,<hex2>
+```
+
+Returns `[{pubkey, status, updated_at}]`. Status values: `online`, `away`, `offline`.
+
+Set your own presence:
+
+```bash
+sprout users set-presence --status online
+sprout users set-presence --status away
+sprout users set-presence --status offline
+```
+
+## Workflows
+
+List workflows for a channel:
+
+```bash
+sprout workflows list --channel <UUID>
+```
+
+Returns `[{workflow_id, content, created_at}]`.
+
+Get a specific workflow definition:
+
+```bash
+sprout workflows get --workflow <UUID>
+```
+
+Create a workflow (YAML definition inline or via stdin):
+
+```bash
+sprout workflows create --channel <UUID> --yaml "name: review\nsteps: ..."
+cat workflow.yaml | sprout workflows create --channel <UUID> --yaml -
+```
+
+Returns `{event_id, workflow_id, accepted, message}`.
+
+Trigger a workflow:
+
+```bash
+sprout workflows trigger --workflow <UUID>
+```
+
+Approve or deny a pending workflow step:
+
+```bash
+sprout workflows approve --token <UUID>                               # approve (default)
+sprout workflows approve --token <UUID> --approved false --note "needs revision"
+```
+
+Get run history for a workflow:
+
+```bash
+sprout workflows runs --workflow <UUID>
+```
+
+Returns `[{event_id, kind, content, created_at, tags}]`.
+
+## Feed
+
+Get your activity feed (mentions, needs-action items, recent channel activity):
+
+```bash
+sprout feed get --limit 20
+```
+
+Returns events sorted newest-first.
+
+Poll for recent activity since a timestamp:
+
+```bash
+sprout feed get --since 1716000000 --limit 50
+```
+
+## Polling Pattern
+
+The Sprout relay has no push or webhook support. Poll with `--since` and sleep between iterations.
+
+When `--since` is set without `--before`, `messages get` returns results oldest-first (chronological order). `feed get` always returns newest-first regardless of `--since`.
+
+Recommended poll loop:
+
+1. Run `sprout messages get --channel <UUID> --limit 50` — note the maximum `created_at` value from results.
+2. Sleep 10–30 seconds.
+3. Run `sprout messages get --channel <UUID> --since <max_created_at> --limit 50`.
+4. Repeat from step 2, advancing `--since` each iteration.
+
+Use shorter intervals (10s) when latency matters; longer intervals (30s) for background monitoring. Avoid intervals under 5 seconds to prevent relay rate limiting.
+
+## Quick Reference
+
+| Command | Required Flags | Returns |
+|---------|---------------|---------|
+| `messages send` | `--channel`, `--content` | `{event_id, ...}` |
+| `messages get` | `--channel` | array of message objects |
+| `messages thread` | `--channel`, `--event` | array of thread events |
+| `messages search` | `--query` | array of matching messages |
+| `messages send-diff` | `--channel`, `--diff`, `--repo`, `--commit` | `{event_id, ...}` |
+| `channels list` | — | `[{channel_id, name, description, created_at}]` |
+| `channels create` | `--name`, `--type`, `--visibility` | `{event_id, channel_id, accepted, message}` |
+| `channels join` | `--channel` | `{event_id, accepted, message}` |
+| `channels members` | `--channel` | `[{pubkey, role}]` |
+| `canvas get` | `--channel` | markdown string (not JSON) |
+| `canvas set` | `--channel`, `--content` | `{event_id, accepted, message}` |
+| `reactions add` | `--event`, `--emoji` | `{event_id, accepted, message}` |
+| `reactions get` | `--event` | `{"reactions": [{emoji, count, pubkeys}]}` |
+| `dms list` | — | `[{dm_id, participants, created_at}]` |
+| `dms open` | `--pubkey` | `{event_id, dm_id, accepted, message}` |
+| `users get` | — | flat profile object |
+| `workflows list` | `--channel` | `[{workflow_id, content, created_at}]` |
+| `feed get` | — | array of feed events, newest-first |

From 1983a850d36c0696c5ebe05e6e6188ce656acb51 Mon Sep 17 00:00:00 2001
From: Will Pfleger <wpfleger@block.xyz>
Date: Mon, 18 May 2026 19:08:26 -0400
Subject: [PATCH 02/10] fix(nest): correct skill content inaccuracies + chmod
 intermediate dirs
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Four inaccuracies in nest_skill.md caught by comparing against CLI source:

1. canvas get returns a JSON array of kind:40100 events (markdown in
   `content`), not a plain markdown string. Agents parsing this as a
   raw string would get the Nostr event wrapper, not the content.

2. reactions get returns a raw JSON array of kind:7 events, not the
   aggregated {"reactions": [...]} shape. No aggregation happens in
   cmd_get_reactions.

3. Error stderr category field values were placeholder text ("input error",
   "network/relay error") instead of the actual enum values emitted by
   error.rs: user_error, relay_error, network_error, auth_error, key_error.

4. set-presence documents as working but cmd_set_presence has an explicit
   comment noting it always fails — kind:20001 is ephemeral and the relay
   rejects it over HTTP POST. Added a "this will fail" caveat.

Also fixes a permission gap in nest.rs: create_dir_all(".claude/skills/
sprout-cli") created the intermediate .claude/ and .claude/skills/ dirs
with umask-default permissions (typically 755), but only the final
sprout-cli/ dir was chmod'd to 700. Lock down all three dirs in the
skill path, matching how NEST_DIRS are handled. Test updated to assert
all three dirs get 700 on Unix.

Docstring for ensure_nest_at updated to mention SKILL.md.

Signed-off-by: Will Pfleger <wpfleger@block.xyz>
---
 desktop/src-tauri/src/managed_agents/nest.rs  | 39 ++++++++++++-------
 .../src/managed_agents/nest_skill.md          | 12 +++---
 2 files changed, 33 insertions(+), 18 deletions(-)

diff --git a/desktop/src-tauri/src/managed_agents/nest.rs b/desktop/src-tauri/src/managed_agents/nest.rs
index e2d7ab9173..a84ebeb7bc 100644
--- a/desktop/src-tauri/src/managed_agents/nest.rs
+++ b/desktop/src-tauri/src/managed_agents/nest.rs
@@ -47,10 +47,12 @@ pub fn ensure_nest() -> Result<(), String> {
 ///
 /// - Creates the root directory and all subdirectories.
 /// - Writes `AGENTS.md` only if it doesn't already exist.
-/// - Sets 700 permissions on the root and all subdirectories (Unix).
+/// - Writes `.claude/skills/sprout-cli/SKILL.md` only if it doesn't already exist.
+/// - Sets 700 permissions on the root, all subdirectories, and the skill
+///   directory tree (Unix).
 ///
 /// Idempotent: safe to call on every launch. Existing files are never
-/// overwritten — users can freely edit AGENTS.md and it will persist.
+/// overwritten — users can freely edit AGENTS.md or SKILL.md and they persist.
 ///
 /// Rejects symlinks at the root path to prevent redirect attacks.
 ///
@@ -143,14 +145,22 @@ pub fn ensure_nest_at(root: &Path) -> Result<(), String> {
                     .map_err(|e| format!("set permissions on {}: {e}", path.display()))?;
             }
         }
-        // Skill directory also gets 700 permissions.
-        let is_symlink = skill_dir
-            .symlink_metadata()
-            .map(|m| m.file_type().is_symlink())
-            .unwrap_or(false);
-        if !is_symlink {
-            fs::set_permissions(&skill_dir, perms.clone())
-                .map_err(|e| format!("set permissions on {}: {e}", skill_dir.display()))?;
+        // Skill directory and its intermediate parents inside root get 700.
+        // create_dir_all creates .claude/ and .claude/skills/ with umask
+        // defaults — lock them down the same way we do NEST_DIRS.
+        for dir in [
+            root.join(".claude"),
+            root.join(".claude/skills"),
+            skill_dir.clone(),
+        ] {
+            let is_symlink = dir
+                .symlink_metadata()
+                .map(|m| m.file_type().is_symlink())
+                .unwrap_or(false);
+            if !is_symlink {
+                fs::set_permissions(&dir, perms.clone())
+                    .map_err(|e| format!("set permissions on {}: {e}", dir.display()))?;
+            }
         }
     }
 
@@ -327,9 +337,12 @@ mod tests {
         let tmp = tempfile::tempdir().unwrap();
         let root = tmp.path().join(".sprout");
         ensure_nest_at(&root).unwrap();
-        let skill_dir = root.join(".claude/skills/sprout-cli");
-        let mode = fs::metadata(&skill_dir).unwrap().permissions().mode() & 0o777;
-        assert_eq!(mode, 0o700, "skill dir should be 700");
+        // All three dirs in the skill path should be locked down.
+        for dir in [".claude", ".claude/skills", ".claude/skills/sprout-cli"] {
+            let path = root.join(dir);
+            let mode = fs::metadata(&path).unwrap().permissions().mode() & 0o777;
+            assert_eq!(mode, 0o700, "{dir} should be 700");
+        }
     }
 
     #[cfg(unix)]
diff --git a/desktop/src-tauri/src/managed_agents/nest_skill.md b/desktop/src-tauri/src/managed_agents/nest_skill.md
index 9499dbc73f..a230c51da8 100644
--- a/desktop/src-tauri/src/managed_agents/nest_skill.md
+++ b/desktop/src-tauri/src/managed_agents/nest_skill.md
@@ -21,7 +21,7 @@ version: 1
 
 All output is JSON on stdout. Commands that return lists return JSON arrays; commands that return a single resource return a JSON object.
 
-Errors go to stderr as `{"error": "category", "message": "detail"}`. Exit codes: 0=ok, 1=input error, 2=network/relay error, 3=auth error, 4=other error. On non-zero exit, parse stderr for the error message before retrying or escalating.
+Errors go to stderr as `{"error": "<category>", "message": "<detail>"}`. Category values: `user_error` (exit 1), `relay_error` / `network_error` (exit 2), `auth_error` / `key_error` (exit 3), `error` (exit 4). On non-zero exit, parse stderr for the error message before retrying or escalating.
 
 ## Parameter Conventions
 
@@ -154,7 +154,7 @@ sprout channels delete --channel <UUID>
 
 ## Canvas
 
-Get the canvas document for a channel (returns the markdown content string directly, not JSON-wrapped):
+Get the canvas document for a channel (returns a JSON array of kind:40100 events; the canvas markdown is in the `content` field of the first element):
 
 ```bash
 sprout canvas get --channel <UUID>
@@ -187,7 +187,7 @@ Get all reactions on an event:
 sprout reactions get --event <hex-event-id>
 ```
 
-Returns `{"reactions": [{emoji, count, pubkeys}]}`.
+Returns a JSON array of raw kind:7 reaction events. Each event's `content` field is the emoji character, and the `pubkey` field identifies the reactor.
 
 ## DMs
 
@@ -263,6 +263,8 @@ sprout users set-presence --status away
 sprout users set-presence --status offline
 ```
 
+Note: `set-presence` sends an ephemeral kind:20001 event that requires a WebSocket connection. The current CLI uses HTTP POST, which the relay rejects for ephemeral events. This command will fail until WebSocket support is added.
+
 ## Workflows
 
 List workflows for a channel:
@@ -353,10 +355,10 @@ Use shorter intervals (10s) when latency matters; longer intervals (30s) for bac
 | `channels create` | `--name`, `--type`, `--visibility` | `{event_id, channel_id, accepted, message}` |
 | `channels join` | `--channel` | `{event_id, accepted, message}` |
 | `channels members` | `--channel` | `[{pubkey, role}]` |
-| `canvas get` | `--channel` | markdown string (not JSON) |
+| `canvas get` | `--channel` | JSON array of kind:40100 events (markdown in `content`) |
 | `canvas set` | `--channel`, `--content` | `{event_id, accepted, message}` |
 | `reactions add` | `--event`, `--emoji` | `{event_id, accepted, message}` |
-| `reactions get` | `--event` | `{"reactions": [{emoji, count, pubkeys}]}` |
+| `reactions get` | `--event` | JSON array of kind:7 reaction events |
 | `dms list` | — | `[{dm_id, participants, created_at}]` |
 | `dms open` | `--pubkey` | `{event_id, dm_id, accepted, message}` |
 | `users get` | — | flat profile object |

From 6d77694209566ef54560b2bcf0adb3a6b6f79a76 Mon Sep 17 00:00:00 2001
From: Will Pfleger <wpfleger@block.xyz>
Date: Mon, 18 May 2026 19:37:12 -0400
Subject: [PATCH 03/10] fix(nest): update skill content for CLI parity changes

Sync SKILL.md descriptions with post-review CLI output shapes: canvas
get returns markdown string or null (not event array), reactions get
returns grouped {emoji, count, pubkeys} (not raw events), users get
always returns array, workflows get documents normalized shape, workflows
runs notes empty-result limitation. Quick reference table updated.

Signed-off-by: Will Pfleger <wpfleger@block.xyz>
---
 .../src-tauri/src/managed_agents/nest_skill.md   | 16 +++++++++-------
 1 file changed, 9 insertions(+), 7 deletions(-)

diff --git a/desktop/src-tauri/src/managed_agents/nest_skill.md b/desktop/src-tauri/src/managed_agents/nest_skill.md
index a230c51da8..0c1425ea2f 100644
--- a/desktop/src-tauri/src/managed_agents/nest_skill.md
+++ b/desktop/src-tauri/src/managed_agents/nest_skill.md
@@ -154,7 +154,7 @@ sprout channels delete --channel <UUID>
 
 ## Canvas
 
-Get the canvas document for a channel (returns a JSON array of kind:40100 events; the canvas markdown is in the `content` field of the first element):
+Get the canvas document for a channel (returns the markdown content string directly, or `null` if no canvas is set):
 
 ```bash
 sprout canvas get --channel <UUID>
@@ -187,7 +187,7 @@ Get all reactions on an event:
 sprout reactions get --event <hex-event-id>
 ```
 
-Returns a JSON array of raw kind:7 reaction events. Each event's `content` field is the emoji character, and the `pubkey` field identifies the reactor.
+Returns `{"reactions": [{emoji, count, pubkeys}]}` — reactions grouped by emoji with reactor pubkeys. Empty content on a reaction is normalized to `"+"`.
 
 ## DMs
 
@@ -221,7 +221,7 @@ Get your own profile:
 sprout users get
 ```
 
-Returns a flat profile object: `{display_name, about, picture, pubkey, ...}`.
+Returns `[{display_name, about, picture, pubkey, ...}]` — always an array, even for a single profile lookup.
 
 Get a specific user's profile:
 
@@ -281,6 +281,8 @@ Get a specific workflow definition:
 sprout workflows get --workflow <UUID>
 ```
 
+Returns `{workflow_id, content, created_at, pubkey}`, or `null` if not found.
+
 Create a workflow (YAML definition inline or via stdin):
 
 ```bash
@@ -309,7 +311,7 @@ Get run history for a workflow:
 sprout workflows runs --workflow <UUID>
 ```
 
-Returns `[{event_id, kind, content, created_at, tags}]`.
+Returns `[{event_id, kind, content, created_at, tags}]`. Note: currently returns empty results because run history is stored in the database rather than as Nostr events.
 
 ## Feed
 
@@ -355,12 +357,12 @@ Use shorter intervals (10s) when latency matters; longer intervals (30s) for bac
 | `channels create` | `--name`, `--type`, `--visibility` | `{event_id, channel_id, accepted, message}` |
 | `channels join` | `--channel` | `{event_id, accepted, message}` |
 | `channels members` | `--channel` | `[{pubkey, role}]` |
-| `canvas get` | `--channel` | JSON array of kind:40100 events (markdown in `content`) |
+| `canvas get` | `--channel` | markdown string or `null` |
 | `canvas set` | `--channel`, `--content` | `{event_id, accepted, message}` |
 | `reactions add` | `--event`, `--emoji` | `{event_id, accepted, message}` |
-| `reactions get` | `--event` | JSON array of kind:7 reaction events |
+| `reactions get` | `--event` | `{"reactions": [{emoji, count, pubkeys}]}` |
 | `dms list` | — | `[{dm_id, participants, created_at}]` |
 | `dms open` | `--pubkey` | `{event_id, dm_id, accepted, message}` |
-| `users get` | — | flat profile object |
+| `users get` | — | `[{display_name, about, picture, pubkey, ...}]` |
 | `workflows list` | `--channel` | `[{workflow_id, content, created_at}]` |
 | `feed get` | — | array of feed events, newest-first |

From e3f1e11610d10d92bcc5d0994f94d331c7177eca Mon Sep 17 00:00:00 2001
From: Will Pfleger <wpfleger@block.xyz>
Date: Wed, 20 May 2026 14:18:14 -0400
Subject: [PATCH 04/10] style: fix rustfmt formatting after rebase

Reformat code after rebasing onto main (which includes PR #614's
nest.rs additions). No logic changes.

Signed-off-by: Will Pfleger <wpfleger@block.xyz>
---
 desktop/src-tauri/src/managed_agents/nest.rs | 8 ++------
 1 file changed, 2 insertions(+), 6 deletions(-)

diff --git a/desktop/src-tauri/src/managed_agents/nest.rs b/desktop/src-tauri/src/managed_agents/nest.rs
index a84ebeb7bc..25639b8d91 100644
--- a/desktop/src-tauri/src/managed_agents/nest.rs
+++ b/desktop/src-tauri/src/managed_agents/nest.rs
@@ -106,8 +106,7 @@ pub fn ensure_nest_at(root: &Path) -> Result<(), String> {
 
     // Write sprout-cli skill alongside AGENTS.md (same idempotent pattern).
     let skill_dir = root.join(".claude/skills/sprout-cli");
-    fs::create_dir_all(&skill_dir)
-        .map_err(|e| format!("create {}: {e}", skill_dir.display()))?;
+    fs::create_dir_all(&skill_dir).map_err(|e| format!("create {}: {e}", skill_dir.display()))?;
 
     let skill_md = root.join(".claude/skills/sprout-cli/SKILL.md");
     match fs::OpenOptions::new()
@@ -324,10 +323,7 @@ mod tests {
         let skill = root.join(".claude/skills/sprout-cli/SKILL.md");
         fs::write(&skill, "custom skill content").unwrap();
         ensure_nest_at(&root).unwrap();
-        assert_eq!(
-            fs::read_to_string(&skill).unwrap(),
-            "custom skill content"
-        );
+        assert_eq!(fs::read_to_string(&skill).unwrap(), "custom skill content");
     }
 
     #[cfg(unix)]

From a366514f11b3deba459f35d7505d8753efce36c6 Mon Sep 17 00:00:00 2001
From: Will Pfleger <wpfleger@block.xyz>
Date: Thu, 21 May 2026 12:12:34 -0400
Subject: [PATCH 05/10] docs(cli-skill): update skill file to match CLI parity
 changes

Brings the agent skill file to parity with all 62 CLI subcommands
across 13 command groups after PR #612's output normalization and bug
fixes. Adds 5 missing command groups (social, repos, upload, mem,
pack), 5 missing subcommands in existing groups (channels update/
add-member/remove-member, workflows update/delete), missing flags
(--file, --mention, --kinds, --depth-limit, --limit), fixes output
contract inaccuracies (channels get pubkey field, workflows list
pubkey, exit code 5, feed description), and restructures the Quick
Reference table by output pattern for scannability.

Signed-off-by: Will Pfleger <wpfleger@block.xyz>
---
 .../src/managed_agents/nest_skill.md          | 307 ++++++++++++++++--
 1 file changed, 281 insertions(+), 26 deletions(-)

diff --git a/desktop/src-tauri/src/managed_agents/nest_skill.md b/desktop/src-tauri/src/managed_agents/nest_skill.md
index 0c1425ea2f..30c21b3085 100644
--- a/desktop/src-tauri/src/managed_agents/nest_skill.md
+++ b/desktop/src-tauri/src/managed_agents/nest_skill.md
@@ -2,12 +2,12 @@
 name: sprout-cli
 description: >
   Use the Sprout CLI (`sprout` command) to interact with a Sprout relay: send
-  and read messages in channels, post threaded replies, manage channels
-  (create, list, join, leave, archive), set canvas documents, add reactions,
-  open direct messages, query user profiles and presence, trigger and approve
-  workflows, search messages, post code diffs, and manage repositories.
-  Activate when the task involves messaging, channels, feeds, DMs, reactions,
-  workflows, or any Sprout relay operation via the `sprout` command.
+  and read messages, manage channels, set canvas documents, add reactions,
+  open DMs, query user profiles, trigger workflows, search messages, post code
+  diffs, manage repositories, publish social notes, upload files, and manage
+  persistent agent memory. Activate when the task involves messaging, channels,
+  feeds, DMs, reactions, workflows, social notes, repos, uploads, agent memory,
+  or any Sprout relay operation via the `sprout` command.
 version: 1
 ---
 
@@ -19,9 +19,9 @@ version: 1
 
 `SPROUT_RELAY_URL` defaults to `http://localhost:3000`. Override only if explicitly instructed.
 
-All output is JSON on stdout. Commands that return lists return JSON arrays; commands that return a single resource return a JSON object.
+All output is JSON on stdout unless noted otherwise. Commands that return lists return JSON arrays; commands that return a single resource return a JSON object. Exceptions: `canvas get` returns a raw markdown string (not JSON); `mem get`/`mem hash` write raw values to stdout; `pack inspect` outputs human-readable text; `social` and `repos` commands return raw Nostr event JSON (includes `sig` and relay-specific fields — not the normalized format used by `messages`, `channels`, etc.).
 
-Errors go to stderr as `{"error": "<category>", "message": "<detail>"}`. Category values: `user_error` (exit 1), `relay_error` / `network_error` (exit 2), `auth_error` / `key_error` (exit 3), `error` (exit 4). On non-zero exit, parse stderr for the error message before retrying or escalating.
+Errors go to stderr as `{"error": "<category>", "message": "<detail>"}`. Category values: `user_error` (exit 1), `relay_error` / `network_error` (exit 2), `auth_error` / `key_error` (exit 3), `error` (exit 4), `conflict` (exit 5 — write conflict, relay rejected as superseded). On non-zero exit, parse stderr for the error message before retrying or escalating.
 
 ## Parameter Conventions
 
@@ -46,24 +46,50 @@ Send a threaded reply:
 sprout messages send --channel <UUID> --content "reply text" --reply-to <event-id>
 ```
 
-Read recent messages (returns array, newest messages at the end when no `--since`):
+Send a forum post (kind 45001) or forum comment (kind 45003):
+
+```bash
+sprout messages send --channel <UUID> --content "post title" --kind 45001
+sprout messages send --channel <UUID> --content "comment" --kind 45003 --reply-to <event-id>
+```
+
+`--kind` routing: omitted or `9` sends a stream message; `45001` sends a forum post; `45003` sends a forum comment (requires `--reply-to`). Other kinds are rejected.
+
+Attach files (uploads to Blossom, includes as imeta tags):
+
+```bash
+sprout messages send --channel <UUID> --content "see attached" --file /path/to/image.png
+```
+
+Explicitly mention users by pubkey (in addition to @name auto-resolution from message body):
+
+```bash
+sprout messages send --channel <UUID> --content "cc @alice" --mention <hex-pubkey>
+```
+
+`--file` and `--mention` are repeatable.
+
+Read recent messages (default kinds: `[9, 40002, 40008, 45001, 45003]`):
 
 ```bash
 sprout messages get --channel <UUID> --limit 20
 sprout messages get --channel <UUID> --limit 50 --since 1716000000
 sprout messages get --channel <UUID> --limit 50 --before 1716050000
+sprout messages get --channel <UUID> --kinds "1,1984"   # override default kinds
 ```
 
 Get a thread rooted at a specific event:
 
 ```bash
 sprout messages thread --channel <UUID> --event <event-id>
+sprout messages thread --channel <UUID> --event <event-id> --limit 100 --depth-limit 5
 ```
 
 Full-text search across all channels you can access:
 
 ```bash
 sprout messages search --query "architecture decision"
+sprout messages search --query "deploy" --limit 50
 ```
 
 Send a code diff with repository metadata (pipe the diff via stdin):
@@ -112,6 +138,7 @@ Create a channel:
 
 ```bash
 sprout channels create --name "eng-backend" --type stream --visibility open
+sprout channels create --name "rfcs" --type forum --visibility open --description "Architecture RFCs"
 ```
 
 Returns `{event_id, channel_id, accepted, message}`. Use `channel_id` for subsequent operations.
@@ -122,6 +149,14 @@ Get channel details:
 sprout channels get --channel <UUID>
 ```
 
+Returns `{channel_id, name, description, created_at, pubkey}`, or `null` if not found.
+
+Update channel metadata:
+
+```bash
+sprout channels update --channel <UUID> --name "new-name" --description "new description"
+```
+
 Join or leave:
 
 ```bash
@@ -142,7 +177,16 @@ List members:
 sprout channels members --channel <UUID>
 ```
 
-Returns `[{pubkey, role}]`.
+Returns `[{pubkey, role}]`. Roles: `owner`, `admin`, `member`, `guest`, `bot`.
+
+Manage members:
+
+```bash
+sprout channels add-member --channel <UUID> --pubkey <hex> --role member
+sprout channels remove-member --channel <UUID> --pubkey <hex>
+```
+
+`--role` is optional (defaults to `member`). Valid roles: `owner`, `admin`, `member`, `guest`, `bot`.
 
 Admin operations (require `admin:channels` scope):
 
@@ -154,7 +198,7 @@ sprout channels delete --channel <UUID>
 
 ## Canvas
 
-Get the canvas document for a channel (returns the markdown content string directly, or `null` if no canvas is set):
+Get the canvas document for a channel (returns the markdown content string directly, or `null` if no canvas is set — this is NOT a JSON envelope):
 
 ```bash
 sprout canvas get --channel <UUID>
@@ -195,6 +239,7 @@ List existing DM conversations:
 
 ```bash
 sprout dms list
+sprout dms list --limit 10
 ```
 
 Returns `[{dm_id, participants, created_at}]`.
@@ -203,6 +248,7 @@ Open a new DM (creates a group DM conversation):
 
 ```bash
 sprout dms open --pubkey <hex-pubkey>
+sprout dms open --pubkey <hex1> --pubkey <hex2>   # group DM (up to 8)
 ```
 
 Returns `{event_id, dm_id, accepted, message}`. Use `dm_id` as the `--channel` value for subsequent `messages` commands.
@@ -273,7 +319,7 @@ List workflows for a channel:
 sprout workflows list --channel <UUID>
 ```
 
-Returns `[{workflow_id, content, created_at}]`.
+Returns `[{workflow_id, content, created_at, pubkey}]`.
 
 Get a specific workflow definition:
 
@@ -292,6 +338,18 @@ cat workflow.yaml | sprout workflows create --channel <UUID> --yaml -
 
 Returns `{event_id, workflow_id, accepted, message}`.
 
+Update a workflow (requires both `--channel` and `--workflow`):
+
+```bash
+sprout workflows update --channel <UUID> --workflow <UUID> --yaml "name: updated\nsteps: ..."
+```
+
+Delete a workflow:
+
+```bash
+sprout workflows delete --workflow <UUID>
+```
+
 Trigger a workflow:
 
 ```bash
@@ -309,19 +367,20 @@ Get run history for a workflow:
 
 ```bash
 sprout workflows runs --workflow <UUID>
+sprout workflows runs --workflow <UUID> --limit 10
 ```
 
 Returns `[{event_id, kind, content, created_at, tags}]`. Note: currently returns empty results because run history is stored in the database rather than as Nostr events.
 
 ## Feed
 
-Get your activity feed (mentions, needs-action items, recent channel activity):
+Get your activity feed (mentions of your pubkey):
 
 ```bash
 sprout feed get --limit 20
 ```
 
-Returns events sorted newest-first.
+Returns events sorted newest-first (descending `created_at`). This is the only list command that sorts newest-first; all others sort oldest-first.
 
 Poll for recent activity since a timestamp:
 
@@ -329,6 +388,149 @@ Poll for recent activity since a timestamp:
 sprout feed get --since 1716000000 --limit 50
 ```
 
+## Social
+
+Nostr social protocol commands (NIP-01, NIP-02, NIP-51/NIP-65). These commands return **raw Nostr event JSON** including `sig` and all relay fields — not the normalized format used by `messages`, `channels`, etc.
+
+Publish a text note (kind:1):
+
+```bash
+sprout social publish --content "Hello world"
+sprout social publish --content "reply text" --reply-to <event-id>
+```
+
+Set your contact list (kind:3):
+
+```bash
+sprout social set-contacts --contacts '[{"pubkey":"<hex>","relay_url":"","petname":"alice"}]'
+```
+
+Get a single event by ID:
+
+```bash
+sprout social event --event <hex-event-id>
+```
+
+Get a user's recent notes:
+
+```bash
+sprout social notes --pubkey <hex> --limit 20
+sprout social notes --pubkey <hex> --limit 20 --before 1716050000
+```
+
+Get a user's contact list:
+
+```bash
+sprout social contacts --pubkey <hex>
+```
+
+Publish a NIP-51/NIP-65 social list (supported kinds: 10000, 10001, 10002, 10003, 30000, 30003):
+
+```bash
+sprout social set-list --kind 10002 --tags '[["r","wss://relay.example.com"]]'
+```
+
+Read a user's social lists by kind:
+
+```bash
+sprout social list --pubkey <hex> --kind 10002
+sprout social list --pubkey <hex> --kind 30000 --d-tag "friends"
+```
+
+## Repos
+
+Git repository announcements (NIP-34 kind:30617). Returns raw Nostr event JSON.
+
+Create a repository announcement:
+
+```bash
+sprout repos create --id "my-repo" --name "My Repo" --description "A project" --clone https://github.com/org/repo.git
+```
+
+`--clone`, `--web`, and `--nostr-relay` flags are optional; `--clone` is repeatable.
+
+Get a repository:
+
+```bash
+sprout repos get --id "my-repo"
+sprout repos get --id "my-repo" --owner <hex-pubkey>
+```
+
+List repositories:
+
+```bash
+sprout repos list
+sprout repos list --owner <hex-pubkey> --limit 10
+```
+
+## Upload
+
+Upload a file to the relay's Blossom store:
+
+```bash
+sprout upload file --file /path/to/image.png
+```
+
+Returns a pretty-printed (multi-line) JSON `BlobDescriptor`: `{url, sha256, size, type, uploaded}`. Optional fields: `dim`, `blurhash`, `thumb`, `duration`.
+
+## Mem (Agent Memory)
+
+Persistent agent memory (NIP-AE engrams). Progress messages go to stderr; data goes to stdout. Exit code 5 on write conflicts.
+
+List memory entries:
+
+```bash
+sprout mem ls             # tab-delimited: slug, created_at, event_id
+sprout mem ls --json      # JSON array: [{slug, event_id, created_at}]
+```
+
+Read a memory value (raw bytes to stdout, no trailing newline):
+
+```bash
+sprout mem get <slug>
+```
+
+Get the SHA-256 hash of a value (use as `--base-hash` for safe patching):
+
+```bash
+sprout mem hash <slug>
+```
+
+Set a memory value:
+
+```bash
+sprout mem set <slug> "value"
+echo "value" | sprout mem set <slug> -          # read from stdin
+sprout mem set <slug> - --allow-empty           # allow empty stdin
+```
+
+Patch a memory value with a unified diff (safer than `set` for concurrent access):
+
+```bash
+sprout mem hash <slug>                                          # get base hash first
+diff -u old.txt new.txt | sprout mem patch <slug> --base-hash <hex>
+sprout mem patch <slug> --patch-file changes.patch --base-hash <hex>
+sprout mem patch <slug> --no-base-hash                          # skip hash check (unsafe)
+sprout mem patch <slug> --base-hash <hex> --dry-run             # preview without writing
+```
+
+Delete (tombstone) a memory entry:
+
+```bash
+sprout mem rm <slug>
+```
+
+The `core` slug cannot be deleted.
+
+## Pack (Local-Only)
+
+Persona pack operations. No relay connection or `--private-key` needed.
+
+```bash
+sprout pack validate /path/to/pack    # validate pack directory structure
+sprout pack inspect /path/to/pack     # show pack metadata (human-readable text, not JSON)
+```
+
 ## Polling Pattern
 
 The Sprout relay has no push or webhook support. Poll with `--since` and sleep between iterations.
@@ -346,23 +548,76 @@ Use shorter intervals (10s) when latency matters; longer intervals (30s) for bac
 
 ## Quick Reference
 
+Most write commands return `{event_id, accepted, message}`. Exceptions noted below.
+
+**Read commands:**
+
 | Command | Required Flags | Returns |
 |---------|---------------|---------|
-| `messages send` | `--channel`, `--content` | `{event_id, ...}` |
-| `messages get` | `--channel` | array of message objects |
-| `messages thread` | `--channel`, `--event` | array of thread events |
-| `messages search` | `--query` | array of matching messages |
-| `messages send-diff` | `--channel`, `--diff`, `--repo`, `--commit` | `{event_id, ...}` |
+| `messages get` | `--channel` | `[{id, pubkey, kind, content, created_at, tags}]` |
+| `messages thread` | `--channel`, `--event` | `[{id, pubkey, kind, content, created_at, tags}]` |
+| `messages search` | `--query` | `[{id, pubkey, kind, content, created_at, tags}]` |
 | `channels list` | — | `[{channel_id, name, description, created_at}]` |
-| `channels create` | `--name`, `--type`, `--visibility` | `{event_id, channel_id, accepted, message}` |
-| `channels join` | `--channel` | `{event_id, accepted, message}` |
+| `channels get` | `--channel` | `{channel_id, name, description, created_at, pubkey}` or `null` |
 | `channels members` | `--channel` | `[{pubkey, role}]` |
 | `canvas get` | `--channel` | markdown string or `null` |
-| `canvas set` | `--channel`, `--content` | `{event_id, accepted, message}` |
-| `reactions add` | `--event`, `--emoji` | `{event_id, accepted, message}` |
 | `reactions get` | `--event` | `{"reactions": [{emoji, count, pubkeys}]}` |
 | `dms list` | — | `[{dm_id, participants, created_at}]` |
-| `dms open` | `--pubkey` | `{event_id, dm_id, accepted, message}` |
 | `users get` | — | `[{display_name, about, picture, pubkey, ...}]` |
-| `workflows list` | `--channel` | `[{workflow_id, content, created_at}]` |
-| `feed get` | — | array of feed events, newest-first |
+| `users presence` | `--pubkeys` | `[{pubkey, status, updated_at}]` |
+| `workflows list` | `--channel` | `[{workflow_id, content, created_at, pubkey}]` |
+| `workflows get` | `--workflow` | `{workflow_id, content, created_at, pubkey}` or `null` |
+| `workflows runs` | `--workflow` | `[{event_id, kind, content, created_at, tags}]` |
+| `feed get` | — | event array, newest-first |
+
+**Write commands** (all return `{event_id, accepted, message}` unless noted):
+
+| Command | Required Flags | Notes |
+|---------|---------------|-------|
+| `messages send` | `--channel`, `--content` | |
+| `messages send-diff` | `--channel`, `--diff`, `--repo`, `--commit` | |
+| `messages edit` | `--event`, `--content` | |
+| `messages delete` | `--event` | |
+| `messages vote` | `--event`, `--direction` | |
+| `channels create` | `--name`, `--type`, `--visibility` | adds `channel_id` |
+| `channels update/join/leave/topic/purpose` | `--channel` (+ value flag) | |
+| `channels add-member/remove-member` | `--channel`, `--pubkey` | |
+| `channels archive/unarchive/delete` | `--channel` | |
+| `canvas set` | `--channel`, `--content` | |
+| `reactions add/remove` | `--event`, `--emoji` | |
+| `dms open` | `--pubkey` | adds `dm_id` |
+| `dms add-member` | `--channel`, `--pubkey` | |
+| `users set-profile` | one of `--name`/`--avatar`/`--about`/`--nip05` | |
+| `workflows create` | `--channel`, `--yaml` | adds `workflow_id` |
+| `workflows update` | `--channel`, `--workflow`, `--yaml` | |
+| `workflows delete/trigger` | `--workflow` | |
+| `workflows approve` | `--token` | |
+| `social publish` | `--content` | |
+| `social set-contacts` | `--contacts` | |
+
+**Raw output commands** (return unprocessed Nostr event JSON including `sig`):
+
+| Command | Required Flags |
+|---------|---------------|
+| `social event` | `--event` |
+| `social notes` | `--pubkey` |
+| `social contacts` | `--pubkey` |
+| `social set-list` | `--kind`, `--tags` |
+| `social list` | `--pubkey`, `--kind` |
+| `repos create` | `--id` |
+| `repos get` | `--id` |
+| `repos list` | — |
+
+**Other output formats:**
+
+| Command | Required Flags | Returns |
+|---------|---------------|---------|
+| `upload file` | `--file` | `{url, sha256, size, type, uploaded}` (pretty-printed) |
+| `mem ls` | — | tab-delimited lines or `--json` array |
+| `mem get` | `<slug>` | raw value (stdout, no newline) |
+| `mem hash` | `<slug>` | SHA-256 hex string |
+| `mem set` | `<slug>`, `<value>` | stderr progress only |
+| `mem patch` | `<slug>`, `--base-hash` or `--no-base-hash` | stderr progress only |
+| `mem rm` | `<slug>` | stderr progress only |
+| `pack validate` | `<path>` | "Valid." or errors |
+| `pack inspect` | `<path>` | human-readable text |

From 4932cc73d272d3fc842156e85669a38a6a85fd0b Mon Sep 17 00:00:00 2001
From: Will Pfleger <wpfleger@block.xyz>
Date: Thu, 21 May 2026 18:43:32 -0400
Subject: [PATCH 06/10] docs(cli-skill): add --limit and --format compact to
 skill file
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Documents the new --limit flag on channels list and the global
--format json|compact flag added in PR #612. Compact mode reduces
read output: channels → {channel_id, name}, messages → {id, content,
created_at}, users → {pubkey, display_name}.

Signed-off-by: Will Pfleger <wpfleger@block.xyz>
---
 desktop/src-tauri/src/managed_agents/nest_skill.md | 14 ++++++++++++++
 1 file changed, 14 insertions(+)

diff --git a/desktop/src-tauri/src/managed_agents/nest_skill.md b/desktop/src-tauri/src/managed_agents/nest_skill.md
index 30c21b3085..26c2b31f26 100644
--- a/desktop/src-tauri/src/managed_agents/nest_skill.md
+++ b/desktop/src-tauri/src/managed_agents/nest_skill.md
@@ -32,6 +32,19 @@ Errors go to stderr as `{"error": "<category>", "message": "<detail>"}`. Categor
 - Content max 65,536 bytes. Larger content will be rejected with exit code 1.
 - Diffs max 61,440 bytes; the CLI auto-truncates at a hunk boundary if the diff exceeds this limit.
 
+## Output Format
+
+Use `--format compact` to reduce response size on read commands. This is a global flag.
+
+```bash
+sprout --format compact channels list --limit 50   # [{channel_id, name}]
+sprout --format compact messages get --channel <UUID>  # [{id, content, created_at}]
+sprout --format compact users get                  # [{pubkey, display_name}]
+sprout --format compact feed get                   # [{id, content, created_at}]
+```
+
+`--format json` (default) returns full fields. Write commands are unaffected by `--format`.
+
 ## Messaging
 
 Send a message to a channel:
@@ -123,6 +136,7 @@ List all visible channels:
 
 ```bash
 sprout channels list
+sprout channels list --limit 50            # limit results (default: 500)
 ```
 
 Returns `[{channel_id, name, description, created_at}]`.

From 933cf42f95f53e5fdc7aa5f39af2921d60a20b7f Mon Sep 17 00:00:00 2001
From: Will Pfleger <wpfleger@block.xyz>
Date: Thu, 21 May 2026 19:04:02 -0400
Subject: [PATCH 07/10] docs(cli-skill): fix 6 documentation gaps in skill file

Add missing send-diff optional flags, mark repos create --name as
optional, document --owner on all mem subcommands, add --allow-empty to
mem patch, remove dead --depth-limit from thread example, and document
social set-list --content.

Signed-off-by: Will Pfleger <wpfleger@block.xyz>
---
 desktop/src-tauri/src/managed_agents/nest_skill.md | 11 +++++++++--
 1 file changed, 9 insertions(+), 2 deletions(-)

diff --git a/desktop/src-tauri/src/managed_agents/nest_skill.md b/desktop/src-tauri/src/managed_agents/nest_skill.md
index 26c2b31f26..e3be6dfa3a 100644
--- a/desktop/src-tauri/src/managed_agents/nest_skill.md
+++ b/desktop/src-tauri/src/managed_agents/nest_skill.md
@@ -95,7 +95,7 @@ Get a thread rooted at a specific event:
 
 ```bash
 sprout messages thread --channel <UUID> --event <event-id>
-sprout messages thread --channel <UUID> --event <event-id> --limit 100 --depth-limit 5
+sprout messages thread --channel <UUID> --event <event-id> --limit 100
 ```
 
 Full-text search across all channels you can access:
@@ -111,6 +111,8 @@ Send a code diff with repository metadata (pipe the diff via stdin):
 git diff HEAD~1 | sprout messages send-diff --channel <UUID> --diff - --repo https://github.com/org/repo --commit abc123def456
 ```
 
+Additional optional flags: `--file <path>` (file path within repo), `--parent-commit <hash>`, `--source-branch <name>` / `--target-branch <name>` (must be paired), `--pr <number>`, `--lang <language>` (auto-inferred from `--file` extension), `--reply-to <event-id>`, `--description <text>`.
+
 Edit a message you sent:
 
 ```bash
@@ -444,6 +446,8 @@ Publish a NIP-51/NIP-65 social list (supported kinds: 10000, 10001, 10002, 10003
 sprout social set-list --kind 10002 --tags '[["r","wss://relay.example.com"]]'
 ```
 
+Optional `--content <string>` sets the list content (defaults to empty string).
+
 Read a user's social lists by kind:
 
 ```bash
@@ -461,7 +465,7 @@ Create a repository announcement:
 sprout repos create --id "my-repo" --name "My Repo" --description "A project" --clone https://github.com/org/repo.git
 ```
 
-`--clone`, `--web`, and `--nostr-relay` flags are optional; `--clone` is repeatable.
+`--name`, `--description`, `--clone`, `--web`, and `--nostr-relay` flags are optional; `--clone` and `--nostr-relay` are repeatable.
 
 Get a repository:
 
@@ -491,6 +495,8 @@ Returns a pretty-printed (multi-line) JSON `BlobDescriptor`: `{url, sha256, size
 
 Persistent agent memory (NIP-AE engrams). Progress messages go to stderr; data goes to stdout. Exit code 5 on write conflicts.
 
+All `mem` subcommands accept `--owner <hex-pubkey>` to query or write memories owned by a different pubkey (for multi-agent scenarios). If omitted, defaults to the owner from `SPROUT_AUTH_TAG`.
+
 List memory entries:
 
 ```bash
@@ -526,6 +532,7 @@ diff -u old.txt new.txt | sprout mem patch <slug> --base-hash <hex>
 sprout mem patch <slug> --patch-file changes.patch --base-hash <hex>
 sprout mem patch <slug> --no-base-hash                          # skip hash check (unsafe)
 sprout mem patch <slug> --base-hash <hex> --dry-run             # preview without writing
+sprout mem patch <slug> --base-hash <hex> --allow-empty         # allow empty result after patch
 ```
 
 Delete (tombstone) a memory entry:

From 990df78a496339599789fbf7588fb3b15885ce1f Mon Sep 17 00:00:00 2001
From: npub1rq35h4cfluqvg73mvmcqze6m69xqwuqdschdv8pp53prmnqaj6rsrkj0c2
 <18234bd709ff00c47a3b66f001675bd14c07700d862ed61c21a4423dcc1d9687@sprout-oss.stage.blox.sqprod.co>
Date: Thu, 21 May 2026 19:30:01 -0400
Subject: [PATCH 08/10] Condense sprout-cli skill to ~90 lines of navigation
 context

The skill duplicated the CLI's own per-subcommand --help (usage, flags,
examples). Rewrite to teach what --help can't: discovery, conventions
(UUID vs hex, stdin -, --format), output asymmetries, error/exit
contract, polling. Point to 'sprout <cmd> --help' for the rest.
630 deletions, 78 insertions.

Signed-off-by: Will Pfleger <wpfleger@block.xyz>
---
 .../src/managed_agents/nest_skill.md          | 708 ++----------------
 1 file changed, 78 insertions(+), 630 deletions(-)

diff --git a/desktop/src-tauri/src/managed_agents/nest_skill.md b/desktop/src-tauri/src/managed_agents/nest_skill.md
index e3be6dfa3a..97d6925c6a 100644
--- a/desktop/src-tauri/src/managed_agents/nest_skill.md
+++ b/desktop/src-tauri/src/managed_agents/nest_skill.md
@@ -1,644 +1,92 @@
 ---
 name: sprout-cli
 description: >
-  Use the Sprout CLI (`sprout` command) to interact with a Sprout relay: send
-  and read messages, manage channels, set canvas documents, add reactions,
-  open DMs, query user profiles, trigger workflows, search messages, post code
-  diffs, manage repositories, publish social notes, upload files, and manage
-  persistent agent memory. Activate when the task involves messaging, channels,
-  feeds, DMs, reactions, workflows, social notes, repos, uploads, agent memory,
-  or any Sprout relay operation via the `sprout` command.
+  Use the Sprout CLI (`sprout` command) to interact with a Sprout relay:
+  messages, channels, canvas, reactions, DMs, users, workflows, feed, social
+  notes, repos, file uploads, and persistent agent memory. Activate for any
+  task involving a Sprout relay via the `sprout` command.
 version: 1
 ---
 
-# Sprout CLI Skill
+# Sprout CLI
 
-## Environment
-
-`SPROUT_PRIVATE_KEY` is pre-set by the harness. Never prompt for it, never read it, never echo it. All authentication is handled automatically via NIP-98 Schnorr signatures derived from this key.
-
-`SPROUT_RELAY_URL` defaults to `http://localhost:3000`. Override only if explicitly instructed.
-
-All output is JSON on stdout unless noted otherwise. Commands that return lists return JSON arrays; commands that return a single resource return a JSON object. Exceptions: `canvas get` returns a raw markdown string (not JSON); `mem get`/`mem hash` write raw values to stdout; `pack inspect` outputs human-readable text; `social` and `repos` commands return raw Nostr event JSON (includes `sig` and relay-specific fields — not the normalized format used by `messages`, `channels`, etc.).
-
-Errors go to stderr as `{"error": "<category>", "message": "<detail>"}`. Category values: `user_error` (exit 1), `relay_error` / `network_error` (exit 2), `auth_error` / `key_error` (exit 3), `error` (exit 4), `conflict` (exit 5 — write conflict, relay rejected as superseded). On non-zero exit, parse stderr for the error message before retrying or escalating.
-
-## Parameter Conventions
-
-- `--channel` accepts UUID format (e.g., `550e8400-e29b-41d4-a716-446655440000`)
-- `--event` accepts 64-character lowercase hex (e.g., `a3f2...`). Do not pass Bech32-encoded `note1...` identifiers — convert first if needed.
-- `--pubkey` accepts 64-character lowercase hex. Do not pass `npub...` identifiers.
-- `--content -` reads content from stdin, enabling pipe-friendly workflows.
-- Content max 65,536 bytes. Larger content will be rejected with exit code 1.
-- Diffs max 61,440 bytes; the CLI auto-truncates at a hunk boundary if the diff exceeds this limit.
-
-## Output Format
-
-Use `--format compact` to reduce response size on read commands. This is a global flag.
-
-```bash
-sprout --format compact channels list --limit 50   # [{channel_id, name}]
-sprout --format compact messages get --channel <UUID>  # [{id, content, created_at}]
-sprout --format compact users get                  # [{pubkey, display_name}]
-sprout --format compact feed get                   # [{id, content, created_at}]
-```
-
-`--format json` (default) returns full fields. Write commands are unaffected by `--format`.
-
-## Messaging
-
-Send a message to a channel:
-
-```bash
-sprout messages send --channel <UUID> --content "text"
-```
-
-Send a threaded reply:
-
-```bash
-sprout messages send --channel <UUID> --content "reply text" --reply-to <event-id>
-```
-
-Send a forum post (kind 45001) or forum comment (kind 45003):
-
-```bash
-sprout messages send --channel <UUID> --content "post title" --kind 45001
-sprout messages send --channel <UUID> --content "comment" --kind 45003 --reply-to <event-id>
-```
-
-`--kind` routing: omitted or `9` sends a stream message; `45001` sends a forum post; `45003` sends a forum comment (requires `--reply-to`). Other kinds are rejected.
-
-Attach files (uploads to Blossom, includes as imeta tags):
-
-```bash
-sprout messages send --channel <UUID> --content "see attached" --file /path/to/image.png
-```
-
-Explicitly mention users by pubkey (in addition to @name auto-resolution from message body):
-
-```bash
-sprout messages send --channel <UUID> --content "cc @alice" --mention <hex-pubkey>
-```
-
-`--file` and `--mention` are repeatable.
-
-Read recent messages (default kinds: `[9, 40002, 40008, 45001, 45003]`):
-
-```bash
-sprout messages get --channel <UUID> --limit 20
-sprout messages get --channel <UUID> --limit 50 --since 1716000000
-sprout messages get --channel <UUID> --limit 50 --before 1716050000
-sprout messages get --channel <UUID> --kinds "1,1984"   # override default kinds
-```
-
-Get a thread rooted at a specific event:
-
-```bash
-sprout messages thread --channel <UUID> --event <event-id>
-sprout messages thread --channel <UUID> --event <event-id> --limit 100
-```
-
-Full-text search across all channels you can access:
-
-```bash
-sprout messages search --query "architecture decision"
-sprout messages search --query "deploy" --limit 50
-```
-
-Send a code diff with repository metadata (pipe the diff via stdin):
-
-```bash
-git diff HEAD~1 | sprout messages send-diff --channel <UUID> --diff - --repo https://github.com/org/repo --commit abc123def456
-```
-
-Additional optional flags: `--file <path>` (file path within repo), `--parent-commit <hash>`, `--source-branch <name>` / `--target-branch <name>` (must be paired), `--pr <number>`, `--lang <language>` (auto-inferred from `--file` extension), `--reply-to <event-id>`, `--description <text>`.
-
-Edit a message you sent:
-
-```bash
-sprout messages edit --event <event-id> --content "updated text"
-```
-
-Delete a message:
-
-```bash
-sprout messages delete --event <event-id>
-```
-
-Vote on a forum post:
-
-```bash
-sprout messages vote --event <event-id> --direction up
-sprout messages vote --event <event-id> --direction down
-```
-
-## Channels
-
-List all visible channels:
-
-```bash
-sprout channels list
-sprout channels list --limit 50            # limit results (default: 500)
-```
-
-Returns `[{channel_id, name, description, created_at}]`.
-
-Filter channel lists:
-
-```bash
-sprout channels list --member              # only channels you've joined
-sprout channels list --visibility open     # open or private
-```
-
-Create a channel:
-
-```bash
-sprout channels create --name "eng-backend" --type stream --visibility open
-sprout channels create --name "rfcs" --type forum --visibility open --description "Architecture RFCs"
-```
-
-Returns `{event_id, channel_id, accepted, message}`. Use `channel_id` for subsequent operations.
-
-Get channel details:
+`sprout` talks to a Sprout relay. The CLI is self-documenting — **lean on
+`--help`** for command details and only rely on this skill for the conventions
+and gotchas that `--help` doesn't surface.
 
-```bash
-sprout channels get --channel <UUID>
-```
-
-Returns `{channel_id, name, description, created_at, pubkey}`, or `null` if not found.
-
-Update channel metadata:
-
-```bash
-sprout channels update --channel <UUID> --name "new-name" --description "new description"
-```
-
-Join or leave:
-
-```bash
-sprout channels join --channel <UUID>
-sprout channels leave --channel <UUID>
-```
-
-Set topic or purpose:
-
-```bash
-sprout channels topic --channel <UUID> --topic "Sprint 42 coordination"
-sprout channels purpose --channel <UUID> --purpose "Backend team daily sync"
-```
-
-List members:
-
-```bash
-sprout channels members --channel <UUID>
-```
-
-Returns `[{pubkey, role}]`. Roles: `owner`, `admin`, `member`, `guest`, `bot`.
-
-Manage members:
+## Discovering commands
 
 ```bash
-sprout channels add-member --channel <UUID> --pubkey <hex> --role member
-sprout channels remove-member --channel <UUID> --pubkey <hex>
+sprout --help                 # 13 command groups + global flags + exit codes
+sprout messages --help        # subcommands of a group
+sprout messages send --help   # flags + worked examples for one subcommand
 ```
 
-`--role` is optional (defaults to `member`). Valid roles: `owner`, `admin`, `member`, `guest`, `bot`.
-
-Admin operations (require `admin:channels` scope):
-
-```bash
-sprout channels archive --channel <UUID>
-sprout channels unarchive --channel <UUID>
-sprout channels delete --channel <UUID>
-```
-
-## Canvas
-
-Get the canvas document for a channel (returns the markdown content string directly, or `null` if no canvas is set — this is NOT a JSON envelope):
-
-```bash
-sprout canvas get --channel <UUID>
-```
-
-Set the canvas (inline or via stdin):
-
-```bash
-sprout canvas set --channel <UUID> --content "# Project Brief\n\nObjectives..."
-echo "# Doc" | sprout canvas set --channel <UUID> --content -
-```
-
-## Reactions
-
-Add a reaction to any event (message, note, etc.):
-
-```bash
-sprout reactions add --event <hex-event-id> --emoji "👍"
-```
-
-Remove a reaction:
-
-```bash
-sprout reactions remove --event <hex-event-id> --emoji "👍"
-```
-
-Get all reactions on an event:
-
-```bash
-sprout reactions get --event <hex-event-id>
-```
-
-Returns `{"reactions": [{emoji, count, pubkeys}]}` — reactions grouped by emoji with reactor pubkeys. Empty content on a reaction is normalized to `"+"`.
-
-## DMs
-
-List existing DM conversations:
-
-```bash
-sprout dms list
-sprout dms list --limit 10
-```
-
-Returns `[{dm_id, participants, created_at}]`.
-
-Open a new DM (creates a group DM conversation):
-
-```bash
-sprout dms open --pubkey <hex-pubkey>
-sprout dms open --pubkey <hex1> --pubkey <hex2>   # group DM (up to 8)
-```
-
-Returns `{event_id, dm_id, accepted, message}`. Use `dm_id` as the `--channel` value for subsequent `messages` commands.
-
-Add a member to a DM group:
-
-```bash
-sprout dms add-member --channel <UUID> --pubkey <hex-pubkey>
-```
-
-## Users
-
-Get your own profile:
-
-```bash
-sprout users get
-```
-
-Returns `[{display_name, about, picture, pubkey, ...}]` — always an array, even for a single profile lookup.
-
-Get a specific user's profile:
-
-```bash
-sprout users get --pubkey <hex-pubkey>
-```
-
-Batch lookup (up to 200 pubkeys):
-
-```bash
-sprout users get --pubkey <hex1> --pubkey <hex2> --pubkey <hex3>
-```
-
-Search by display name:
-
-```bash
-sprout users get --name "alice"
-```
+Every leaf command's help lists its required flags and shows real examples.
+When unsure of a flag, check `--help` rather than guessing. The 13 groups:
+`messages channels canvas reactions dms users workflows feed social repos
+upload mem pack`.
 
-Update your profile:
-
-```bash
-sprout users set-profile --name "Alice" --avatar "https://example.com/avatar.png" --about "Backend engineer" --nip05 "alice@example.com"
-```
-
-Get presence for one or more users:
-
-```bash
-sprout users presence --pubkeys <hex1>,<hex2>
-```
-
-Returns `[{pubkey, status, updated_at}]`. Status values: `online`, `away`, `offline`.
-
-Set your own presence:
-
-```bash
-sprout users set-presence --status online
-sprout users set-presence --status away
-sprout users set-presence --status offline
-```
-
-Note: `set-presence` sends an ephemeral kind:20001 event that requires a WebSocket connection. The current CLI uses HTTP POST, which the relay rejects for ephemeral events. This command will fail until WebSocket support is added.
-
-## Workflows
-
-List workflows for a channel:
-
-```bash
-sprout workflows list --channel <UUID>
-```
-
-Returns `[{workflow_id, content, created_at, pubkey}]`.
-
-Get a specific workflow definition:
-
-```bash
-sprout workflows get --workflow <UUID>
-```
-
-Returns `{workflow_id, content, created_at, pubkey}`, or `null` if not found.
-
-Create a workflow (YAML definition inline or via stdin):
-
-```bash
-sprout workflows create --channel <UUID> --yaml "name: review\nsteps: ..."
-cat workflow.yaml | sprout workflows create --channel <UUID> --yaml -
-```
-
-Returns `{event_id, workflow_id, accepted, message}`.
-
-Update a workflow (requires both `--channel` and `--workflow`):
-
-```bash
-sprout workflows update --channel <UUID> --workflow <UUID> --yaml "name: updated\nsteps: ..."
-```
-
-Delete a workflow:
-
-```bash
-sprout workflows delete --workflow <UUID>
-```
-
-Trigger a workflow:
-
-```bash
-sprout workflows trigger --workflow <UUID>
-```
-
-Approve or deny a pending workflow step:
-
-```bash
-sprout workflows approve --token <UUID>                               # approve (default)
-sprout workflows approve --token <UUID> --approved false --note "needs revision"
-```
-
-Get run history for a workflow:
-
-```bash
-sprout workflows runs --workflow <UUID>
-sprout workflows runs --workflow <UUID> --limit 10
-```
-
-Returns `[{event_id, kind, content, created_at, tags}]`. Note: currently returns empty results because run history is stored in the database rather than as Nostr events.
-
-## Feed
-
-Get your activity feed (mentions of your pubkey):
-
-```bash
-sprout feed get --limit 20
-```
-
-Returns events sorted newest-first (descending `created_at`). This is the only list command that sorts newest-first; all others sort oldest-first.
-
-Poll for recent activity since a timestamp:
-
-```bash
-sprout feed get --since 1716000000 --limit 50
-```
-
-## Social
-
-Nostr social protocol commands (NIP-01, NIP-02, NIP-51/NIP-65). These commands return **raw Nostr event JSON** including `sig` and all relay fields — not the normalized format used by `messages`, `channels`, etc.
-
-Publish a text note (kind:1):
-
-```bash
-sprout social publish --content "Hello world"
-sprout social publish --content "reply text" --reply-to <event-id>
-```
-
-Set your contact list (kind:3):
-
-```bash
-sprout social set-contacts --contacts '[{"pubkey":"<hex>","relay_url":"","petname":"alice"}]'
-```
-
-Get a single event by ID:
-
-```bash
-sprout social event --event <hex-event-id>
-```
-
-Get a user's recent notes:
-
-```bash
-sprout social notes --pubkey <hex> --limit 20
-sprout social notes --pubkey <hex> --limit 20 --before 1716050000
-```
-
-Get a user's contact list:
-
-```bash
-sprout social contacts --pubkey <hex>
-```
-
-Publish a NIP-51/NIP-65 social list (supported kinds: 10000, 10001, 10002, 10003, 30000, 30003):
-
-```bash
-sprout social set-list --kind 10002 --tags '[["r","wss://relay.example.com"]]'
-```
-
-Optional `--content <string>` sets the list content (defaults to empty string).
-
-Read a user's social lists by kind:
-
-```bash
-sprout social list --pubkey <hex> --kind 10002
-sprout social list --pubkey <hex> --kind 30000 --d-tag "friends"
-```
-
-## Repos
-
-Git repository announcements (NIP-34 kind:30617). Returns raw Nostr event JSON.
-
-Create a repository announcement:
-
-```bash
-sprout repos create --id "my-repo" --name "My Repo" --description "A project" --clone https://github.com/org/repo.git
-```
-
-`--name`, `--description`, `--clone`, `--web`, and `--nostr-relay` flags are optional; `--clone` and `--nostr-relay` are repeatable.
-
-Get a repository:
-
-```bash
-sprout repos get --id "my-repo"
-sprout repos get --id "my-repo" --owner <hex-pubkey>
-```
-
-List repositories:
-
-```bash
-sprout repos list
-sprout repos list --owner <hex-pubkey> --limit 10
-```
-
-## Upload
-
-Upload a file to the relay's Blossom store:
-
-```bash
-sprout upload file --file /path/to/image.png
-```
-
-Returns a pretty-printed (multi-line) JSON `BlobDescriptor`: `{url, sha256, size, type, uploaded}`. Optional fields: `dim`, `blurhash`, `thumb`, `duration`.
-
-## Mem (Agent Memory)
-
-Persistent agent memory (NIP-AE engrams). Progress messages go to stderr; data goes to stdout. Exit code 5 on write conflicts.
-
-All `mem` subcommands accept `--owner <hex-pubkey>` to query or write memories owned by a different pubkey (for multi-agent scenarios). If omitted, defaults to the owner from `SPROUT_AUTH_TAG`.
-
-List memory entries:
-
-```bash
-sprout mem ls             # tab-delimited: slug, created_at, event_id
-sprout mem ls --json      # JSON array: [{slug, event_id, created_at}]
-```
-
-Read a memory value (raw bytes to stdout, no trailing newline):
-
-```bash
-sprout mem get <slug>
-```
-
-Get the SHA-256 hash of a value (use as `--base-hash` for safe patching):
-
-```bash
-sprout mem hash <slug>
-```
-
-Set a memory value:
-
-```bash
-sprout mem set <slug> "value"
-echo "value" | sprout mem set <slug> -          # read from stdin
-sprout mem set <slug> - --allow-empty           # allow empty stdin
-```
-
-Patch a memory value with a unified diff (safer than `set` for concurrent access):
-
-```bash
-sprout mem hash <slug>                                          # get base hash first
-diff -u old.txt new.txt | sprout mem patch <slug> --base-hash <hex>
-sprout mem patch <slug> --patch-file changes.patch --base-hash <hex>
-sprout mem patch <slug> --no-base-hash                          # skip hash check (unsafe)
-sprout mem patch <slug> --base-hash <hex> --dry-run             # preview without writing
-sprout mem patch <slug> --base-hash <hex> --allow-empty         # allow empty result after patch
-```
-
-Delete (tombstone) a memory entry:
-
-```bash
-sprout mem rm <slug>
-```
-
-The `core` slug cannot be deleted.
-
-## Pack (Local-Only)
-
-Persona pack operations. No relay connection or `--private-key` needed.
-
-```bash
-sprout pack validate /path/to/pack    # validate pack directory structure
-sprout pack inspect /path/to/pack     # show pack metadata (human-readable text, not JSON)
-```
-
-## Polling Pattern
-
-The Sprout relay has no push or webhook support. Poll with `--since` and sleep between iterations.
-
-When `--since` is set without `--before`, `messages get` returns results oldest-first (chronological order). `feed get` always returns newest-first regardless of `--since`.
-
-Recommended poll loop:
-
-1. Run `sprout messages get --channel <UUID> --limit 50` — note the maximum `created_at` value from results.
-2. Sleep 10–30 seconds.
-3. Run `sprout messages get --channel <UUID> --since <max_created_at> --limit 50`.
-4. Repeat from step 2, advancing `--since` each iteration.
-
-Use shorter intervals (10s) when latency matters; longer intervals (30s) for background monitoring. Avoid intervals under 5 seconds to prevent relay rate limiting.
-
-## Quick Reference
-
-Most write commands return `{event_id, accepted, message}`. Exceptions noted below.
-
-**Read commands:**
-
-| Command | Required Flags | Returns |
-|---------|---------------|---------|
-| `messages get` | `--channel` | `[{id, pubkey, kind, content, created_at, tags}]` |
-| `messages thread` | `--channel`, `--event` | `[{id, pubkey, kind, content, created_at, tags}]` |
-| `messages search` | `--query` | `[{id, pubkey, kind, content, created_at, tags}]` |
-| `channels list` | — | `[{channel_id, name, description, created_at}]` |
-| `channels get` | `--channel` | `{channel_id, name, description, created_at, pubkey}` or `null` |
-| `channels members` | `--channel` | `[{pubkey, role}]` |
-| `canvas get` | `--channel` | markdown string or `null` |
-| `reactions get` | `--event` | `{"reactions": [{emoji, count, pubkeys}]}` |
-| `dms list` | — | `[{dm_id, participants, created_at}]` |
-| `users get` | — | `[{display_name, about, picture, pubkey, ...}]` |
-| `users presence` | `--pubkeys` | `[{pubkey, status, updated_at}]` |
-| `workflows list` | `--channel` | `[{workflow_id, content, created_at, pubkey}]` |
-| `workflows get` | `--workflow` | `{workflow_id, content, created_at, pubkey}` or `null` |
-| `workflows runs` | `--workflow` | `[{event_id, kind, content, created_at, tags}]` |
-| `feed get` | — | event array, newest-first |
-
-**Write commands** (all return `{event_id, accepted, message}` unless noted):
-
-| Command | Required Flags | Notes |
-|---------|---------------|-------|
-| `messages send` | `--channel`, `--content` | |
-| `messages send-diff` | `--channel`, `--diff`, `--repo`, `--commit` | |
-| `messages edit` | `--event`, `--content` | |
-| `messages delete` | `--event` | |
-| `messages vote` | `--event`, `--direction` | |
-| `channels create` | `--name`, `--type`, `--visibility` | adds `channel_id` |
-| `channels update/join/leave/topic/purpose` | `--channel` (+ value flag) | |
-| `channels add-member/remove-member` | `--channel`, `--pubkey` | |
-| `channels archive/unarchive/delete` | `--channel` | |
-| `canvas set` | `--channel`, `--content` | |
-| `reactions add/remove` | `--event`, `--emoji` | |
-| `dms open` | `--pubkey` | adds `dm_id` |
-| `dms add-member` | `--channel`, `--pubkey` | |
-| `users set-profile` | one of `--name`/`--avatar`/`--about`/`--nip05` | |
-| `workflows create` | `--channel`, `--yaml` | adds `workflow_id` |
-| `workflows update` | `--channel`, `--workflow`, `--yaml` | |
-| `workflows delete/trigger` | `--workflow` | |
-| `workflows approve` | `--token` | |
-| `social publish` | `--content` | |
-| `social set-contacts` | `--contacts` | |
-
-**Raw output commands** (return unprocessed Nostr event JSON including `sig`):
-
-| Command | Required Flags |
-|---------|---------------|
-| `social event` | `--event` |
-| `social notes` | `--pubkey` |
-| `social contacts` | `--pubkey` |
-| `social set-list` | `--kind`, `--tags` |
-| `social list` | `--pubkey`, `--kind` |
-| `repos create` | `--id` |
-| `repos get` | `--id` |
-| `repos list` | — |
-
-**Other output formats:**
+## Environment
 
-| Command | Required Flags | Returns |
-|---------|---------------|---------|
-| `upload file` | `--file` | `{url, sha256, size, type, uploaded}` (pretty-printed) |
-| `mem ls` | — | tab-delimited lines or `--json` array |
-| `mem get` | `<slug>` | raw value (stdout, no newline) |
-| `mem hash` | `<slug>` | SHA-256 hex string |
-| `mem set` | `<slug>`, `<value>` | stderr progress only |
-| `mem patch` | `<slug>`, `--base-hash` or `--no-base-hash` | stderr progress only |
-| `mem rm` | `<slug>` | stderr progress only |
-| `pack validate` | `<path>` | "Valid." or errors |
-| `pack inspect` | `<path>` | human-readable text |
+`SPROUT_PRIVATE_KEY` and `SPROUT_AUTH_TAG` are pre-set by the harness; auth is
+automatic (NIP-98). Never prompt for, read, or echo the key. `SPROUT_RELAY_URL`
+defaults to `http://localhost:3000` — override only if told to. `pack` is local
+and needs no relay.
+
+## Parameter conventions
+
+- `--channel` / `--workflow` / `--token`: UUID (`550e8400-...`).
+- `--event` / `--pubkey` / `--mention`: **64-char lowercase hex**. Convert
+  `note1...` / `npub...` Bech32 first — they are rejected.
+- `--content -`, `--diff -`, `--yaml -`: read from stdin (pipe-friendly).
+- Content max 65,536 bytes; diffs max 61,440 (auto-truncated at a hunk boundary).
+- IDs flow forward: `channels create` → `channel_id`, `dms open` → `dm_id`
+  (use as `--channel`), `workflows create` → `workflow_id`.
+
+## Output contract
+
+Default is JSON on stdout — arrays for lists, objects for single resources,
+`null` when not found. Most **writes** return `{event_id, accepted, message}`.
+`--format compact` trims read results to essential fields (global flag).
+
+Asymmetries worth knowing (the rest is plain JSON):
+- `canvas get` → raw markdown string, not JSON.
+- `social *` and `repos *` → **raw Nostr event JSON** (includes `sig`, relay
+  fields) — not the normalized shape used elsewhere.
+- `mem get`/`mem hash` → raw value/hex to stdout, no trailing newline.
+- `mem set/patch/rm` → progress on **stderr**, nothing on stdout.
+- `upload file` → pretty-printed `BlobDescriptor` `{url, sha256, size, ...}`.
+- `pack inspect` → human-readable text.
+
+## Errors & exit codes
+
+Errors are `{"error": "<category>", "message": "<detail>"}` on **stderr**.
+Exit: `0` ok · `1` bad input · `2` relay/network · `3` auth · `4` other ·
+`5` write conflict (superseded). On non-zero exit, read stderr before retrying.
+For `mem`, a `5` means someone else wrote first — re-fetch and retry.
+
+## Reading & polling
+
+The relay has **no push/webhooks** — poll. List commands return oldest-first,
+**except `feed get`** (mentions of you), which is newest-first. Pattern:
+
+1. `sprout messages get --channel <UUID> --limit 50` — note max `created_at`.
+2. Sleep 10–30s (never under 5s — rate limits).
+3. `sprout messages get --channel <UUID> --since <max_created_at> --limit 50`.
+4. Repeat, advancing `--since` each pass.
+
+`messages get --before <ts>` pages backward; `messages thread --event <id>`
+fetches a reply tree; `messages search --query` searches across channels.
+
+## Common gotchas
+
+- Reply/thread with `--reply-to <event-id>` (not `--parent`).
+- `messages send --kind`: omit/`9` = stream, `45001` = forum post,
+  `45003` = forum comment (needs `--reply-to`); others rejected.
+- `users get` always returns an **array**, even for one profile.
+- `users set-presence` currently fails (needs WebSocket; CLI uses HTTP).
+- `mem patch` is safer than `mem set` under concurrency: `mem hash <slug>`
+  first, pass `--base-hash <hex>`. The `core` slug can't be deleted.
+- Multi-line content with `$`, backticks, or `*`: pipe a quoted heredoc
+  (`<<'EOF'`) into `--content -` so the shell doesn't expand it.

From ea9757e4ee4c6b73af4949e1274328f7ee3176c6 Mon Sep 17 00:00:00 2001
From: npub1rq35h4cfluqvg73mvmcqze6m69xqwuqdschdv8pp53prmnqaj6rsrkj0c2
 <18234bd709ff00c47a3b66f001675bd14c07700d862ed61c21a4423dcc1d9687@sprout-oss.stage.blox.sqprod.co>
Date: Thu, 21 May 2026 19:36:44 -0400
Subject: [PATCH 09/10] Fix sprout-cli skill: correct claims to match CLI
 source
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Source audit (crates/sprout-cli/) caught five wrong claims inherited
from or worsened by the original file:
- No --format flag exists; removed all references.
- Reads are NOT normalized — every read prints the raw relay /query
  response (raw Nostr events with sig). Reframed accordingly.
- canvas get returns kind:40100 event JSON, not a markdown string.
- Default get kinds are [9, 40002], not the 5-kind list.
- --kind on send is dead code (#[allow(dead_code)]); send is always
  kind:9. Removed the forum-routing claim.
Also: feed ordering is relay-determined, not a CLI guarantee.
Every retained claim verified against source + live binary.

Signed-off-by: Will Pfleger <wpfleger@block.xyz>
---
 .../src/managed_agents/nest_skill.md          | 69 +++++++++++--------
 1 file changed, 39 insertions(+), 30 deletions(-)

diff --git a/desktop/src-tauri/src/managed_agents/nest_skill.md b/desktop/src-tauri/src/managed_agents/nest_skill.md
index 97d6925c6a..48d92cb150 100644
--- a/desktop/src-tauri/src/managed_agents/nest_skill.md
+++ b/desktop/src-tauri/src/managed_agents/nest_skill.md
@@ -11,8 +11,8 @@ version: 1
 # Sprout CLI
 
 `sprout` talks to a Sprout relay. The CLI is self-documenting — **lean on
-`--help`** for command details and only rely on this skill for the conventions
-and gotchas that `--help` doesn't surface.
+`--help`** for command details and rely on this skill for the conventions and
+gotchas `--help` doesn't surface.
 
 ## Discovering commands
 
@@ -23,16 +23,15 @@ sprout messages send --help   # flags + worked examples for one subcommand
 ```
 
 Every leaf command's help lists its required flags and shows real examples.
-When unsure of a flag, check `--help` rather than guessing. The 13 groups:
-`messages channels canvas reactions dms users workflows feed social repos
-upload mem pack`.
+Check `--help` rather than guessing flags. The 13 groups: `messages channels
+canvas reactions dms users workflows feed social repos upload mem pack`.
 
 ## Environment
 
 `SPROUT_PRIVATE_KEY` and `SPROUT_AUTH_TAG` are pre-set by the harness; auth is
 automatic (NIP-98). Never prompt for, read, or echo the key. `SPROUT_RELAY_URL`
-defaults to `http://localhost:3000` — override only if told to. `pack` is local
-and needs no relay.
+defaults to `http://localhost:3000` (ws/wss URLs are normalized to http/https).
+Override only if told to. `pack` is local and needs no relay.
 
 ## Parameter conventions
 
@@ -44,49 +43,59 @@ and needs no relay.
 - IDs flow forward: `channels create` → `channel_id`, `dms open` → `dm_id`
   (use as `--channel`), `workflows create` → `workflow_id`.
 
-## Output contract
+## Output: reads are raw Nostr events
 
-Default is JSON on stdout — arrays for lists, objects for single resources,
-`null` when not found. Most **writes** return `{event_id, accepted, message}`.
-`--format compact` trims read results to essential fields (global flag).
+**Read commands print the relay's `/query` response verbatim** — a JSON array
+of raw Nostr events, each `{id, pubkey, created_at, kind, tags, content, sig}`.
+This holds for `messages`, `channels`, `users`, `feed`, `canvas`, `social`,
+`repos` — all of them. There is **no `--format` flag** and no normalization;
+parse the fields you need (e.g. `canvas get` returns kind:40100 events with the
+document in `content`, not a bare markdown string). Empty results are `[]`.
 
-Asymmetries worth knowing (the rest is plain JSON):
-- `canvas get` → raw markdown string, not JSON.
-- `social *` and `repos *` → **raw Nostr event JSON** (includes `sig`, relay
-  fields) — not the normalized shape used elsewhere.
-- `mem get`/`mem hash` → raw value/hex to stdout, no trailing newline.
-- `mem set/patch/rm` → progress on **stderr**, nothing on stdout.
+Non-query output:
+- **Writes** → `{event_id, accepted, message}` (the relay's submit response).
 - `upload file` → pretty-printed `BlobDescriptor` `{url, sha256, size, ...}`.
-- `pack inspect` → human-readable text.
+- `mem get` → raw value to stdout, **no trailing newline**; `mem hash` → sha256
+  hex (with newline); `mem set/patch/rm` → progress on **stderr**, not stdout.
+- `mem ls` → tab-separated `slug<TAB>created_at<TAB>event_id` (or `--json`).
+- `pack validate/inspect` → human-readable text on stdout.
 
 ## Errors & exit codes
 
 Errors are `{"error": "<category>", "message": "<detail>"}` on **stderr**.
-Exit: `0` ok · `1` bad input · `2` relay/network · `3` auth · `4` other ·
-`5` write conflict (superseded). On non-zero exit, read stderr before retrying.
-For `mem`, a `5` means someone else wrote first — re-fetch and retry.
+Exit: `0` ok · `1` bad input / not-found · `2` relay/network · `3` auth
+(incl. relay 401/403) · `4` other · `5` write conflict (NIP-33 superseded).
+On non-zero exit, read stderr before retrying. For `mem`, a `5` means someone
+else wrote first — re-fetch and retry.
 
 ## Reading & polling
 
-The relay has **no push/webhooks** — poll. List commands return oldest-first,
-**except `feed get`** (mentions of you), which is newest-first. Pattern:
+The relay has **no push/webhooks** — poll. `messages get` defaults to kinds
+`[9, 40002]` (override with `--kinds "9,1984"`), `--limit` 50 (max 200).
+`--since <ts>` returns events after a time, `--before <ts>` pages backward
+(maps to the relay `until` filter). Ordering follows the relay, not the CLI —
+don't assume it; sort by `created_at` yourself if order matters. Poll loop:
 
 1. `sprout messages get --channel <UUID> --limit 50` — note max `created_at`.
 2. Sleep 10–30s (never under 5s — rate limits).
 3. `sprout messages get --channel <UUID> --since <max_created_at> --limit 50`.
 4. Repeat, advancing `--since` each pass.
 
-`messages get --before <ts>` pages backward; `messages thread --event <id>`
-fetches a reply tree; `messages search --query` searches across channels.
+`messages thread --event <id>` fetches events e-tagging the root; `messages
+search --query` does a relay full-text search.
 
 ## Common gotchas
 
 - Reply/thread with `--reply-to <event-id>` (not `--parent`).
-- `messages send --kind`: omit/`9` = stream, `45001` = forum post,
-  `45003` = forum comment (needs `--reply-to`); others rejected.
-- `users get` always returns an **array**, even for one profile.
-- `users set-presence` currently fails (needs WebSocket; CLI uses HTTP).
+- `messages send` always posts a kind:9 message. The `--kind` flag is parsed
+  but **not yet wired up** — forum posts (45001/45003) aren't routable via the
+  CLI today.
+- `users get` always returns an **array**, even for one profile. `--name` is a
+  case-insensitive substring search.
+- `users set-presence` currently **fails**: kind:20001 is ephemeral and the
+  relay only accepts it over WebSocket; the CLI posts over HTTP.
 - `mem patch` is safer than `mem set` under concurrency: `mem hash <slug>`
-  first, pass `--base-hash <hex>`. The `core` slug can't be deleted.
+  first, pass `--base-hash <hex>`. `core` can't be deleted — overwrite it with
+  `mem set core ''` instead.
 - Multi-line content with `$`, backticks, or `*`: pipe a quoted heredoc
   (`<<'EOF'`) into `--content -` so the shell doesn't expand it.

From 604939d048bd64f0b94435db138d506855b330f5 Mon Sep 17 00:00:00 2001
From: Will Pfleger <wpfleger@block.xyz>
Date: Thu, 21 May 2026 19:40:02 -0400
Subject: [PATCH 10/10] refactor(cli-skill): rewrite skill file for token
 efficiency
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

650 → 100 lines (85% reduction). Agents can discover flags via
`sprout --help` — the skill now documents only what --help can't:
output contracts, compact format projections, behavioral gotchas,
mem patch workflow, and polling pattern.

Signed-off-by: Will Pfleger <wpfleger@block.xyz>
---
 .../src/managed_agents/nest_skill.md          | 173 +++++++++---------
 1 file changed, 86 insertions(+), 87 deletions(-)

diff --git a/desktop/src-tauri/src/managed_agents/nest_skill.md b/desktop/src-tauri/src/managed_agents/nest_skill.md
index 48d92cb150..5f7b6f03bb 100644
--- a/desktop/src-tauri/src/managed_agents/nest_skill.md
+++ b/desktop/src-tauri/src/managed_agents/nest_skill.md
@@ -1,101 +1,100 @@
 ---
 name: sprout-cli
 description: >
-  Use the Sprout CLI (`sprout` command) to interact with a Sprout relay:
-  messages, channels, canvas, reactions, DMs, users, workflows, feed, social
-  notes, repos, file uploads, and persistent agent memory. Activate for any
-  task involving a Sprout relay via the `sprout` command.
+  Sprout CLI for relay operations: messaging, channels, DMs, users, workflows,
+  feed, reactions, canvas, social, repos, uploads, and agent memory.
 version: 1
 ---
 
-# Sprout CLI
+# Sprout CLI Skill
 
-`sprout` talks to a Sprout relay. The CLI is self-documenting — **lean on
-`--help`** for command details and rely on this skill for the conventions and
-gotchas `--help` doesn't surface.
+## Environment
+
+`SPROUT_PRIVATE_KEY` is pre-set by the harness. Never prompt for it, never read it, never echo it.
+
+`SPROUT_RELAY_URL` defaults to `http://localhost:3000`. Override only if explicitly instructed.
+
+Run `sprout --help` and `sprout <command> <subcommand> --help` to discover all flags, arguments, and usage. This skill documents only what `--help` cannot tell you.
+
+## Output Contracts
+
+Output varies by command group — `--help` shows flags but not response shapes.
+
+**Read commands** (messages, channels, users, feed, workflows): normalized JSON arrays with `sig` stripped. Fields: `{id, pubkey, kind, content, created_at, tags}` for events; command-specific shapes for channels (`{channel_id, name, description, created_at}`), users (kind:0 profile JSON with `pubkey` injected), workflows (`{workflow_id, content, created_at, pubkey}`).
 
-## Discovering commands
+**Write commands**: all return `{event_id, accepted, message}`. Create commands add the generated entity ID: `channels create` → `channel_id`, `dms open` → `dm_id`, `workflows create` → `workflow_id`.
+
+**Exceptions to the above patterns:**
+
+| Command | Output |
+|---------|--------|
+| `canvas get` | raw markdown string or `null` — NOT a JSON envelope |
+| `social *`, `repos *` | raw Nostr event JSON INCLUDING `sig` — different contract than read commands above |
+| `upload file` | pretty-printed multi-line `BlobDescriptor`: `{url, sha256, size, type, uploaded}` |
+| `mem get` | raw bytes to stdout, no trailing newline |
+| `mem hash` | SHA-256 hex string |
+| `mem set/patch/rm` | nothing to stdout; progress to stderr |
+| `mem ls` | tab-delimited (`slug\tcreated_at\tevent_id`) by default; `--json` for JSON array |
+| `reactions get` | `{"reactions": [{emoji, count, pubkeys}]}` — aggregated, not raw events |
+| `pack validate/inspect` | human-readable text, not JSON |
+
+**Errors** go to stderr as `{"error": "<category>", "message": "<detail>"}`. Exit codes: 0 = success, 1 = input/not-found, 2 = relay/network, 3 = auth, 4 = other, 5 = write conflict (value superseded).
+
+## Compact Format
+
+`--format compact` is a global flag — position it before the subcommand:
 
 ```bash
-sprout --help                 # 13 command groups + global flags + exit codes
-sprout messages --help        # subcommands of a group
-sprout messages send --help   # flags + worked examples for one subcommand
+sprout --format compact channels list          # [{channel_id, name}]
+sprout --format compact messages get --channel <UUID>  # [{id, content, created_at}]
+sprout --format compact users get              # [{pubkey, display_name}]
+sprout --format compact feed get               # [{id, content, created_at}]
 ```
 
-Every leaf command's help lists its required flags and shows real examples.
-Check `--help` rather than guessing flags. The 13 groups: `messages channels
-canvas reactions dms users workflows feed social repos upload mem pack`.
+Write commands are unaffected. `--format json` (default) returns full fields.
 
-## Environment
+## Gotchas
+
+1. **`feed get` sorts newest-first** — every other list command sorts oldest-first. Don't assume consistent sort order.
+2. **`users set-presence` is broken** — sends ephemeral kind:20001 via HTTP POST; relay rejects ephemeral kinds over HTTP. Will fail until WebSocket support is added.
+3. **`workflow runs` always returns `[]`** — run history lives in the relay's database, not as Nostr events.
+4. **`dms open` returns `dm_id`** — use this value as `--channel` for subsequent `messages send/get` commands on that DM.
+5. **Content max 65,536 bytes** (exit 1 if exceeded). Diffs auto-truncate at 61,440 bytes at a hunk boundary.
+6. **`users get` always returns an array** — even for a single pubkey lookup. Never expect a bare object.
+7. **All `mem` subcommands accept `--owner <hex-pubkey>`** — for querying or writing memories owned by a different pubkey in multi-agent scenarios. Defaults to the owner from `SPROUT_AUTH_TAG`.
+8. **`mem rm` cannot delete `core`** — use `mem set core ''` instead.
+
+## Forum Posts
+
+`messages send --kind` routes to different event builders:
+
+- Omitted or `9` → stream message (default)
+- `45001` → forum post (thread root)
+- `45003` → forum comment (requires `--reply-to <event-id>`)
+
+Other kind values are rejected. Use `messages vote --event <id> --direction up|down` to vote on forum posts.
+
+## Mem Patch Workflow
+
+For safe concurrent writes, use hash-based conflict detection:
+
+```bash
+HASH=$(sprout mem hash <slug>)                                    # 1. get current SHA-256
+# ... build unified diff ...
+sprout mem patch <slug> --base-hash "$HASH" --patch-file diff.patch  # 2. apply with check
+```
+
+Exit code 5 if the value changed since the hash was read (another agent wrote first). Retry by re-reading, re-diffing, and re-patching.
+
+Flags: `--dry-run` to preview without writing, `--no-base-hash` to skip conflict detection (unsafe), `--allow-empty` to permit empty result after patch.
+
+## Polling Pattern
+
+The relay has no push or webhook support. Poll with a `--since` cursor:
+
+1. `sprout messages get --channel <UUID> --limit 50` — note the maximum `created_at` from results
+2. Sleep 10-30 seconds
+3. `sprout messages get --channel <UUID> --since <max_created_at> --limit 50`
+4. Repeat, advancing `--since` each iteration
 
-`SPROUT_PRIVATE_KEY` and `SPROUT_AUTH_TAG` are pre-set by the harness; auth is
-automatic (NIP-98). Never prompt for, read, or echo the key. `SPROUT_RELAY_URL`
-defaults to `http://localhost:3000` (ws/wss URLs are normalized to http/https).
-Override only if told to. `pack` is local and needs no relay.
-
-## Parameter conventions
-
-- `--channel` / `--workflow` / `--token`: UUID (`550e8400-...`).
-- `--event` / `--pubkey` / `--mention`: **64-char lowercase hex**. Convert
-  `note1...` / `npub...` Bech32 first — they are rejected.
-- `--content -`, `--diff -`, `--yaml -`: read from stdin (pipe-friendly).
-- Content max 65,536 bytes; diffs max 61,440 (auto-truncated at a hunk boundary).
-- IDs flow forward: `channels create` → `channel_id`, `dms open` → `dm_id`
-  (use as `--channel`), `workflows create` → `workflow_id`.
-
-## Output: reads are raw Nostr events
-
-**Read commands print the relay's `/query` response verbatim** — a JSON array
-of raw Nostr events, each `{id, pubkey, created_at, kind, tags, content, sig}`.
-This holds for `messages`, `channels`, `users`, `feed`, `canvas`, `social`,
-`repos` — all of them. There is **no `--format` flag** and no normalization;
-parse the fields you need (e.g. `canvas get` returns kind:40100 events with the
-document in `content`, not a bare markdown string). Empty results are `[]`.
-
-Non-query output:
-- **Writes** → `{event_id, accepted, message}` (the relay's submit response).
-- `upload file` → pretty-printed `BlobDescriptor` `{url, sha256, size, ...}`.
-- `mem get` → raw value to stdout, **no trailing newline**; `mem hash` → sha256
-  hex (with newline); `mem set/patch/rm` → progress on **stderr**, not stdout.
-- `mem ls` → tab-separated `slug<TAB>created_at<TAB>event_id` (or `--json`).
-- `pack validate/inspect` → human-readable text on stdout.
-
-## Errors & exit codes
-
-Errors are `{"error": "<category>", "message": "<detail>"}` on **stderr**.
-Exit: `0` ok · `1` bad input / not-found · `2` relay/network · `3` auth
-(incl. relay 401/403) · `4` other · `5` write conflict (NIP-33 superseded).
-On non-zero exit, read stderr before retrying. For `mem`, a `5` means someone
-else wrote first — re-fetch and retry.
-
-## Reading & polling
-
-The relay has **no push/webhooks** — poll. `messages get` defaults to kinds
-`[9, 40002]` (override with `--kinds "9,1984"`), `--limit` 50 (max 200).
-`--since <ts>` returns events after a time, `--before <ts>` pages backward
-(maps to the relay `until` filter). Ordering follows the relay, not the CLI —
-don't assume it; sort by `created_at` yourself if order matters. Poll loop:
-
-1. `sprout messages get --channel <UUID> --limit 50` — note max `created_at`.
-2. Sleep 10–30s (never under 5s — rate limits).
-3. `sprout messages get --channel <UUID> --since <max_created_at> --limit 50`.
-4. Repeat, advancing `--since` each pass.
-
-`messages thread --event <id>` fetches events e-tagging the root; `messages
-search --query` does a relay full-text search.
-
-## Common gotchas
-
-- Reply/thread with `--reply-to <event-id>` (not `--parent`).
-- `messages send` always posts a kind:9 message. The `--kind` flag is parsed
-  but **not yet wired up** — forum posts (45001/45003) aren't routable via the
-  CLI today.
-- `users get` always returns an **array**, even for one profile. `--name` is a
-  case-insensitive substring search.
-- `users set-presence` currently **fails**: kind:20001 is ephemeral and the
-  relay only accepts it over WebSocket; the CLI posts over HTTP.
-- `mem patch` is safer than `mem set` under concurrency: `mem hash <slug>`
-  first, pass `--base-hash <hex>`. `core` can't be deleted — overwrite it with
-  `mem set core ''` instead.
-- Multi-line content with `$`, backticks, or `*`: pipe a quoted heredoc
-  (`<<'EOF'`) into `--content -` so the shell doesn't expand it.
+Minimum interval: 5 seconds (relay rate limiting). Use 10s for low-latency, 30s for background monitoring. `feed get` always returns newest-first regardless of `--since`.
