From ec30a45ed5085e0ae045de2c4f08b411a36ce0a9 Mon Sep 17 00:00:00 2001 From: Will Pfleger Date: Thu, 14 May 2026 14:53:03 -0400 Subject: [PATCH 01/10] feat: two-layer prompt architecture for managed agents MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Agents running in Sprout have no platform awareness — they don't know about channels, MCP tools, @mention syntax, or the other agents in the workspace. This adds a base layer prompt (compiled into sprout-acp via include_str!) that gives every agent reliable Sprout context on every turn, regardless of persona or runtime. Base layer: platform identity, MCP tool reference, communication patterns, workspace layout, startup recovery. Persona layer: unchanged role-specific content (Solo/Kit/Scout behavioral protocols, worktree discipline, quality bar). Also adds default model (claude-sonnet-4-20250514) to all three built-in personas so users don't need to pick at agent creation, and fixes a merge_personas() bug where .is_some() checks would cause infinite reset loops once built-ins carry non-None model values. --- crates/sprout-acp/src/base_prompt.md | 35 +++++ crates/sprout-acp/src/lib.rs | 5 + crates/sprout-acp/src/pool.rs | 2 + crates/sprout-acp/src/queue.rs | 99 +++++++++---- crates/sprout-persona/PERSONA_PACK_SPEC.md | 133 ++++++++++++++---- .../src-tauri/src/managed_agents/personas.rs | 12 +- 6 files changed, 230 insertions(+), 56 deletions(-) create mode 100644 crates/sprout-acp/src/base_prompt.md diff --git a/crates/sprout-acp/src/base_prompt.md b/crates/sprout-acp/src/base_prompt.md new file mode 100644 index 00000000..852d3755 --- /dev/null +++ b/crates/sprout-acp/src/base_prompt.md @@ -0,0 +1,35 @@ +You are operating inside the Sprout platform — a Nostr-based messaging platform for human-agent collaboration. The sprout-acp harness bridges channel events to your session. + +## MCP Tools + +- `get_messages(channel_id, limit=50)` — fetch recent history (max 200 per call) +- `get_messages(channel_id, since=)` — fetch messages since timestamp; returns oldest-first when `since` is set without `before` +- `get_thread(channel_id, event_id)` — fetch a full thread by root event ID +- `get_feed()` — personalized feed of mentions and needs-action items across all channels +- `send_message(channel_id, content)` — post a new message to a channel +- `send_message(channel_id, content, parent_event_id)` — reply within an existing thread +- `search(q="your query")` — cross-channel full-text search + +## Communication Patterns + +- Address agents and humans with `@name` in message content. +- Use `parent_event_id` when responding to a thread; post a new message for new topics. +- There are no push notifications — poll for new messages using `since=`. + +## Startup Recovery + +On startup or after a gap: call `get_feed()` first to surface pending mentions and action items, then call `get_messages` on your assigned channels to catch up, then check the workspace `AGENTS.md` for team context. + +## Workspace Layout + +Persistent workspace at `$AGENT_CWD/` with the following directories: + +- `RESEARCH/` — findings and reference material +- `PLANS/` — project and task plans +- `GUIDES/` — how-to documentation +- `WORK_LOGS/` — timestamped activity logs +- `OUTBOX/` — drafts pending review or send +- `REPOS/` — checked-out source repositories +- `.scratch/` — ephemeral working files + +Knowledge files use `ALL_CAPS_WITH_UNDERSCORES.md` naming and YAML frontmatter. `AGENTS.md` in the working directory lists active agents and their assigned roles. diff --git a/crates/sprout-acp/src/lib.rs b/crates/sprout-acp/src/lib.rs index e6e173be..d9fc7f8f 100644 --- a/crates/sprout-acp/src/lib.rs +++ b/crates/sprout-acp/src/lib.rs @@ -1074,6 +1074,11 @@ async fn tokio_main() -> Result<()> { max_turn_duration: Duration::from_secs(config.max_turn_duration_secs), dedup_mode: config.dedup_mode, system_prompt: config.system_prompt.clone(), + base_prompt: if std::env::var("SPROUT_ACP_BASE_PROMPT_DISABLED").is_ok() { + None + } else { + Some(include_str!("base_prompt.md").to_string()) + }, heartbeat_prompt: config.heartbeat_prompt.clone(), cwd: std::env::current_dir() .unwrap_or_else(|_| std::path::PathBuf::from("/")) diff --git a/crates/sprout-acp/src/pool.rs b/crates/sprout-acp/src/pool.rs index b489f394..fdab36fb 100644 --- a/crates/sprout-acp/src/pool.rs +++ b/crates/sprout-acp/src/pool.rs @@ -187,6 +187,7 @@ pub struct PromptContext { pub dedup_mode: DedupMode, pub system_prompt: Option, pub heartbeat_prompt: Option, + pub base_prompt: Option, pub cwd: String, /// REST client for pre-prompt context fetches (thread/DM history). pub rest_client: RestClient, @@ -873,6 +874,7 @@ pub async fn run_prompt_task( crate::queue::format_prompt( b, + ctx.base_prompt.as_deref(), ctx.system_prompt.as_deref(), channel_info.as_ref(), conversation_context.as_ref(), diff --git a/crates/sprout-acp/src/queue.rs b/crates/sprout-acp/src/queue.rs index a4077fce..b459e5c9 100644 --- a/crates/sprout-acp/src/queue.rs +++ b/crates/sprout-acp/src/queue.rs @@ -945,12 +945,14 @@ fn format_conversation_context( /// Format a [`FlushBatch`] into a prompt string for the agent. /// /// Produces a stable prompt with these sections (in order): -/// 1. `[System]` — system prompt (if configured) -/// 2. `[Context]` — scope, channel name, structural hints +/// 0. `[Base]\n{base_prompt}` — platform orientation (if configured) +/// 1. `[System]\n{system_prompt}` — if system prompt is set +/// 2. `[Context]` — scope, channel name, and contextual hints for the agent /// 3. `[Thread Context]` or `[Conversation Context]` — if fetched /// 4. `[Event]` / `[Sprout events]` — the triggering event(s) pub fn format_prompt( batch: &FlushBatch, + base_prompt: Option<&str>, system_prompt: Option<&str>, channel_info: Option<&PromptChannelInfo>, conversation_context: Option<&ConversationContext>, @@ -972,7 +974,12 @@ pub fn format_prompt( .map(|ci| ci.channel_type == "dm") .unwrap_or(false); - let mut sections: Vec = Vec::with_capacity(4); + let mut sections: Vec = Vec::with_capacity(5); + + // 0. Base prompt (platform-level, always first). + if let Some(bp) = base_prompt { + sections.push(format!("[Base]\n{bp}")); + } // 1. System prompt. if let Some(sp) = system_prompt { @@ -1298,7 +1305,7 @@ mod tests { cancelled_events: vec![], }; - let prompt = format_prompt(&batch, None, None, None, None); + let prompt = format_prompt(&batch, None, None, None, None, None); // Should contain [Context] section before the event. assert!(prompt.contains("[Context]")); @@ -1394,7 +1401,7 @@ mod tests { cancelled_events: vec![], }; - let prompt = format_prompt(&batch, None, None, None, None); + let prompt = format_prompt(&batch, None, None, None, None, None); assert!(prompt.contains("[Context]")); assert!(prompt.contains("[Sprout events — 3 events]")); @@ -1423,10 +1430,54 @@ mod tests { cancelled_events: vec![], }; - let prompt = format_prompt(&batch, Some("You are a triage bot."), None, None, None); + let prompt = format_prompt( + &batch, + None, + Some("You are a triage bot."), + None, + None, + None, + ); assert!(prompt.starts_with("[System]\nYou are a triage bot.\n\n[Context]")); } + // ── Test 11b: base prompt prepended before system prompt ───────────────── + + #[test] + fn test_format_prompt_with_base_prompt() { + let ch = Uuid::new_v4(); + let event = make_event("hello"); + + let batch = FlushBatch { + channel_id: ch, + events: vec![BatchEvent { + event, + prompt_tag: "test".into(), + received_at: Instant::now(), + }], + cancelled_events: vec![], + }; + + // Both base_prompt and system_prompt: [Base] comes first, then [System]. + let prompt = format_prompt( + &batch, + Some("Platform base."), + Some("Role prompt."), + None, + None, + None, + ); + assert!(prompt.starts_with("[Base]\nPlatform base.\n\n[System]\nRole prompt.")); + + // Only base_prompt (no system_prompt): [Base] comes first, then [Context]. + let prompt = format_prompt(&batch, Some("Platform base."), None, None, None, None); + assert!(prompt.starts_with("[Base]\nPlatform base.\n\n[Context]")); + + // No base_prompt: no [Base] section emitted. + let prompt = format_prompt(&batch, None, None, None, None, None); + assert!(!prompt.contains("[Base]")); + } + // ── Test 12: drop mode discards in-flight channel events ───────────────── #[test] @@ -1903,7 +1954,7 @@ mod tests { channel_type: "stream".into(), }; - let prompt = format_prompt(&batch, None, Some(&ci), None, None); + let prompt = format_prompt(&batch, None, None, Some(&ci), None, None); assert!(prompt.contains("engineering (#")); assert!(prompt.contains("Scope: channel")); } @@ -1926,7 +1977,7 @@ mod tests { channel_type: "dm".into(), }; - let prompt = format_prompt(&batch, None, Some(&ci), None, None); + let prompt = format_prompt(&batch, None, None, Some(&ci), None, None); assert!(prompt.contains("Scope: dm")); } @@ -1952,7 +2003,7 @@ mod tests { cancelled_events: vec![], }; - let prompt = format_prompt(&batch, None, None, None, None); + let prompt = format_prompt(&batch, None, None, None, None, None); assert!(prompt.contains("Scope: thread")); assert!(prompt.contains("Thread root: root123")); } @@ -1995,7 +2046,7 @@ mod tests { truncated: true, }; - let prompt = format_prompt(&batch, None, None, Some(&ctx), None); + let prompt = format_prompt(&batch, None, None, None, Some(&ctx), None); assert!(prompt.contains("[Thread Context (2 of 5 messages, truncated)]")); assert!(prompt.contains("Let's refactor auth")); assert!(prompt.contains("Thread context included below")); @@ -2028,7 +2079,7 @@ mod tests { truncated: false, }; - let prompt = format_prompt(&batch, None, Some(&ci), Some(&ctx), None); + let prompt = format_prompt(&batch, None, None, Some(&ci), Some(&ctx), None); assert!(prompt.contains("Scope: dm")); assert!(prompt.contains("[Conversation Context (1 of 1 messages)]")); assert!(prompt.contains("Can you deploy?")); @@ -2080,7 +2131,7 @@ mod tests { ), ]); - let prompt = format_prompt(&batch, None, None, Some(&ctx), Some(&profiles)); + let prompt = format_prompt(&batch, None, None, None, Some(&ctx), Some(&profiles)); assert!(prompt.contains("From: Wes (npub:")); assert!(prompt.contains( @@ -2175,7 +2226,7 @@ mod tests { truncated: false, }; - let prompt = format_prompt(&batch, None, Some(&ci), Some(&ctx), None); + let prompt = format_prompt(&batch, None, None, Some(&ci), Some(&ctx), None); // Scope should be "dm", not "thread". assert!( prompt.contains("Scope: dm"), @@ -2214,7 +2265,7 @@ mod tests { }; // No context fetched — hints only. - let prompt = format_prompt(&batch, None, Some(&ci), None, None); + let prompt = format_prompt(&batch, None, None, Some(&ci), None, None); assert!(prompt.contains("Scope: dm")); assert!( prompt.contains("get_channel_history()"), @@ -2241,7 +2292,7 @@ mod tests { cancelled_events: vec![], }; - let prompt = format_prompt(&batch, None, None, None, None); + let prompt = format_prompt(&batch, None, None, None, None, None); assert!( prompt.contains(&format!("Event ID: {event_id}")), "prompt should contain the event ID" @@ -2264,7 +2315,7 @@ mod tests { cancelled_events: vec![], }; - let prompt = format_prompt(&batch, None, None, None, None); + let prompt = format_prompt(&batch, None, None, None, None, None); assert!( prompt.contains(&format!("From: {npub} (hex: {hex})")), "prompt should contain both npub and hex" @@ -2286,7 +2337,7 @@ mod tests { cancelled_events: vec![], }; - let prompt = format_prompt(&batch, None, None, None, None); + let prompt = format_prompt(&batch, None, None, None, None, None); assert!( prompt.contains("Tags:"), "tags should always be included, even for stream messages" @@ -2610,7 +2661,7 @@ mod tests { cancelled_events: vec![], }; - let prompt = format_prompt(&batch, None, None, None, None); + let prompt = format_prompt(&batch, None, None, None, None, None); assert!( prompt.contains(&format!("parent_event_id=\"{event_id}\"")), "channel thread reply should include reply instruction with triggering event ID" @@ -2644,7 +2695,7 @@ mod tests { channel_type: "dm".into(), }; - let prompt = format_prompt(&batch, None, Some(&ci), None, None); + let prompt = format_prompt(&batch, None, None, Some(&ci), None, None); assert!( prompt.contains(&format!("parent_event_id=\"{event_id}\"")), "DM thread reply should include reply instruction" @@ -2665,7 +2716,7 @@ mod tests { cancelled_events: vec![], }; - let prompt = format_prompt(&batch, None, None, None, None); + let prompt = format_prompt(&batch, None, None, None, None, None); assert!( !prompt.contains("parent_event_id"), "top-level message should NOT include reply instruction" @@ -2690,7 +2741,7 @@ mod tests { channel_type: "dm".into(), }; - let prompt = format_prompt(&batch, None, Some(&ci), None, None); + let prompt = format_prompt(&batch, None, None, Some(&ci), None, None); assert!( !prompt.contains("parent_event_id"), "DM non-reply should NOT include reply instruction" @@ -2720,7 +2771,7 @@ mod tests { cancelled_events: vec![], }; - let prompt = format_prompt(&batch, None, None, None, None); + let prompt = format_prompt(&batch, None, None, None, None, None); // The instruction should use the triggering event's own ID — not root or parent. assert!( prompt.contains(&format!("parent_event_id=\"{event_id}\"")), @@ -2763,7 +2814,7 @@ mod tests { cancelled_events: vec![], }; - let prompt = format_prompt(&batch, None, None, None, None); + let prompt = format_prompt(&batch, None, None, None, None, None); assert!( prompt.contains(&format!("parent_event_id=\"{threaded_id}\"")), "batched prompt should use last (threaded) event's ID" @@ -2796,7 +2847,7 @@ mod tests { cancelled_events: vec![], }; - let prompt = format_prompt(&batch, None, None, None, None); + let prompt = format_prompt(&batch, None, None, None, None, None); assert!( !prompt.contains("parent_event_id"), "batched prompt where last event is top-level should NOT include reply instruction" diff --git a/crates/sprout-persona/PERSONA_PACK_SPEC.md b/crates/sprout-persona/PERSONA_PACK_SPEC.md index cbbeaecf..5ffc961a 100644 --- a/crates/sprout-persona/PERSONA_PACK_SPEC.md +++ b/crates/sprout-persona/PERSONA_PACK_SPEC.md @@ -94,7 +94,7 @@ none of them override it. - **Extension mechanism**: Sprout-specific fields sit at the top level of `plugin.json` alongside OPS fields. No OPS core field is overloaded. - **`defaults`**: ignored entirely by OPS consumers. sprout-acp resolves it at deploy time before - constructing per-persona configurations (see Section 9 and Section 11). + constructing per-persona configurations (see Section 10 and Section 12). --- @@ -129,7 +129,7 @@ my-pack/ - `agents/` — all persona files. No nesting; flat directory. - `skills/` — one subdirectory per skill. Each skill directory contains a `SKILL.md` file. - Both `name:` and `description:` frontmatter fields are **required** — see Section 5. + Both `name:` and `description:` frontmatter fields are **required** — see Section 6. - `.plugin/` — OPS-required location for the manifest. - `hooks/` — optional; omit if no hooks are needed. - `instructions.md` — optional; omit if no pack-level instructions. @@ -146,7 +146,7 @@ A persona file is a markdown document with YAML frontmatter. The **YAML frontmat identity, skills, MCP servers, and behavioral config. The **markdown body** (everything after the closing `---`) is the agent's persona prompt text. -> **Note**: The persona prompt is currently delivered as a `[System]` prefix in the user message text (see Section 11). True system prompt injection (once at session creation rather than every turn) is planned — see Section 15. +> **Note**: The persona prompt is currently delivered as a `[System]` prefix in the user message text (see Section 12). True system prompt injection (once at session creation rather than every turn) is planned — see Section 16. ### Full Schema @@ -209,14 +209,14 @@ You are Lep, a security-focused code reviewer on the Meadow team. | `author` | string | ❌ | OPS compatibility field. | | `skills` | string[] | ❌ | Pack-relative paths to skill directories for this agent only. | | `mcp_servers` | object[] | ❌ | Per-persona MCP servers. Merged with pack-level `.mcp.json`. | -| `subscribe` | string[] | ❌ | Channels to monitor. See Section 9. | -| `triggers` | object | ❌ | Controls which messages activate a response. See Section 9. | -| `model` | string | ❌ | Model to use. See Section 9. | -| `temperature` | float | ❌ | Sampling temperature. See Section 9. | -| `max_context_tokens` | int | ❌ | Context window limit. See Section 9. | -| `thread_replies` | bool | ❌ | Reply in-thread when triggering message is in a thread. See Section 9. | -| `broadcast_replies` | bool | ❌ | Surface thread replies to the main channel. See Section 9. | -| `hooks` | object | ❌ | Lifecycle hooks. Harness-managed. See Section 8. | +| `subscribe` | string[] | ❌ | Channels to monitor. See Section 10. | +| `triggers` | object | ❌ | Controls which messages activate a response. See Section 10. | +| `model` | string | ❌ | Model to use. See Section 10. | +| `temperature` | float | ❌ | Sampling temperature. See Section 10. | +| `max_context_tokens` | int | ❌ | Context window limit. See Section 10. | +| `thread_replies` | bool | ❌ | Reply in-thread when triggering message is in a thread. See Section 10. | +| `broadcast_replies` | bool | ❌ | Surface thread replies to the main channel. See Section 10. | +| `hooks` | object | ❌ | Lifecycle hooks. Harness-managed. See Section 9. | > **Legacy alias**: The YAML key `respond_to` is accepted as an alias for `triggers` in persona frontmatter. In `plugin.json` defaults, both `triggers` and `respond_to` are accepted. The canonical key is `triggers`. @@ -228,7 +228,86 @@ files (agent runtimes typically do not read them). --- -## 5. Skills +## 5. Two-Layer Prompt Architecture + +sprout-acp assembles the agent's context from two distinct prompt layers before sending each +message. Understanding this layering is essential for persona authors — content that belongs in +one layer should not be duplicated in the other. + +### Prompt Section Order + +Each message delivered to the agent runtime includes these sections in order: + +``` +[Base] + + +[System] + + +--- +# Team Instructions + + +[Context] + + +[Thread/Conversation Context] + + +[Sprout event] + +``` + +### The `[Base]` Layer + +The `[Base]` layer is compiled into sprout-acp and is **identical for every agent**. It covers: + +| Content | Purpose | +|---------|---------| +| Platform identity | Tells the agent it is running inside Sprout and what that means | +| MCP tool reference | Documents the tools available via the connected MCP servers | +| Workspace layout | Describes `$AGENT_CWD`, skill discovery paths, and file conventions | +| Message polling | Explains how to check for new messages proactively | + +Pack authors do not write or configure the `[Base]` layer — it is maintained by the Sprout team +and updated in sprout-acp releases. + +**Disabling the base layer**: Set `SPROUT_ACP_BASE_PROMPT_DISABLED=1` in the sprout-acp process +environment to omit the `[Base]` section entirely. This is intended for testing and advanced +deployments where operators supply their own platform context. + +### The `[System]` Layer + +The `[System]` layer is the persona prompt — the markdown body of the `.persona.md` file. It is +**unique per agent** and defines the agent's role, identity, and behavioral rules. This is where +pack authors write their persona content. + +What belongs in `[System]`: + +| Content | Examples | +|---------|---------| +| Agent name and role | "You are Lep, a security-focused code reviewer" | +| Team protocols | Escalation rules, @-mention discipline, handoff conventions | +| Domain rules | Security checklists, review criteria, coding standards | +| Behavioral autonomy | When to act independently vs. when to ask | + +### Guidance for Pack Authors + +**Do not duplicate base layer content in persona prompts.** Users with the base layer enabled +(the default) would see that content twice per message. Specifically, do not re-explain: + +- How to use MCP tools (covered by `[Base]`) +- How to poll for new messages or use the `since` parameter (covered by `[Base]`) +- Workspace layout or skill loading mechanics (covered by `[Base]`) +- That the agent is running inside Sprout (covered by `[Base]`) + +Focus persona prompts on what makes this agent unique: its role, personality, domain expertise, +and team-specific protocols. + +--- + +## 6. Skills > **Implementation note**: Skill paths are stored as declared in persona frontmatter. Resolution > to `SKILL.md` `name:` fields and runtime copying to `$AGENT_CWD/.agents/skills/` is planned @@ -323,7 +402,7 @@ load(source: "security-review") ``` sprout-acp lists available skills in the user message prefix so the agent knows what's available. -See Section 11 for the full message format. +See Section 12 for the full message format. ### Skill File Format @@ -344,7 +423,7 @@ enforce required metadata fields (see PF-5). --- -## 6. MCP Server Configuration +## 7. MCP Server Configuration MCP servers provide external tool access (GitHub, Semgrep, databases, etc.). Configuration is defined at two levels: pack-level (shared across all agents) and per-persona (agent-specific). @@ -408,13 +487,13 @@ sprout-acp passes the merged config via `NewSessionRequest.mcp_servers`. **No `. --- -## 7. Pack-Level Instructions +## 8. Pack-Level Instructions `instructions.md` contains shared rules, coding standards, and team norms that apply to all agents in the pack. sprout-acp appends it to the persona prompt in the user message prefix. sprout-acp appends `instructions.md` to the persona prompt in the user message prefix (see -Section 11). **No file is written to disk.** +Section 12). **No file is written to disk.** **What does NOT work**: `.mdc` rule files (agent runtimes typically don't read them), `rules/` directory (no `--rules-dir` flag), relying on the pack's `AGENTS.md` for runtime injection (it's for human @@ -426,7 +505,7 @@ contributors only). --- -## 8. Lifecycle Hooks +## 9. Lifecycle Hooks > **Implementation note**: Hooks are parsed and validated at pack load time but not yet executed. > Hook execution is planned for a future release. @@ -492,7 +571,7 @@ sprout-acp means no hooks fire. --- -## 9. Behavioral Configuration +## 10. Behavioral Configuration The behavioral config fields in a persona's frontmatter control how the agent participates in Sprout conversations. These are all Sprout-specific — the agent runtime has no awareness of them. They sit @@ -763,7 +842,7 @@ All fields are consumed entirely by sprout-acp. None are passed to the agent run --- -## 10. Distribution +## 11. Distribution ### Phase 1: Zip File @@ -845,7 +924,7 @@ The Sprout desktop app can import persona packs via the Import button: --- -## 11. Delivery Mechanism Summary +## 12. Delivery Mechanism Summary How each pack component reaches the running agent: @@ -862,7 +941,7 @@ How each pack component reaches the running agent: > **Pack defaults are resolved at deploy time**, not at runtime. When sprout-acp loads a pack and > constructs per-persona session configurations, it merges the `defaults` object with each persona's -> frontmatter behavioral config fields (per the precedence model in Section 9) and stores the +> frontmatter behavioral config fields (per the precedence model in Section 10) and stores the > resulting effective configuration. The `defaults` object itself is not forwarded to the agent runtime or > stored in any runtime artifact — only the resolved per-persona values are used. @@ -892,7 +971,7 @@ Load a skill with: load(source: "skill-name") The `[System]` prefix re-sends the full persona prompt on every turn. True system prompt injection — calling `agent.extend_system_prompt()` after `create_agent_for_session()` in `on_new_session()` -— fires once at session creation. This is planned work; see Section 15. +— fires once at session creation. This is planned work; see Section 16. ### What Does NOT Work (Anti-Pattern Reference) @@ -911,12 +990,12 @@ The `[System]` prefix re-sends the full persona prompt on every turn. True syste --- -## 12. Security Considerations +## 13. Security Considerations ### Secret Management Never embed secrets in pack files. Use `${VAR_NAME}` references in all `env` blocks. Currently, -`${VAR_NAME}` strings are passed through as literals to the agent runtime (see Section 6). When +`${VAR_NAME}` strings are passed through as literals to the agent runtime (see Section 7). When harness-side interpolation is implemented, sprout-acp will resolve them from the process environment at startup and refuse to start if any are unresolved. Inject secrets via your deployment mechanism (systemd env files, Vault, Kubernetes secrets, etc.). @@ -944,7 +1023,7 @@ both with the same caution as any untrusted prompt content. --- -## 13. Migration Path +## 14. Migration Path ### From V6 (sprout-namespaced) Format @@ -1022,7 +1101,7 @@ The V6 namespaced `sprout:` block format is not supported. Only the current flat --- -## 14. Open Questions / Future Work +## 15. Open Questions / Future Work ### Unresolved @@ -1052,7 +1131,7 @@ The V6 namespaced `sprout:` block format is not supported. Only the current flat --- -## 15. Planned Features +## 16. Planned Features Features required by this spec but not yet implemented. diff --git a/desktop/src-tauri/src/managed_agents/personas.rs b/desktop/src-tauri/src/managed_agents/personas.rs index b3cc61ae..55899f74 100644 --- a/desktop/src-tauri/src/managed_agents/personas.rs +++ b/desktop/src-tauri/src/managed_agents/personas.rs @@ -14,6 +14,7 @@ struct BuiltInPersona { avatar_url: Option<&'static str>, system_prompt: &'static str, name_pool: &'static [&'static str], + model: Option<&'static str>, } const SOLO_AVATAR: &str = "data:image/png;base64,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"; @@ -110,7 +111,6 @@ Surface to the user only when: - After **fixes**: what was addressed before re-review. - **Between phase updates**, stay quiet while things go well. The Autonomy section above governs when to surface a question vs. resolve it independently — default to resolving. - For longer phases, post a one-line progress update roughly every 15 minutes so the user knows you're alive. -- In a Sprout channel, check for new messages proactively (use your read tools with the `since` parameter) so you don't miss the user trying to talk to you. - **Completion** — Concise final report: - What changed and why - Files changed @@ -146,6 +146,7 @@ const BUILT_IN_PERSONAS: &[BuiltInPersona] = &[ "Atlas", "Ember", "Flint", "Sage", "Drift", "Quill", "Wren", "Cedar", "Pike", "Lark", "Birch", "Dune", "Fern", "Moss", "Reed", "Slate", "Ash", "Brook", "Glen", "Stone", ], + model: Some("claude-sonnet-4-20250514"), }, BuiltInPersona { id: "builtin:kit", @@ -269,7 +270,6 @@ Surface to the user only when: - **Between phase updates**, stay quiet while things go well. Default to resolving via the Autonomy ladder. - For longer phases, post a one-line progress update roughly every 15 minutes so the user knows you're alive. - `@scout` follows the @-Mention Discipline rules at the top of this prompt — only on real assignment messages (new assignment, focused fix after COMPLETE/BLOCKED, or answer to a specific blocker), with the structured fields attached. Never `@mention` Scout twice for the same active phase. -- In a Sprout channel, check for new messages proactively (use your read tools with the `since` parameter) so you don't miss the user trying to talk to you. - **Completion** — Concise final report: - What changed and why - Files changed @@ -294,6 +294,7 @@ Aim for 9/10+ on the first pass. There is no separate refactoring pass — if it Don't present work that doesn't meet this bar. No emojis. Your name is Kit. You are friendly and helpful. You are understated, but have a sense of humor."#, + model: Some("claude-sonnet-4-20250514"), }, BuiltInPersona { id: "builtin:scout", @@ -452,6 +453,7 @@ You are read-only, but you still resolve questions yourself before pinging Kit: If you're invoked outside a Sprout team channel (or by an agent that isn't Kit), apply the same protocols. Default to FULL REVIEW for completed work or PLAN REVIEW + RESEARCH for plans. Report to whoever invoked you. Your name is Scout. You are friendly and helpful. You are understated, but have a sense of humor."#, + model: Some("claude-sonnet-4-20250514"), }, ]; @@ -468,7 +470,7 @@ fn built_in_persona_records(now: &str) -> Vec { avatar_url: persona.avatar_url.map(|s| s.to_string()), system_prompt: persona.system_prompt.to_string(), provider: None, - model: None, + model: persona.model.map(|s| s.to_string()), name_pool: persona.name_pool.iter().map(|s| s.to_string()).collect(), is_builtin: true, is_active: true, @@ -522,8 +524,8 @@ fn merge_personas(mut stored: Vec, now: &str) -> (Vec Date: Thu, 14 May 2026 16:19:44 -0400 Subject: [PATCH 02/10] fix: address all review feedback on two-layer prompt architecture MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Consolidated fixes from 5 independent reviewers (3 Claude specialists, Codex/GPT-5.5, Gemini) plus PLAN author feedback. Structural: refactor format_prompt() from 6 positional params to FormatPromptArgs struct — eliminates positional confusion and makes future additions zero-cost for existing call sites. Correctness: replace get_channel_history() references in [Context] hints with get_messages() (the actual MCP tool name), fix $AGENT_CWD reference in base_prompt.md (env var doesn't exist), fix [Context] description in PERSONA_PACK_SPEC to match reality. Config: migrate SPROUT_ACP_BASE_PROMPT_DISABLED from bare env::var check to proper Config field (no_base_prompt), add base_prompt_file for runtime override. Change base_prompt type from Option to Option<&'static str> to reflect compile-time constant nature. Personas: add provider: Some("claude") alongside model on all three built-in personas so the UI/runtime agree on the backend. Add comment documenting merge_personas() canonical-override semantics. Coverage: prepend [Base] to heartbeat and initial_message paths that previously bypassed format_prompt(). Add section-ordering test with full context. Fix trailing-newline triple-gap via trim_end(). --- crates/sprout-acp/README.md | 2 +- crates/sprout-acp/src/base_prompt.md | 6 +- crates/sprout-acp/src/config.rs | 22 ++ crates/sprout-acp/src/lib.rs | 15 +- crates/sprout-acp/src/pool.rs | 21 +- crates/sprout-acp/src/queue.rs | 242 +++++++++++++----- crates/sprout-persona/PERSONA_PACK_SPEC.md | 8 +- .../src-tauri/src/managed_agents/personas.rs | 9 +- 8 files changed, 246 insertions(+), 79 deletions(-) diff --git a/crates/sprout-acp/README.md b/crates/sprout-acp/README.md index ecd66bd6..b72c2f6a 100644 --- a/crates/sprout-acp/README.md +++ b/crates/sprout-acp/README.md @@ -237,7 +237,7 @@ Forum event kinds: 2. **Channel discovery** — Queries the relay REST API for accessible channels, subscribes to each. 3. **Event loop** — Listens for @mention events (kind 9 with the agent's pubkey in a `#p` tag). Events queue per channel. 4. **Prompting** — When events are pending and no prompt is in flight for that channel, drains all queued events for the oldest channel into a single batched prompt via ACP `session/prompt`. -5. **Agent response** — The agent processes the prompt and uses Sprout MCP tools (`send_message`, `get_channel_history`, etc.) to interact with Sprout. +5. **Agent response** — The agent processes the prompt and uses Sprout MCP tools (`send_message`, `get_messages`, etc.) to interact with Sprout. 6. **Recovery** — If the agent crashes, the harness respawns it. If the relay disconnects, the harness reconnects with a `since` filter to avoid missing events. Each channel has at most one prompt in flight. Multiple channels can be processed concurrently when agents > 1. diff --git a/crates/sprout-acp/src/base_prompt.md b/crates/sprout-acp/src/base_prompt.md index 852d3755..b9a0a490 100644 --- a/crates/sprout-acp/src/base_prompt.md +++ b/crates/sprout-acp/src/base_prompt.md @@ -1,6 +1,6 @@ You are operating inside the Sprout platform — a Nostr-based messaging platform for human-agent collaboration. The sprout-acp harness bridges channel events to your session. -## MCP Tools +## MCP Tools (via `sprout-mcp`) - `get_messages(channel_id, limit=50)` — fetch recent history (max 200 per call) - `get_messages(channel_id, since=)` — fetch messages since timestamp; returns oldest-first when `since` is set without `before` @@ -18,11 +18,11 @@ You are operating inside the Sprout platform — a Nostr-based messaging platfor ## Startup Recovery -On startup or after a gap: call `get_feed()` first to surface pending mentions and action items, then call `get_messages` on your assigned channels to catch up, then check the workspace `AGENTS.md` for team context. +On startup or after a gap: call `get_feed()` first to surface pending mentions and action items, then call `get_messages` on your assigned channels to catch up, then check `AGENTS.md` for team context. Use `search()` for cross-channel keyword lookups when you need to find specific prior discussions. ## Workspace Layout -Persistent workspace at `$AGENT_CWD/` with the following directories: +Your persistent workspace is in your working directory, with the following subdirectories: - `RESEARCH/` — findings and reference material - `PLANS/` — project and task plans diff --git a/crates/sprout-acp/src/config.rs b/crates/sprout-acp/src/config.rs index f7cf4923..3f8596ed 100644 --- a/crates/sprout-acp/src/config.rs +++ b/crates/sprout-acp/src/config.rs @@ -328,6 +328,20 @@ pub struct CliArgs { #[arg(long, env = "SPROUT_ACP_NO_TYPING")] pub no_typing: bool, + /// Disable the [Base] platform-context section prepended to every prompt. + /// When set, agents receive only the persona [System] prompt with no Sprout orientation. + #[arg(long, env = "SPROUT_ACP_NO_BASE_PROMPT")] + pub no_base_prompt: bool, + + /// Path to a custom base prompt file. Overrides the compiled-in default. + /// Mutually exclusive with --no-base-prompt. + #[arg( + long, + env = "SPROUT_ACP_BASE_PROMPT_FILE", + conflicts_with = "no_base_prompt" + )] + pub base_prompt_file: Option, + /// Desired LLM model ID. Applied to every new ACP session after creation. /// Use `sprout-acp models` to discover available model IDs. #[arg(long, env = "SPROUT_ACP_MODEL")] @@ -432,6 +446,10 @@ pub struct Config { /// Agent owner pubkey (hex). Used for `--respond-to=owner-only` gate. /// Replaces the old REST-based owner lookup. pub agent_owner: Option, + /// Disable the [Base] platform-context section prepended to every prompt. + pub no_base_prompt: bool, + /// Path to a custom base prompt file that overrides the compiled-in default. + pub base_prompt_file: Option, } /// Validate and deduplicate allowlist entries: each must be exactly 64 hex chars. @@ -768,6 +786,8 @@ impl Config { persona_env_vars, relay_observer: args.relay_observer, agent_owner: args.agent_owner.map(|s| s.trim().to_ascii_lowercase()), + no_base_prompt: args.no_base_prompt, + base_prompt_file: args.base_prompt_file, }; Ok(config) @@ -1129,6 +1149,8 @@ mod tests { persona_env_vars: vec![], relay_observer: false, agent_owner: None, + no_base_prompt: false, + base_prompt_file: None, } } diff --git a/crates/sprout-acp/src/lib.rs b/crates/sprout-acp/src/lib.rs index d9fc7f8f..3430bac2 100644 --- a/crates/sprout-acp/src/lib.rs +++ b/crates/sprout-acp/src/lib.rs @@ -1074,10 +1074,15 @@ async fn tokio_main() -> Result<()> { max_turn_duration: Duration::from_secs(config.max_turn_duration_secs), dedup_mode: config.dedup_mode, system_prompt: config.system_prompt.clone(), - base_prompt: if std::env::var("SPROUT_ACP_BASE_PROMPT_DISABLED").is_ok() { + base_prompt: if config.no_base_prompt { None + } else if let Some(ref path) = config.base_prompt_file { + let content = std::fs::read_to_string(path).unwrap_or_else(|e| { + panic!("failed to read base prompt file {}: {e}", path.display()) + }); + Some(Box::leak(content.into_boxed_str())) } else { - Some(include_str!("base_prompt.md").to_string()) + Some(include_str!("base_prompt.md")) }, heartbeat_prompt: config.heartbeat_prompt.clone(), cwd: std::env::current_dir() @@ -2267,6 +2272,10 @@ fn dispatch_heartbeat( .heartbeat_prompt .clone() .unwrap_or_else(default_heartbeat_prompt); + let prompt_text = match ctx.base_prompt { + Some(bp) => format!("[Base]\n{}\n\n{prompt_text}", bp.trim_end()), + None => prompt_text, + }; let result_tx = pool.result_tx(); let ctx_clone = Arc::clone(ctx); let agent_index = agent.index; @@ -2760,6 +2769,8 @@ mod build_mcp_servers_tests { persona_env_vars: vec![], relay_observer: false, agent_owner: None, + no_base_prompt: false, + base_prompt_file: None, } } diff --git a/crates/sprout-acp/src/pool.rs b/crates/sprout-acp/src/pool.rs index fdab36fb..b5f9391c 100644 --- a/crates/sprout-acp/src/pool.rs +++ b/crates/sprout-acp/src/pool.rs @@ -187,7 +187,7 @@ pub struct PromptContext { pub dedup_mode: DedupMode, pub system_prompt: Option, pub heartbeat_prompt: Option, - pub base_prompt: Option, + pub base_prompt: Option<&'static str>, pub cwd: String, /// REST client for pre-prompt context fetches (thread/DM history). pub rest_client: RestClient, @@ -747,11 +747,16 @@ pub async fn run_prompt_task( target: "pool::session", "sending initial_message to session {session_id} for channel {cid}" ); + // Prepend base prompt to initial_message for platform orientation. + let init_msg = match ctx.base_prompt { + Some(bp) => format!("[Base]\n{}\n\n{initial_msg}", bp.trim_end()), + None => initial_msg.to_string(), + }; let init_result = agent .acp .session_prompt_with_idle_timeout( &session_id, - initial_msg, + &init_msg, ctx.idle_timeout, ctx.max_turn_duration, ) @@ -874,11 +879,13 @@ pub async fn run_prompt_task( crate::queue::format_prompt( b, - ctx.base_prompt.as_deref(), - ctx.system_prompt.as_deref(), - channel_info.as_ref(), - conversation_context.as_ref(), - profile_lookup.as_ref(), + &crate::queue::FormatPromptArgs { + base_prompt: ctx.base_prompt, + system_prompt: ctx.system_prompt.as_deref(), + channel_info: channel_info.as_ref(), + conversation_context: conversation_context.as_ref(), + profile_lookup: profile_lookup.as_ref(), + }, ) } else { // Should not happen — batch is None only for heartbeats which have prompt_text. diff --git a/crates/sprout-acp/src/queue.rs b/crates/sprout-acp/src/queue.rs index b459e5c9..fa4b9ddc 100644 --- a/crates/sprout-acp/src/queue.rs +++ b/crates/sprout-acp/src/queue.rs @@ -846,15 +846,15 @@ fn format_context_hints( if is_dm { let is_reply = thread_tags.root_event_id.is_some(); // DM replies use get_thread() because /messages excludes thread replies. - // DM non-replies use get_channel_history() for recent conversation. + // DM non-replies use get_messages() for recent conversation. let ctx_hint = if has_conversation_context && is_reply { "Thread context included below. Use get_thread() for full history if truncated." } else if has_conversation_context { - "Conversation context included below. Use get_channel_history() for full history if truncated." + "Conversation context included below. Use get_messages() for full history if truncated." } else if is_reply { "Use get_thread() to fetch the reply chain." } else { - "Use get_channel_history() for conversation context." + "Use get_messages() for conversation context." }; let mut s = format!( "[Context]\n\ @@ -902,7 +902,7 @@ fn format_context_hints( "[Context]\n\ Scope: channel\n\ Channel: {channel_display}\n\ - Hint: Use get_channel_history() for recent messages if needed." + Hint: Use get_messages() for recent messages if needed." ) } } @@ -942,6 +942,16 @@ fn format_conversation_context( s } +/// Arguments for [`format_prompt`] beyond the required [`FlushBatch`]. +#[derive(Default)] +pub struct FormatPromptArgs<'a> { + pub base_prompt: Option<&'a str>, + pub system_prompt: Option<&'a str>, + pub channel_info: Option<&'a PromptChannelInfo>, + pub conversation_context: Option<&'a ConversationContext>, + pub profile_lookup: Option<&'a PromptProfileLookup>, +} + /// Format a [`FlushBatch`] into a prompt string for the agent. /// /// Produces a stable prompt with these sections (in order): @@ -950,14 +960,7 @@ fn format_conversation_context( /// 2. `[Context]` — scope, channel name, and contextual hints for the agent /// 3. `[Thread Context]` or `[Conversation Context]` — if fetched /// 4. `[Event]` / `[Sprout events]` — the triggering event(s) -pub fn format_prompt( - batch: &FlushBatch, - base_prompt: Option<&str>, - system_prompt: Option<&str>, - channel_info: Option<&PromptChannelInfo>, - conversation_context: Option<&ConversationContext>, - profile_lookup: Option<&PromptProfileLookup>, -) -> String { +pub fn format_prompt(batch: &FlushBatch, args: &FormatPromptArgs<'_>) -> String { // Scope is always derived from the LAST event in the batch — that's the // one the agent is responding to. Thread/DM context is supplementary info // included alongside, not a scope override. This prevents mixed batches @@ -970,19 +973,20 @@ pub fn format_prompt( } }; let thread_tags = parse_thread_tags(&last_event.event); - let is_dm = channel_info + let is_dm = args + .channel_info .map(|ci| ci.channel_type == "dm") .unwrap_or(false); - let mut sections: Vec = Vec::with_capacity(5); + let mut sections: Vec = Vec::with_capacity(7); // 0. Base prompt (platform-level, always first). - if let Some(bp) = base_prompt { - sections.push(format!("[Base]\n{bp}")); + if let Some(bp) = args.base_prompt { + sections.push(format!("[Base]\n{}", bp.trim_end())); } // 1. System prompt. - if let Some(sp) = system_prompt { + if let Some(sp) = args.system_prompt { sections.push(format!("[System]\n{sp}")); } @@ -994,16 +998,16 @@ pub fn format_prompt( }; sections.push(format_context_hints( batch.channel_id, - channel_info, + args.channel_info, &thread_tags, is_dm, - conversation_context.is_some(), + args.conversation_context.is_some(), triggering_event_id.as_deref(), )); // 3. Conversation context (thread or DM). - if let Some(ctx) = conversation_context { - sections.push(format_conversation_context(ctx, profile_lookup)); + if let Some(ctx) = args.conversation_context { + sections.push(format_conversation_context(ctx, args.profile_lookup)); } // 4a. Cancelled events section (cancel + re-prompt). @@ -1014,7 +1018,7 @@ pub fn format_prompt( "\n\n--- Event {} ({}) ---\n{}", i + 1, be.prompt_tag, - format_event_block(batch.channel_id, channel_info, be, profile_lookup) + format_event_block(batch.channel_id, args.channel_info, be, args.profile_lookup) )); } sections.push(s); @@ -1028,13 +1032,13 @@ pub fn format_prompt( format!( "[New request — supersedes previous]\n\n--- Event 1 ({}) ---\n{}", be.prompt_tag, - format_event_block(batch.channel_id, channel_info, be, profile_lookup) + format_event_block(batch.channel_id, args.channel_info, be, args.profile_lookup) ) } else { format!( "[Sprout event: {}]\n{}", be.prompt_tag, - format_event_block(batch.channel_id, channel_info, be, profile_lookup) + format_event_block(batch.channel_id, args.channel_info, be, args.profile_lookup) ) } } else { @@ -1052,7 +1056,7 @@ pub fn format_prompt( "\n\n--- Event {} ({}) ---\n{}", i + 1, be.prompt_tag, - format_event_block(batch.channel_id, channel_info, be, profile_lookup) + format_event_block(batch.channel_id, args.channel_info, be, args.profile_lookup) )); } s @@ -1305,7 +1309,7 @@ mod tests { cancelled_events: vec![], }; - let prompt = format_prompt(&batch, None, None, None, None, None); + let prompt = format_prompt(&batch, &FormatPromptArgs::default()); // Should contain [Context] section before the event. assert!(prompt.contains("[Context]")); @@ -1401,7 +1405,7 @@ mod tests { cancelled_events: vec![], }; - let prompt = format_prompt(&batch, None, None, None, None, None); + let prompt = format_prompt(&batch, &FormatPromptArgs::default()); assert!(prompt.contains("[Context]")); assert!(prompt.contains("[Sprout events — 3 events]")); @@ -1432,11 +1436,10 @@ mod tests { let prompt = format_prompt( &batch, - None, - Some("You are a triage bot."), - None, - None, - None, + &FormatPromptArgs { + system_prompt: Some("You are a triage bot."), + ..Default::default() + }, ); assert!(prompt.starts_with("[System]\nYou are a triage bot.\n\n[Context]")); } @@ -1461,21 +1464,81 @@ mod tests { // Both base_prompt and system_prompt: [Base] comes first, then [System]. let prompt = format_prompt( &batch, - Some("Platform base."), - Some("Role prompt."), - None, - None, - None, + &FormatPromptArgs { + base_prompt: Some("Platform base."), + system_prompt: Some("Role prompt."), + ..Default::default() + }, ); assert!(prompt.starts_with("[Base]\nPlatform base.\n\n[System]\nRole prompt.")); // Only base_prompt (no system_prompt): [Base] comes first, then [Context]. - let prompt = format_prompt(&batch, Some("Platform base."), None, None, None, None); + let prompt = format_prompt( + &batch, + &FormatPromptArgs { + base_prompt: Some("Platform base."), + ..Default::default() + }, + ); assert!(prompt.starts_with("[Base]\nPlatform base.\n\n[Context]")); // No base_prompt: no [Base] section emitted. - let prompt = format_prompt(&batch, None, None, None, None, None); + let prompt = format_prompt(&batch, &FormatPromptArgs::default()); assert!(!prompt.contains("[Base]")); + assert!(prompt.starts_with("[Context]")); + } + + #[test] + fn test_format_prompt_base_prompt_ordering_with_full_context() { + let ch = Uuid::new_v4(); + let event = make_event("hello"); + let batch = FlushBatch { + channel_id: ch, + events: vec![BatchEvent { + event, + prompt_tag: "test".into(), + received_at: Instant::now(), + }], + cancelled_events: vec![], + }; + + let ctx = ConversationContext::Thread { + messages: vec![ContextMessage { + pubkey: "npub1test".into(), + content: "prior message".into(), + timestamp: "2024-01-01T00:00:00Z".into(), + }], + total: 1, + truncated: false, + }; + + let prompt = format_prompt( + &batch, + &FormatPromptArgs { + base_prompt: Some("Platform base."), + system_prompt: Some("Role prompt."), + conversation_context: Some(&ctx), + ..Default::default() + }, + ); + + // Verify section ordering: [Base] < [System] < [Context] < [Thread Context] + let base_pos = prompt.find("[Base]").expect("[Base] missing"); + let system_pos = prompt.find("[System]").expect("[System] missing"); + let context_pos = prompt.find("[Context]").expect("[Context] missing"); + let thread_pos = prompt + .find("[Thread Context") + .expect("[Thread Context] missing"); + + assert!(base_pos < system_pos, "[Base] must come before [System]"); + assert!( + system_pos < context_pos, + "[System] must come before [Context]" + ); + assert!( + context_pos < thread_pos, + "[Context] must come before [Thread Context]" + ); } // ── Test 12: drop mode discards in-flight channel events ───────────────── @@ -1954,7 +2017,13 @@ mod tests { channel_type: "stream".into(), }; - let prompt = format_prompt(&batch, None, None, Some(&ci), None, None); + let prompt = format_prompt( + &batch, + &FormatPromptArgs { + channel_info: Some(&ci), + ..Default::default() + }, + ); assert!(prompt.contains("engineering (#")); assert!(prompt.contains("Scope: channel")); } @@ -1977,7 +2046,13 @@ mod tests { channel_type: "dm".into(), }; - let prompt = format_prompt(&batch, None, None, Some(&ci), None, None); + let prompt = format_prompt( + &batch, + &FormatPromptArgs { + channel_info: Some(&ci), + ..Default::default() + }, + ); assert!(prompt.contains("Scope: dm")); } @@ -2003,7 +2078,7 @@ mod tests { cancelled_events: vec![], }; - let prompt = format_prompt(&batch, None, None, None, None, None); + let prompt = format_prompt(&batch, &FormatPromptArgs::default()); assert!(prompt.contains("Scope: thread")); assert!(prompt.contains("Thread root: root123")); } @@ -2046,7 +2121,13 @@ mod tests { truncated: true, }; - let prompt = format_prompt(&batch, None, None, None, Some(&ctx), None); + let prompt = format_prompt( + &batch, + &FormatPromptArgs { + conversation_context: Some(&ctx), + ..Default::default() + }, + ); assert!(prompt.contains("[Thread Context (2 of 5 messages, truncated)]")); assert!(prompt.contains("Let's refactor auth")); assert!(prompt.contains("Thread context included below")); @@ -2079,7 +2160,14 @@ mod tests { truncated: false, }; - let prompt = format_prompt(&batch, None, None, Some(&ci), Some(&ctx), None); + let prompt = format_prompt( + &batch, + &FormatPromptArgs { + channel_info: Some(&ci), + conversation_context: Some(&ctx), + ..Default::default() + }, + ); assert!(prompt.contains("Scope: dm")); assert!(prompt.contains("[Conversation Context (1 of 1 messages)]")); assert!(prompt.contains("Can you deploy?")); @@ -2131,7 +2219,14 @@ mod tests { ), ]); - let prompt = format_prompt(&batch, None, None, None, Some(&ctx), Some(&profiles)); + let prompt = format_prompt( + &batch, + &FormatPromptArgs { + conversation_context: Some(&ctx), + profile_lookup: Some(&profiles), + ..Default::default() + }, + ); assert!(prompt.contains("From: Wes (npub:")); assert!(prompt.contains( @@ -2226,13 +2321,20 @@ mod tests { truncated: false, }; - let prompt = format_prompt(&batch, None, None, Some(&ci), Some(&ctx), None); + let prompt = format_prompt( + &batch, + &FormatPromptArgs { + channel_info: Some(&ci), + conversation_context: Some(&ctx), + ..Default::default() + }, + ); // Scope should be "dm", not "thread". assert!( prompt.contains("Scope: dm"), "DM reply should have Scope: dm, got:\n{prompt}" ); - // Hint should point to get_thread(), not get_channel_history(). + // Hint should point to get_thread(), not get_messages(). assert!( prompt.contains("get_thread()"), "DM reply hint should mention get_thread(), got:\n{prompt}" @@ -2247,7 +2349,7 @@ mod tests { } #[test] - fn test_format_prompt_dm_non_reply_hints_get_channel_history() { + fn test_format_prompt_dm_non_reply_hints_get_messages() { let ch = Uuid::new_v4(); let event = make_event("hey there"); let batch = FlushBatch { @@ -2265,11 +2367,17 @@ mod tests { }; // No context fetched — hints only. - let prompt = format_prompt(&batch, None, None, Some(&ci), None, None); + let prompt = format_prompt( + &batch, + &FormatPromptArgs { + channel_info: Some(&ci), + ..Default::default() + }, + ); assert!(prompt.contains("Scope: dm")); assert!( - prompt.contains("get_channel_history()"), - "DM non-reply hint should mention get_channel_history()" + prompt.contains("get_messages()"), + "DM non-reply hint should mention get_messages()" ); assert!( !prompt.contains("get_thread()"), @@ -2292,7 +2400,7 @@ mod tests { cancelled_events: vec![], }; - let prompt = format_prompt(&batch, None, None, None, None, None); + let prompt = format_prompt(&batch, &FormatPromptArgs::default()); assert!( prompt.contains(&format!("Event ID: {event_id}")), "prompt should contain the event ID" @@ -2315,7 +2423,7 @@ mod tests { cancelled_events: vec![], }; - let prompt = format_prompt(&batch, None, None, None, None, None); + let prompt = format_prompt(&batch, &FormatPromptArgs::default()); assert!( prompt.contains(&format!("From: {npub} (hex: {hex})")), "prompt should contain both npub and hex" @@ -2337,7 +2445,7 @@ mod tests { cancelled_events: vec![], }; - let prompt = format_prompt(&batch, None, None, None, None, None); + let prompt = format_prompt(&batch, &FormatPromptArgs::default()); assert!( prompt.contains("Tags:"), "tags should always be included, even for stream messages" @@ -2661,7 +2769,7 @@ mod tests { cancelled_events: vec![], }; - let prompt = format_prompt(&batch, None, None, None, None, None); + let prompt = format_prompt(&batch, &FormatPromptArgs::default()); assert!( prompt.contains(&format!("parent_event_id=\"{event_id}\"")), "channel thread reply should include reply instruction with triggering event ID" @@ -2695,7 +2803,13 @@ mod tests { channel_type: "dm".into(), }; - let prompt = format_prompt(&batch, None, None, Some(&ci), None, None); + let prompt = format_prompt( + &batch, + &FormatPromptArgs { + channel_info: Some(&ci), + ..Default::default() + }, + ); assert!( prompt.contains(&format!("parent_event_id=\"{event_id}\"")), "DM thread reply should include reply instruction" @@ -2716,7 +2830,7 @@ mod tests { cancelled_events: vec![], }; - let prompt = format_prompt(&batch, None, None, None, None, None); + let prompt = format_prompt(&batch, &FormatPromptArgs::default()); assert!( !prompt.contains("parent_event_id"), "top-level message should NOT include reply instruction" @@ -2741,7 +2855,13 @@ mod tests { channel_type: "dm".into(), }; - let prompt = format_prompt(&batch, None, None, Some(&ci), None, None); + let prompt = format_prompt( + &batch, + &FormatPromptArgs { + channel_info: Some(&ci), + ..Default::default() + }, + ); assert!( !prompt.contains("parent_event_id"), "DM non-reply should NOT include reply instruction" @@ -2771,7 +2891,7 @@ mod tests { cancelled_events: vec![], }; - let prompt = format_prompt(&batch, None, None, None, None, None); + let prompt = format_prompt(&batch, &FormatPromptArgs::default()); // The instruction should use the triggering event's own ID — not root or parent. assert!( prompt.contains(&format!("parent_event_id=\"{event_id}\"")), @@ -2814,7 +2934,7 @@ mod tests { cancelled_events: vec![], }; - let prompt = format_prompt(&batch, None, None, None, None, None); + let prompt = format_prompt(&batch, &FormatPromptArgs::default()); assert!( prompt.contains(&format!("parent_event_id=\"{threaded_id}\"")), "batched prompt should use last (threaded) event's ID" @@ -2847,7 +2967,7 @@ mod tests { cancelled_events: vec![], }; - let prompt = format_prompt(&batch, None, None, None, None, None); + let prompt = format_prompt(&batch, &FormatPromptArgs::default()); assert!( !prompt.contains("parent_event_id"), "batched prompt where last event is top-level should NOT include reply instruction" diff --git a/crates/sprout-persona/PERSONA_PACK_SPEC.md b/crates/sprout-persona/PERSONA_PACK_SPEC.md index 5ffc961a..f29fa322 100644 --- a/crates/sprout-persona/PERSONA_PACK_SPEC.md +++ b/crates/sprout-persona/PERSONA_PACK_SPEC.md @@ -250,7 +250,7 @@ Each message delivered to the agent runtime includes these sections in order: [Context] - + [Thread/Conversation Context] @@ -273,9 +273,9 @@ The `[Base]` layer is compiled into sprout-acp and is **identical for every agen Pack authors do not write or configure the `[Base]` layer — it is maintained by the Sprout team and updated in sprout-acp releases. -**Disabling the base layer**: Set `SPROUT_ACP_BASE_PROMPT_DISABLED=1` in the sprout-acp process -environment to omit the `[Base]` section entirely. This is intended for testing and advanced -deployments where operators supply their own platform context. +**Disabling or customizing the base layer**: Set `SPROUT_ACP_NO_BASE_PROMPT` to omit the `[Base]` +section entirely. To replace the compiled-in default with custom content, set +`SPROUT_ACP_BASE_PROMPT_FILE` to a file path — sprout-acp reads it at startup and uses it instead. ### The `[System]` Layer diff --git a/desktop/src-tauri/src/managed_agents/personas.rs b/desktop/src-tauri/src/managed_agents/personas.rs index 55899f74..e7bb0318 100644 --- a/desktop/src-tauri/src/managed_agents/personas.rs +++ b/desktop/src-tauri/src/managed_agents/personas.rs @@ -15,6 +15,7 @@ struct BuiltInPersona { system_prompt: &'static str, name_pool: &'static [&'static str], model: Option<&'static str>, + provider: Option<&'static str>, } const SOLO_AVATAR: &str = "data:image/png;base64,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"; @@ -147,6 +148,7 @@ const BUILT_IN_PERSONAS: &[BuiltInPersona] = &[ "Birch", "Dune", "Fern", "Moss", "Reed", "Slate", "Ash", "Brook", "Glen", "Stone", ], model: Some("claude-sonnet-4-20250514"), + provider: Some("claude"), }, BuiltInPersona { id: "builtin:kit", @@ -295,6 +297,7 @@ Don't present work that doesn't meet this bar. No emojis. Your name is Kit. You are friendly and helpful. You are understated, but have a sense of humor."#, model: Some("claude-sonnet-4-20250514"), + provider: Some("claude"), }, BuiltInPersona { id: "builtin:scout", @@ -454,6 +457,7 @@ If you're invoked outside a Sprout team channel (or by an agent that isn't Kit), Your name is Scout. You are friendly and helpful. You are understated, but have a sense of humor."#, model: Some("claude-sonnet-4-20250514"), + provider: Some("claude"), }, ]; @@ -469,7 +473,7 @@ fn built_in_persona_records(now: &str) -> Vec { display_name: persona.display_name.to_string(), avatar_url: persona.avatar_url.map(|s| s.to_string()), system_prompt: persona.system_prompt.to_string(), - provider: None, + provider: persona.provider.map(|s| s.to_string()), model: persona.model.map(|s| s.to_string()), name_pool: persona.name_pool.iter().map(|s| s.to_string()).collect(), is_builtin: true, @@ -519,6 +523,9 @@ fn merge_personas(mut stored: Vec, now: &str) -> (Vec Date: Thu, 14 May 2026 21:05:10 -0400 Subject: [PATCH 03/10] fix: CLI-first base prompt and remove hardcoded model from built-ins MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit base_prompt.md now leads with the sprout CLI (12 subcommand groups from #585) rather than MCP tools, which are still available but secondary. Adds the @mention formatting rule (no bold/italic), get_feed type filtering, and a pointer to nest_agents.md for workspace conventions. Reverts the hardcoded model/provider on Solo, Kit, and Scout back to None. The agent binary's own default is the right choice here — pinning claude-sonnet-4-20250514 is opinionated and will go stale. Users who want a specific model can set it per-agent via the model picker. --- crates/sprout-acp/src/base_prompt.md | 61 ++++++++++++------- .../src-tauri/src/managed_agents/personas.rs | 12 ++-- 2 files changed, 45 insertions(+), 28 deletions(-) diff --git a/crates/sprout-acp/src/base_prompt.md b/crates/sprout-acp/src/base_prompt.md index b9a0a490..5f82e213 100644 --- a/crates/sprout-acp/src/base_prompt.md +++ b/crates/sprout-acp/src/base_prompt.md @@ -1,35 +1,52 @@ -You are operating inside the Sprout platform — a Nostr-based messaging platform for human-agent collaboration. The sprout-acp harness bridges channel events to your session. +You are operating inside the Sprout platform — a Nostr-based messaging platform for human-agent collaboration. The sprout-acp harness routes channel events to your session. -## MCP Tools (via `sprout-mcp`) +## Sprout CLI -- `get_messages(channel_id, limit=50)` — fetch recent history (max 200 per call) -- `get_messages(channel_id, since=)` — fetch messages since timestamp; returns oldest-first when `since` is set without `before` -- `get_thread(channel_id, event_id)` — fetch a full thread by root event ID -- `get_feed()` — personalized feed of mentions and needs-action items across all channels -- `send_message(channel_id, content)` — post a new message to a channel -- `send_message(channel_id, content, parent_event_id)` — reply within an existing thread -- `search(q="your query")` — cross-channel full-text search +The `sprout` CLI is your primary interface. Auth env vars: `SPROUT_RELAY_URL`, `SPROUT_PRIVATE_KEY`, `SPROUT_AUTH_TAG`. Exit codes: 0 ok, 1 user error, 2 network, 3 auth, 4 other. Output is structured JSON — pipe through `jq` as needed. + +| Group | Key commands | +|-------|-------------| +| `sprout messages` | `send`, `get`, `thread`, `search` | +| `sprout channels` | `list`, `get`, `create`, `join`, `members` | +| `sprout canvas` | `get`, `set` | +| `sprout reactions` | `add`, `remove` | +| `sprout dms` | `list`, `open` | +| `sprout users` | `get`, `set-profile`, `presence` | +| `sprout workflows` | `list`, `trigger`, `runs` | +| `sprout feed` | `get` | +| `sprout social` | `publish`, `notes` | +| `sprout repos` | `create`, `get`, `list` | +| `sprout upload` | `file` | + +Run `sprout --help` or `sprout --help` for full usage. + +MCP tools (via `sprout-mcp`) are also available but the CLI is preferred for batch operations and scripting. ## Communication Patterns -- Address agents and humans with `@name` in message content. -- Use `parent_event_id` when responding to a thread; post a new message for new topics. -- There are no push notifications — poll for new messages using `since=`. +- Address agents and humans with plain `@name` — do NOT bold or italicize mention text (formatting prevents alert delivery). +- Use `sprout messages thread` or MCP `get_thread()` when responding in-thread; post new messages for new topics. +- No push notifications — poll with `sprout messages get --since=` or MCP `get_messages(since=)`. When `since` is set without `before`, results are oldest-first (chronological). ## Startup Recovery -On startup or after a gap: call `get_feed()` first to surface pending mentions and action items, then call `get_messages` on your assigned channels to catch up, then check `AGENTS.md` for team context. Use `search()` for cross-channel keyword lookups when you need to find specific prior discussions. +1. `sprout feed get` (or MCP `get_feed()`) — surface pending mentions and action items. Filter by type: `mentions`, `needs_action`, `activity`, `agent_activity`. +2. `sprout messages get ` on assigned channels — catch up on recent history. +3. Check `AGENTS.md` in your working directory for team context. +4. Check `RESEARCH/`, `GUIDES/`, `PLANS/` before searching externally. Use `sprout messages search --query "..."` for cross-channel keyword lookups. ## Workspace Layout -Your persistent workspace is in your working directory, with the following subdirectories: +Your persistent workspace is in your working directory: -- `RESEARCH/` — findings and reference material -- `PLANS/` — project and task plans -- `GUIDES/` — how-to documentation -- `WORK_LOGS/` — timestamped activity logs -- `OUTBOX/` — drafts pending review or send -- `REPOS/` — checked-out source repositories -- `.scratch/` — ephemeral working files +| Dir | Purpose | +|-----|---------| +| `RESEARCH/` | Findings and reference material | +| `PLANS/` | Project and task plans | +| `GUIDES/` | How-to documentation | +| `WORK_LOGS/` | Timestamped activity logs | +| `OUTBOX/` | Drafts pending review or send | +| `REPOS/` | Checked-out source repositories | +| `.scratch/` | Ephemeral working files | -Knowledge files use `ALL_CAPS_WITH_UNDERSCORES.md` naming and YAML frontmatter. `AGENTS.md` in the working directory lists active agents and their assigned roles. +Knowledge files use `ALL_CAPS_WITH_UNDERSCORES.md` naming. `AGENTS.md` lists active agents and roles. See `nest_agents.md` in your working directory for full workspace conventions. diff --git a/desktop/src-tauri/src/managed_agents/personas.rs b/desktop/src-tauri/src/managed_agents/personas.rs index e7bb0318..2bd74c3b 100644 --- a/desktop/src-tauri/src/managed_agents/personas.rs +++ b/desktop/src-tauri/src/managed_agents/personas.rs @@ -147,8 +147,8 @@ const BUILT_IN_PERSONAS: &[BuiltInPersona] = &[ "Atlas", "Ember", "Flint", "Sage", "Drift", "Quill", "Wren", "Cedar", "Pike", "Lark", "Birch", "Dune", "Fern", "Moss", "Reed", "Slate", "Ash", "Brook", "Glen", "Stone", ], - model: Some("claude-sonnet-4-20250514"), - provider: Some("claude"), + model: None, + provider: None, }, BuiltInPersona { id: "builtin:kit", @@ -296,8 +296,8 @@ Aim for 9/10+ on the first pass. There is no separate refactoring pass — if it Don't present work that doesn't meet this bar. No emojis. Your name is Kit. You are friendly and helpful. You are understated, but have a sense of humor."#, - model: Some("claude-sonnet-4-20250514"), - provider: Some("claude"), + model: None, + provider: None, }, BuiltInPersona { id: "builtin:scout", @@ -456,8 +456,8 @@ You are read-only, but you still resolve questions yourself before pinging Kit: If you're invoked outside a Sprout team channel (or by an agent that isn't Kit), apply the same protocols. Default to FULL REVIEW for completed work or PLAN REVIEW + RESEARCH for plans. Report to whoever invoked you. Your name is Scout. You are friendly and helpful. You are understated, but have a sense of humor."#, - model: Some("claude-sonnet-4-20250514"), - provider: Some("claude"), + model: None, + provider: None, }, ]; From 880703132fadfdd28baf949ddda209a5ff2294d7 Mon Sep 17 00:00:00 2001 From: Will Pfleger Date: Thu, 14 May 2026 14:48:33 -0400 Subject: [PATCH 04/10] feat(desktop): dynamic nest AGENTS.md regeneration AGENTS.md in ~/.sprout is now dynamically regenerated whenever personas, agents, or workspace config changes. Agents discover their teammates on every fresh session via a managed section demarcated by HTML comment markers. User edits outside the markers are preserved across regenerations. --- .../src-tauri/src/commands/agent_models.rs | 9 +- desktop/src-tauri/src/commands/agents.rs | 23 +- desktop/src-tauri/src/commands/personas.rs | 26 +- desktop/src-tauri/src/commands/workspace.rs | 10 +- desktop/src-tauri/src/lib.rs | 6 +- desktop/src-tauri/src/managed_agents/nest.rs | 244 ++++++++++++++++++ .../src/managed_agents/nest_agents.md | 7 + 7 files changed, 311 insertions(+), 14 deletions(-) diff --git a/desktop/src-tauri/src/commands/agent_models.rs b/desktop/src-tauri/src/commands/agent_models.rs index 55b6207b..50986b4d 100644 --- a/desktop/src-tauri/src/commands/agent_models.rs +++ b/desktop/src-tauri/src/commands/agent_models.rs @@ -8,8 +8,9 @@ use crate::{ managed_agents::{ build_managed_agent_summary, default_agent_workdir, find_managed_agent_mut, load_managed_agents, managed_agent_avatar_url, missing_command_message, - normalize_agent_args, resolve_command, save_managed_agents, sync_managed_agent_processes, - AgentModelInfo, AgentModelsResponse, UpdateManagedAgentRequest, UpdateManagedAgentResponse, + normalize_agent_args, regenerate_nest_context, resolve_command, save_managed_agents, + sync_managed_agent_processes, AgentModelInfo, AgentModelsResponse, + UpdateManagedAgentRequest, UpdateManagedAgentResponse, }, relay::{relay_ws_url_with_override, sync_managed_agent_profile}, util::now_iso, @@ -246,6 +247,10 @@ pub async fn update_managed_agent( (summary, sync_params) }; // lock dropped here + if let Err(error) = regenerate_nest_context(&app) { + eprintln!("sprout-desktop: nest context regeneration failed: {error}"); + } + // Phase 2: relay profile sync (async, best-effort, outside lock) let profile_sync_error = if let Some((agent_keys, relay_url, display_name, avatar_url, auth_tag)) = sync_params { diff --git a/desktop/src-tauri/src/commands/agents.rs b/desktop/src-tauri/src/commands/agents.rs index 69b0e373..20111b7c 100644 --- a/desktop/src-tauri/src/commands/agents.rs +++ b/desktop/src-tauri/src/commands/agents.rs @@ -8,12 +8,13 @@ use crate::{ build_managed_agent_summary, discover_provider_candidates, ensure_persona_is_active, find_managed_agent_mut, invoke_provider, load_managed_agents, load_personas, managed_agent_avatar_url, managed_agent_log_path, managed_agents_base_dir, - normalize_agent_args, provider_deploy, read_log_tail, resolve_provider_binary, - save_managed_agents, start_managed_agent_process, stop_managed_agent_process, - sync_managed_agent_processes, validate_provider_config, BackendKind, BackendProviderInfo, - CreateManagedAgentRequest, CreateManagedAgentResponse, ManagedAgentLogResponse, - ManagedAgentRecord, ManagedAgentSummary, DEFAULT_ACP_COMMAND, DEFAULT_AGENT_COMMAND, - DEFAULT_AGENT_PARALLELISM, DEFAULT_AGENT_TURN_TIMEOUT_SECONDS, DEFAULT_MCP_COMMAND, + normalize_agent_args, provider_deploy, read_log_tail, regenerate_nest_context, + resolve_provider_binary, save_managed_agents, start_managed_agent_process, + stop_managed_agent_process, sync_managed_agent_processes, validate_provider_config, + BackendKind, BackendProviderInfo, CreateManagedAgentRequest, CreateManagedAgentResponse, + ManagedAgentLogResponse, ManagedAgentRecord, ManagedAgentSummary, DEFAULT_ACP_COMMAND, + DEFAULT_AGENT_COMMAND, DEFAULT_AGENT_PARALLELISM, DEFAULT_AGENT_TURN_TIMEOUT_SECONDS, + DEFAULT_MCP_COMMAND, }, relay::{relay_ws_url_with_override, sync_managed_agent_profile}, util::now_iso, @@ -453,6 +454,10 @@ pub async fn create_managed_agent( (agent, spawn_error) }; + if let Err(error) = regenerate_nest_context(&app) { + eprintln!("sprout-desktop: nest context regeneration failed: {error}"); + } + // ── Phase 4: sync agent profile on relay (async, outside lock) ─────────── let avatar_url = input .avatar_url @@ -714,7 +719,11 @@ pub fn delete_managed_agent( if records.len() == initial_len { return Err(format!("agent {pubkey} not found")); } - save_managed_agents(&app, &records) + save_managed_agents(&app, &records)?; + if let Err(error) = regenerate_nest_context(&app) { + eprintln!("sprout-desktop: nest context regeneration failed: {error}"); + } + Ok(()) } #[tauri::command] diff --git a/desktop/src-tauri/src/commands/personas.rs b/desktop/src-tauri/src/commands/personas.rs index f5ece7f0..53124299 100644 --- a/desktop/src-tauri/src/commands/personas.rs +++ b/desktop/src-tauri/src/commands/personas.rs @@ -7,7 +7,7 @@ use crate::{ managed_agents::{ encode_persona_json, import_persona_pack, list_installed_packs, load_managed_agents, load_personas, load_teams, parse_json_persona, parse_md_persona, parse_png_persona, - parse_zip_personas, save_managed_agents, save_personas, + parse_zip_personas, regenerate_nest_context, save_managed_agents, save_personas, uninstall_persona_pack as do_uninstall_persona_pack, validate_persona_activation_change, validate_persona_deletion, CreatePersonaRequest, PackSummary, ParsePersonaFilesResult, PersonaRecord, UpdatePersonaRequest, @@ -85,6 +85,9 @@ pub fn create_persona( }; personas.push(persona.clone()); save_personas(&app, &personas)?; + if let Err(error) = regenerate_nest_context(&app) { + eprintln!("sprout-desktop: nest context regeneration failed: {error}"); + } Ok(persona) } @@ -134,6 +137,9 @@ pub fn update_persona( persona.updated_at = now_iso(); save_personas(&app, &personas)?; + if let Err(error) = regenerate_nest_context(&app) { + eprintln!("sprout-desktop: nest context regeneration failed: {error}"); + } personas .into_iter() .find(|record| record.id == input.id) @@ -182,6 +188,9 @@ pub fn delete_persona( if changed_agents { save_managed_agents(&app, &agents)?; } + if let Err(error) = regenerate_nest_context(&app) { + eprintln!("sprout-desktop: nest context regeneration failed: {error}"); + } Ok(()) } @@ -230,6 +239,9 @@ pub fn set_persona_active( let updated = persona.clone(); save_personas(&app, &personas)?; + if let Err(error) = regenerate_nest_context(&app) { + eprintln!("sprout-desktop: nest context regeneration failed: {error}"); + } Ok(updated) } @@ -383,7 +395,11 @@ pub fn install_persona_pack( if !source.is_dir() { return Err(format!("pack path is not a directory: {path}")); } - import_persona_pack(&app, &source) + let result = import_persona_pack(&app, &source)?; + if let Err(error) = regenerate_nest_context(&app) { + eprintln!("sprout-desktop: nest context regeneration failed: {error}"); + } + Ok(result) } #[tauri::command] @@ -396,7 +412,11 @@ pub fn uninstall_persona_pack( .managed_agents_store_lock .lock() .map_err(|e| e.to_string())?; - do_uninstall_persona_pack(&app, &pack_id) + do_uninstall_persona_pack(&app, &pack_id)?; + if let Err(error) = regenerate_nest_context(&app) { + eprintln!("sprout-desktop: nest context regeneration failed: {error}"); + } + Ok(()) } #[tauri::command] diff --git a/desktop/src-tauri/src/commands/workspace.rs b/desktop/src-tauri/src/commands/workspace.rs index e75d1801..615dd232 100644 --- a/desktop/src-tauri/src/commands/workspace.rs +++ b/desktop/src-tauri/src/commands/workspace.rs @@ -1,8 +1,9 @@ use nostr::Keys; use serde::Serialize; -use tauri::State; +use tauri::{AppHandle, State}; use crate::app_state::AppState; +use crate::managed_agents::regenerate_nest_context; use crate::relay; #[derive(Serialize)] @@ -30,6 +31,7 @@ pub fn get_active_workspace(state: State<'_, AppState>) -> Result, + app: AppHandle, state: State<'_, AppState>, ) -> Result<(), String> { // ── Validate before mutating ────────────────────────────────────────── @@ -51,5 +53,11 @@ pub fn apply_workspace( *keys_guard = keys; } + if let Err(error) = regenerate_nest_context(&app) { + eprintln!( + "sprout-desktop: failed to regenerate nest context after workspace switch: {error}" + ); + } + Ok(()) } diff --git a/desktop/src-tauri/src/lib.rs b/desktop/src-tauri/src/lib.rs index 0443243a..375aaa4c 100644 --- a/desktop/src-tauri/src/lib.rs +++ b/desktop/src-tauri/src/lib.rs @@ -24,7 +24,7 @@ use huddle::{ speak_agent_message, start_huddle, start_stt_pipeline, }; use managed_agents::{ - ensure_nest, kill_stale_tracked_processes, load_managed_agents, + ensure_nest, kill_stale_tracked_processes, load_managed_agents, regenerate_nest_context, restore_managed_agents_on_launch, save_managed_agents, sync_managed_agent_processes, BackendKind, ManagedAgentProcess, }; @@ -393,6 +393,10 @@ pub fn run() { eprintln!("sprout-desktop: failed to create nest: {error}"); } + if let Err(error) = regenerate_nest_context(&app_handle) { + eprintln!("sprout-desktop: failed to regenerate nest context: {error}"); + } + // Pre-download voice models in the background so they're ready // when the user starts their first huddle. Idempotent — no-op if // already downloaded. ~289 MB total (~100 MB Parakeet STT + ~189 MB Pocket TTS). diff --git a/desktop/src-tauri/src/managed_agents/nest.rs b/desktop/src-tauri/src/managed_agents/nest.rs index b9e9f119..958e0c4b 100644 --- a/desktop/src-tauri/src/managed_agents/nest.rs +++ b/desktop/src-tauri/src/managed_agents/nest.rs @@ -6,8 +6,15 @@ //! //! Idempotent: existing files and directories are never overwritten. +use super::{ + load_managed_agents, load_personas, BackendKind, ManagedAgentRecord, PersonaRecord, RespondTo, +}; +use crate::app_state::AppState; +use crate::relay::relay_ws_url_with_override; use std::fs; +use std::io; use std::path::{Path, PathBuf}; +use tauri::{AppHandle, Manager}; /// Subdirectories created inside the nest. const NEST_DIRS: &[&str] = &[ @@ -122,6 +129,106 @@ pub fn ensure_nest_at(root: &Path) -> Result<(), String> { Ok(()) } +const CLI_QUICK_REFERENCE: &str = "\ +## CLI Quick Reference +`sprout messages send --channel --content ` — send a message +`sprout messages get --channel ` — read recent messages +`sprout channels list` — list available channels +`sprout workflows trigger --workflow ` — trigger a workflow +Run `sprout --help` for the full command reference."; + +pub fn render_dynamic_section( + personas: &[PersonaRecord], + agents: &[ManagedAgentRecord], + relay_url: &str, +) -> String { + let active_agents = if agents.is_empty() { + "## Active Agents\n\n*(No agents deployed yet. Add agents in the Sprout desktop app.)*" + .to_string() + } else { + let mut table = + "## Active Agents\n\n| Name | Role | How to address |\n|------|------|----------------|" + .to_string(); + for agent in agents { + let role = agent + .persona_id + .as_deref() + .and_then(|pid| personas.iter().find(|p| p.id == pid)) + .map(|p| p.display_name.as_str()) + .unwrap_or("—"); + table.push_str(&format!( + "\n| {} | {} | @{} |", + agent.name, role, agent.name + )); + } + table + }; + + format!("{active_agents}\n\n## Workspace\n- Relay: {relay_url}\n\n{CLI_QUICK_REFERENCE}") +} + +pub fn upsert_managed_section(file_path: &Path, new_section_content: &str) -> io::Result<()> { + let current = fs::read_to_string(file_path)?; + + const BEGIN: &str = ""; + + let replacement = format!( + "\n{new_section_content}\n\n" + ); + + let new_content = + if let (Some(begin_pos), Some(end_pos)) = (current.find(BEGIN), current.find(END)) { + // Find the start of the BEGIN marker's line. + let line_start = current[..begin_pos].rfind('\n').map(|p| p + 1).unwrap_or(0); + // END marker spans to the end of its content + the newline after it. + let end_of_end = end_pos + END.len(); + let after_end = if current[end_of_end..].starts_with('\n') { + end_of_end + 1 + } else { + end_of_end + }; + format!( + "{}{}{}", + ¤t[..line_start], + replacement, + ¤t[after_end..] + ) + } else { + format!("{}\n\n{}", current.trim_end_matches('\n'), replacement) + }; + + let tmp_path = file_path.with_extension( + file_path + .extension() + .map(|e| format!("{}.tmp", e.to_string_lossy())) + .unwrap_or_else(|| "tmp".to_string()), + ); + fs::write(&tmp_path, new_content)?; + fs::rename(&tmp_path, file_path)?; + + Ok(()) +} + +pub fn regenerate_nest_context(app: &AppHandle) -> Result<(), String> { + let nest = nest_dir().ok_or("cannot resolve home directory for nest")?; + let agents_md = nest.join("AGENTS.md"); + + if !agents_md.exists() { + return Ok(()); + } + + let personas = load_personas(app)?; + let agents = load_managed_agents(app)?; + let state = app.state::(); + let relay_url = relay_ws_url_with_override(&state); + let content = render_dynamic_section(&personas, &agents, &relay_url); + upsert_managed_section(&agents_md, &content) + .map_err(|e| format!("regenerate nest context: {e}"))?; + + Ok(()) +} + #[cfg(test)] mod tests { use super::*; @@ -230,4 +337,141 @@ mod tests { "symlinked child's target should not be chmod'd" ); } + + fn make_persona(id: &str, display_name: &str) -> PersonaRecord { + PersonaRecord { + id: id.to_string(), + display_name: display_name.to_string(), + avatar_url: None, + system_prompt: String::new(), + provider: None, + model: None, + name_pool: vec![], + is_builtin: false, + is_active: true, + source_pack: None, + source_pack_persona_slug: None, + created_at: String::new(), + updated_at: String::new(), + } + } + + fn make_agent(name: &str, persona_id: Option<&str>) -> ManagedAgentRecord { + ManagedAgentRecord { + pubkey: String::new(), + name: name.to_string(), + persona_id: persona_id.map(|s| s.to_string()), + private_key_nsec: String::new(), + auth_tag: None, + relay_url: String::new(), + acp_command: String::new(), + agent_command: String::new(), + agent_args: vec![], + mcp_command: String::new(), + turn_timeout_seconds: 0, + idle_timeout_seconds: None, + max_turn_duration_seconds: None, + parallelism: 1, + system_prompt: None, + model: None, + mcp_toolsets: None, + start_on_app_launch: false, + runtime_pid: None, + backend: BackendKind::default(), + backend_agent_id: None, + provider_binary_path: None, + persona_pack_path: None, + persona_name_in_pack: None, + created_at: String::new(), + updated_at: String::new(), + last_started_at: None, + last_stopped_at: None, + last_exit_code: None, + last_error: None, + respond_to: RespondTo::default(), + respond_to_allowlist: vec![], + } + } + + #[test] + fn test_render_dynamic_section_with_agents() { + let personas = vec![make_persona("p1", "Builder")]; + let agents = vec![make_agent("Kit", Some("p1"))]; + let output = render_dynamic_section(&personas, &agents, "ws://example.com:3000"); + assert!(output.contains("| Kit | Builder | @Kit |")); + assert!(output.contains("## CLI Quick Reference")); + } + + #[test] + fn test_render_dynamic_section_empty() { + let output = render_dynamic_section(&[], &[], "ws://example.com:3000"); + assert!(output.contains("No agents deployed yet")); + } + + #[test] + fn test_render_dynamic_section_agent_no_persona() { + let personas = vec![make_persona("p1", "Builder")]; + let agents = vec![make_agent("Scout", Some("nonexistent"))]; + let output = render_dynamic_section(&personas, &agents, "ws://example.com:3000"); + assert!(output.contains("| Scout | — | @Scout |")); + } + + #[test] + fn test_upsert_managed_section_with_markers() { + let tmp = tempfile::tempdir().unwrap(); + let file = tmp.path().join("AGENTS.md"); + fs::write( + &file, + "# Header\n\nsome content\n\n\nold section\n\n\nafter\n", + ) + .unwrap(); + + upsert_managed_section(&file, "new section").unwrap(); + + let result = fs::read_to_string(&file).unwrap(); + assert!(result.contains("")); + assert!(result.contains("new section")); + assert!(!result.contains("old section")); + assert!(result.contains("# Header")); + assert!(result.contains("some content")); + assert!(result.contains("after")); + } + + #[test] + fn test_upsert_managed_section_without_markers() { + let tmp = tempfile::tempdir().unwrap(); + let file = tmp.path().join("AGENTS.md"); + fs::write(&file, "# Header\n\nexisting content\n").unwrap(); + + upsert_managed_section(&file, "injected section").unwrap(); + + let result = fs::read_to_string(&file).unwrap(); + assert!(result.contains("# Header")); + assert!(result.contains("existing content")); + assert!(result.contains("")); + assert!(result.contains("injected section")); + let begin_pos = result.find(" +## Active Agents + +*(No agents deployed yet. Add agents in the Sprout desktop app.)* + + From 1ab077a726f2b5986faaaa3171de7af73e0a507f Mon Sep 17 00:00:00 2001 From: Will Pfleger Date: Thu, 14 May 2026 14:50:35 -0400 Subject: [PATCH 05/10] fix: gate test-only imports behind #[cfg(test)] in nest.rs BackendKind and RespondTo are only used in test helper constructors. --- desktop/src-tauri/src/managed_agents/nest.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/desktop/src-tauri/src/managed_agents/nest.rs b/desktop/src-tauri/src/managed_agents/nest.rs index 958e0c4b..b0314da9 100644 --- a/desktop/src-tauri/src/managed_agents/nest.rs +++ b/desktop/src-tauri/src/managed_agents/nest.rs @@ -6,9 +6,9 @@ //! //! Idempotent: existing files and directories are never overwritten. -use super::{ - load_managed_agents, load_personas, BackendKind, ManagedAgentRecord, PersonaRecord, RespondTo, -}; +use super::{load_managed_agents, load_personas, ManagedAgentRecord, PersonaRecord}; +#[cfg(test)] +use super::{BackendKind, RespondTo}; use crate::app_state::AppState; use crate::relay::relay_ws_url_with_override; use std::fs; From 402d045f08c5f8f7bc5b0d39ac6bc80df6bff909 Mon Sep 17 00:00:00 2001 From: Will Pfleger Date: Thu, 14 May 2026 16:23:15 -0400 Subject: [PATCH 06/10] fix(desktop): address review findings for nest AGENTS.md regeneration Fixes identified by crossfire review (Codex + Gemini) and plan author: - Use tempfile::NamedTempFile for atomic writes instead of deterministic .tmp path that races under concurrent regeneration triggers - Enforce ordered BEGIN/END marker search with line-start anchoring to prevent inverted slicing when markers are out of order or mid-line - Strip orphan BEGIN markers before appending new managed section - Escape pipe and newline characters in agent/persona names to prevent Markdown table corruption - Rename "Role" column to "Persona" (display_name is a name, not a role) - Move regenerate_nest_context calls outside lock scope in all mutation hooks to reduce lock hold time and eliminate future deadlock risk - Add 7 adversarial unit tests: marker ordering, orphan cleanup, duplicates, code-block false positives, pipe/newline escaping, idempotency --- desktop/src-tauri/src/commands/agents.rs | 76 ++--- desktop/src-tauri/src/commands/personas.rs | 12 +- desktop/src-tauri/src/managed_agents/nest.rs | 318 ++++++++++++++++--- 3 files changed, 326 insertions(+), 80 deletions(-) diff --git a/desktop/src-tauri/src/commands/agents.rs b/desktop/src-tauri/src/commands/agents.rs index 20111b7c..a81bc1e9 100644 --- a/desktop/src-tauri/src/commands/agents.rs +++ b/desktop/src-tauri/src/commands/agents.rs @@ -678,48 +678,50 @@ pub fn delete_managed_agent( app: AppHandle, state: State<'_, AppState>, ) -> Result<(), String> { - let _store_guard = state - .managed_agents_store_lock - .lock() - .map_err(|error| error.to_string())?; - let mut records = load_managed_agents(&app)?; - let mut runtimes = state - .managed_agent_processes - .lock() - .map_err(|error| error.to_string())?; + { + let _store_guard = state + .managed_agents_store_lock + .lock() + .map_err(|error| error.to_string())?; + let mut records = load_managed_agents(&app)?; + let mut runtimes = state + .managed_agent_processes + .lock() + .map_err(|error| error.to_string())?; - if sync_managed_agent_processes(&mut records, &mut runtimes) { - save_managed_agents(&app, &records)?; - } + if sync_managed_agent_processes(&mut records, &mut runtimes) { + save_managed_agents(&app, &records)?; + } - // Guard: reject deletion of deployed remote agents unless explicitly forced. - // This turns "don't orphan remote infra" from a UI convention into a backend - // invariant — a buggy or compromised IPC caller cannot silently orphan a live - // remote deployment. The frontend sends force_remote_delete: true only after - // the user confirms the orphan warning. - if let Some(record) = records.iter().find(|r| r.pubkey == pubkey) { - if record.backend != BackendKind::Local - && record.backend_agent_id.is_some() - && !force_remote_delete.unwrap_or(false) - { - return Err( - "cannot delete a deployed remote agent without force_remote_delete: true" - .to_string(), - ); + // Guard: reject deletion of deployed remote agents unless explicitly forced. + // This turns "don't orphan remote infra" from a UI convention into a backend + // invariant — a buggy or compromised IPC caller cannot silently orphan a live + // remote deployment. The frontend sends force_remote_delete: true only after + // the user confirms the orphan warning. + if let Some(record) = records.iter().find(|r| r.pubkey == pubkey) { + if record.backend != BackendKind::Local + && record.backend_agent_id.is_some() + && !force_remote_delete.unwrap_or(false) + { + return Err( + "cannot delete a deployed remote agent without force_remote_delete: true" + .to_string(), + ); + } } - } - if let Some(record) = records.iter_mut().find(|record| record.pubkey == pubkey) { - // For local agents: kills the process. For remote agents: no-op (the frontend - // sends !shutdown via WebSocket before calling delete). Either way, safe. - stop_managed_agent_process(&app, record, &mut runtimes)?; - } - let initial_len = records.len(); - records.retain(|record| record.pubkey != pubkey); - if records.len() == initial_len { - return Err(format!("agent {pubkey} not found")); + if let Some(record) = records.iter_mut().find(|record| record.pubkey == pubkey) { + // For local agents: kills the process. For remote agents: no-op (the frontend + // sends !shutdown via WebSocket before calling delete). Either way, safe. + stop_managed_agent_process(&app, record, &mut runtimes)?; + } + let initial_len = records.len(); + records.retain(|record| record.pubkey != pubkey); + if records.len() == initial_len { + return Err(format!("agent {pubkey} not found")); + } + save_managed_agents(&app, &records)?; } - save_managed_agents(&app, &records)?; if let Err(error) = regenerate_nest_context(&app) { eprintln!("sprout-desktop: nest context regeneration failed: {error}"); } diff --git a/desktop/src-tauri/src/commands/personas.rs b/desktop/src-tauri/src/commands/personas.rs index 53124299..39db8a98 100644 --- a/desktop/src-tauri/src/commands/personas.rs +++ b/desktop/src-tauri/src/commands/personas.rs @@ -128,22 +128,20 @@ pub fn update_persona( .filter(|s| !s.is_empty()) .collect(); if let Some(env_vars) = input.env_vars { - // Caller explicitly sent env_vars — replace entirely (empty = clear). crate::managed_agents::validate_user_env_keys(&env_vars)?; persona.env_vars = env_vars; } - // Absent env_vars means "don't touch" — preserve existing creds when - // the caller only meant to edit a different field. persona.updated_at = now_iso(); save_personas(&app, &personas)?; + let result = personas + .into_iter() + .find(|record| record.id == input.id) + .ok_or_else(|| format!("persona {} disappeared unexpectedly", input.id))?; if let Err(error) = regenerate_nest_context(&app) { eprintln!("sprout-desktop: nest context regeneration failed: {error}"); } - personas - .into_iter() - .find(|record| record.id == input.id) - .ok_or_else(|| format!("persona {} disappeared unexpectedly", input.id)) + Ok(result) } #[tauri::command] diff --git a/desktop/src-tauri/src/managed_agents/nest.rs b/desktop/src-tauri/src/managed_agents/nest.rs index b0314da9..fc40f3a7 100644 --- a/desktop/src-tauri/src/managed_agents/nest.rs +++ b/desktop/src-tauri/src/managed_agents/nest.rs @@ -31,6 +31,9 @@ const NEST_DIRS: &[&str] = &[ /// Fully static — no runtime interpolation, no secrets, no user paths. const AGENTS_MD: &str = include_str!("nest_agents.md"); +const BEGIN_MARKER: &str = ""; + /// Returns the nest root path (`~/.sprout`), or `None` if the home /// directory cannot be resolved. pub fn nest_dir() -> Option { @@ -137,6 +140,10 @@ const CLI_QUICK_REFERENCE: &str = "\ `sprout workflows trigger --workflow ` — trigger a workflow Run `sprout --help` for the full command reference."; +fn escape_md_cell(s: &str) -> String { + s.replace('|', "\\|").replace('\n', " ") +} + pub fn render_dynamic_section( personas: &[PersonaRecord], agents: &[ManagedAgentRecord], @@ -147,7 +154,7 @@ pub fn render_dynamic_section( .to_string() } else { let mut table = - "## Active Agents\n\n| Name | Role | How to address |\n|------|------|----------------|" + "## Active Agents\n\n| Name | Persona | How to address |\n|------|---------|----------------|" .to_string(); for agent in agents { let role = agent @@ -156,10 +163,9 @@ pub fn render_dynamic_section( .and_then(|pid| personas.iter().find(|p| p.id == pid)) .map(|p| p.display_name.as_str()) .unwrap_or("—"); - table.push_str(&format!( - "\n| {} | {} | @{} |", - agent.name, role, agent.name - )); + let name = escape_md_cell(&agent.name); + let role_escaped = escape_md_cell(role); + table.push_str(&format!("\n| {name} | {role_escaped} | @{name} |")); } table }; @@ -167,45 +173,80 @@ pub fn render_dynamic_section( format!("{active_agents}\n\n## Workspace\n- Relay: {relay_url}\n\n{CLI_QUICK_REFERENCE}") } +/// Find a marker that appears at the start of a line (position 0 or preceded by `\n`). +fn find_marker_at_line_start(content: &str, marker: &str) -> Option { + let mut search_from = 0; + while let Some(pos) = content[search_from..].find(marker) { + let abs_pos = search_from + pos; + if abs_pos == 0 || content.as_bytes()[abs_pos - 1] == b'\n' { + return Some(abs_pos); + } + search_from = abs_pos + 1; + } + None +} + +/// Find the first valid ordered BEGIN/END marker pair, both at line starts. +/// Returns `(begin_line_start, after_end)` byte offsets for slicing. +fn find_managed_markers(content: &str) -> Option<(usize, usize)> { + let begin_pos = find_marker_at_line_start(content, BEGIN_MARKER)?; + let begin_line_start = content[..begin_pos].rfind('\n').map(|p| p + 1).unwrap_or(0); + let end_pos = content[begin_pos..].find(END_MARKER).map(|p| p + begin_pos)?; + let end_of_end = end_pos + END_MARKER.len(); + let after_end = if content[end_of_end..].starts_with('\n') { + end_of_end + 1 + } else { + end_of_end + }; + Some((begin_line_start, after_end)) +} + +/// Remove an orphan BEGIN marker line (one with no matching END after it). +fn strip_orphan_begin_marker(content: &str) -> String { + if let Some(pos) = find_marker_at_line_start(content, BEGIN_MARKER) { + let line_start = content[..pos].rfind('\n').map(|p| p + 1).unwrap_or(0); + let line_end = content[pos..].find('\n').map(|p| pos + p + 1).unwrap_or(content.len()); + format!( + "{}{}", + &content[..line_start], + content[line_end..].trim_start_matches('\n') + ) + } else { + content.to_string() + } +} + pub fn upsert_managed_section(file_path: &Path, new_section_content: &str) -> io::Result<()> { let current = fs::read_to_string(file_path)?; - const BEGIN: &str = ""; - let replacement = format!( - "\n{new_section_content}\n\n" + "{BEGIN_MARKER} — regenerated automatically, do not edit below -->\n{new_section_content}\n{END_MARKER}\n" ); - let new_content = - if let (Some(begin_pos), Some(end_pos)) = (current.find(BEGIN), current.find(END)) { - // Find the start of the BEGIN marker's line. - let line_start = current[..begin_pos].rfind('\n').map(|p| p + 1).unwrap_or(0); - // END marker spans to the end of its content + the newline after it. - let end_of_end = end_pos + END.len(); - let after_end = if current[end_of_end..].starts_with('\n') { - end_of_end + 1 - } else { - end_of_end - }; + let new_content = match find_managed_markers(¤t) { + Some((begin_line_start, after_end)) => { format!( "{}{}{}", - ¤t[..line_start], + ¤t[..begin_line_start], replacement, ¤t[after_end..] ) - } else { - format!("{}\n\n{}", current.trim_end_matches('\n'), replacement) - }; - - let tmp_path = file_path.with_extension( - file_path - .extension() - .map(|e| format!("{}.tmp", e.to_string_lossy())) - .unwrap_or_else(|| "tmp".to_string()), - ); - fs::write(&tmp_path, new_content)?; - fs::rename(&tmp_path, file_path)?; + } + None => { + let cleaned = strip_orphan_begin_marker(¤t); + format!("{}\n\n{}", cleaned.trim_end_matches('\n'), replacement) + } + }; + + let parent = file_path.parent().ok_or_else(|| { + io::Error::new(io::ErrorKind::InvalidInput, "file path has no parent directory") + })?; + let mut tmp = tempfile::NamedTempFile::new_in(parent)?; + { + use std::io::Write; + tmp.write_all(new_content.as_bytes())?; + } + tmp.persist(file_path).map_err(|e| e.error)?; Ok(()) } @@ -399,6 +440,7 @@ mod tests { let agents = vec![make_agent("Kit", Some("p1"))]; let output = render_dynamic_section(&personas, &agents, "ws://example.com:3000"); assert!(output.contains("| Kit | Builder | @Kit |")); + assert!(output.contains("| Name | Persona | How to address |")); assert!(output.contains("## CLI Quick Reference")); } @@ -468,10 +510,214 @@ mod tests { upsert_managed_section(&file, "content").unwrap(); - let tmp_path = file.with_extension("md.tmp"); + // Verify no stray temp files in the directory + let entries: Vec<_> = fs::read_dir(tmp.path()) + .unwrap() + .filter_map(|e| e.ok()) + .collect(); + assert_eq!(entries.len(), 1, "only AGENTS.md should remain, no temp files"); + assert_eq!(entries[0].file_name(), "AGENTS.md"); + } + + #[test] + fn test_upsert_end_before_begin() { + // An END marker that precedes a BEGIN marker forms no valid ordered pair. + // find_managed_markers returns None (BEGIN found, but no END after it), + // so the orphan BEGIN line is stripped and a new block is appended. + // The stray END line and content between END and BEGIN remain in the file + // because strip_orphan_begin_marker only removes the BEGIN line itself. + let tmp = tempfile::tempdir().unwrap(); + let file = tmp.path().join("AGENTS.md"); + fs::write( + &file, + "# Header\n\n\nsome middle content\n\nold section\n", + ) + .unwrap(); + + upsert_managed_section(&file, "new section").unwrap(); + + let result = fs::read_to_string(&file).unwrap(); + + assert!(result.contains("# Header"), "original header must survive"); + assert!(result.contains("new section"), "new content must be present"); + assert!(result.contains("some middle content"), "content between markers must survive"); + + // Exactly one BEGIN marker in the output (the orphan was stripped, new one appended). + assert_eq!( + result.matches(BEGIN_MARKER).count(), + 1, + "exactly one BEGIN marker after orphan cleanup" + ); + + // The single BEGIN marker must have a matching END marker after it. + let begin_pos = result.find(BEGIN_MARKER).expect("BEGIN marker must be present"); + let end_pos = result[begin_pos..].find(END_MARKER).map(|p| begin_pos + p); + assert!( + end_pos.is_some(), + "an END marker must appear after the appended BEGIN marker" + ); + } + + #[test] + fn test_upsert_begin_only_no_end() { + // A file with BEGIN but no END has an orphan marker. + // find_managed_markers returns None (no END found after BEGIN), + // so strip_orphan_begin_marker removes the BEGIN line. + // Content that followed the orphan BEGIN is preserved (only the marker line is stripped, + // not the body that came after it). + let tmp = tempfile::tempdir().unwrap(); + let file = tmp.path().join("AGENTS.md"); + fs::write( + &file, + "# Header\n\nsome content\n\n\norphaned section without end marker\n", + ) + .unwrap(); + + upsert_managed_section(&file, "fresh section").unwrap(); + + let result = fs::read_to_string(&file).unwrap(); + + assert!(result.contains("# Header"), "original header must survive"); + assert!(result.contains("some content"), "original body must survive"); + assert!(result.contains("fresh section"), "new content must be present"); + + let begin_pos = result.find(BEGIN_MARKER).expect("BEGIN marker must be present"); + let end_pos = result.find(END_MARKER).expect("END marker must be present"); assert!( - !tmp_path.exists(), - ".tmp file should not exist after successful upsert" + begin_pos < end_pos, + "the appended BEGIN marker must precede the appended END marker" + ); + + // Exactly one BEGIN marker after orphan cleanup. + assert_eq!( + result.matches(BEGIN_MARKER).count(), + 1, + "exactly one BEGIN marker after orphan cleanup" + ); + } + + #[test] + fn test_upsert_duplicate_markers() { + let tmp = tempfile::tempdir().unwrap(); + let file = tmp.path().join("AGENTS.md"); + fs::write( + &file, + "# Header\n\n\nfirst block\n\n\nbetween blocks\n\n\nsecond block\n\n", + ) + .unwrap(); + + upsert_managed_section(&file, "replaced").unwrap(); + + let result = fs::read_to_string(&file).unwrap(); + + assert!(result.contains("replaced"), "replacement content must be present"); + assert!(!result.contains("first block"), "first block must be replaced"); + assert!(result.contains("second block"), "second pair content must survive"); + assert!(result.contains("between blocks"), "text between pairs must survive"); + } + + #[test] + fn test_upsert_marker_in_code_block() { + let tmp = tempfile::tempdir().unwrap(); + let file = tmp.path().join("AGENTS.md"); + // Indented by 4 spaces — not at column 0, so should NOT match as a real marker. + fs::write( + &file, + "# Header\n\n \n\nReal content here\n", + ) + .unwrap(); + + upsert_managed_section(&file, "appended content").unwrap(); + + let result = fs::read_to_string(&file).unwrap(); + + assert!( + result.contains(" "), + "indented marker inside code block must be preserved verbatim" + ); + assert!(result.contains("appended content"), "new content must be appended"); + assert!(result.contains("Real content here"), "existing body must survive"); + + // The real markers appended at the end must be at line-start (column 0). + let begin_pos = result + .find("\nexisting section\n\n", + ) + .unwrap(); + + upsert_managed_section(&file, "same content").unwrap(); + let after_first = fs::read_to_string(&file).unwrap(); + + upsert_managed_section(&file, "same content").unwrap(); + let after_second = fs::read_to_string(&file).unwrap(); + + assert_eq!( + after_first, after_second, + "upsert must be idempotent: second call must not alter the file" ); } } From a589d574597be534e778c3923890a920e6dc4b02 Mon Sep 17 00:00:00 2001 From: Will Pfleger Date: Thu, 14 May 2026 16:24:20 -0400 Subject: [PATCH 07/10] style: apply rustfmt to review-fix commit --- desktop/src-tauri/src/managed_agents/nest.rs | 78 ++++++++++++++++---- 1 file changed, 62 insertions(+), 16 deletions(-) diff --git a/desktop/src-tauri/src/managed_agents/nest.rs b/desktop/src-tauri/src/managed_agents/nest.rs index fc40f3a7..0b9ab682 100644 --- a/desktop/src-tauri/src/managed_agents/nest.rs +++ b/desktop/src-tauri/src/managed_agents/nest.rs @@ -191,7 +191,9 @@ fn find_marker_at_line_start(content: &str, marker: &str) -> Option { fn find_managed_markers(content: &str) -> Option<(usize, usize)> { let begin_pos = find_marker_at_line_start(content, BEGIN_MARKER)?; let begin_line_start = content[..begin_pos].rfind('\n').map(|p| p + 1).unwrap_or(0); - let end_pos = content[begin_pos..].find(END_MARKER).map(|p| p + begin_pos)?; + let end_pos = content[begin_pos..] + .find(END_MARKER) + .map(|p| p + begin_pos)?; let end_of_end = end_pos + END_MARKER.len(); let after_end = if content[end_of_end..].starts_with('\n') { end_of_end + 1 @@ -205,7 +207,10 @@ fn find_managed_markers(content: &str) -> Option<(usize, usize)> { fn strip_orphan_begin_marker(content: &str) -> String { if let Some(pos) = find_marker_at_line_start(content, BEGIN_MARKER) { let line_start = content[..pos].rfind('\n').map(|p| p + 1).unwrap_or(0); - let line_end = content[pos..].find('\n').map(|p| pos + p + 1).unwrap_or(content.len()); + let line_end = content[pos..] + .find('\n') + .map(|p| pos + p + 1) + .unwrap_or(content.len()); format!( "{}{}", &content[..line_start], @@ -239,7 +244,10 @@ pub fn upsert_managed_section(file_path: &Path, new_section_content: &str) -> io }; let parent = file_path.parent().ok_or_else(|| { - io::Error::new(io::ErrorKind::InvalidInput, "file path has no parent directory") + io::Error::new( + io::ErrorKind::InvalidInput, + "file path has no parent directory", + ) })?; let mut tmp = tempfile::NamedTempFile::new_in(parent)?; { @@ -515,7 +523,11 @@ mod tests { .unwrap() .filter_map(|e| e.ok()) .collect(); - assert_eq!(entries.len(), 1, "only AGENTS.md should remain, no temp files"); + assert_eq!( + entries.len(), + 1, + "only AGENTS.md should remain, no temp files" + ); assert_eq!(entries[0].file_name(), "AGENTS.md"); } @@ -539,8 +551,14 @@ mod tests { let result = fs::read_to_string(&file).unwrap(); assert!(result.contains("# Header"), "original header must survive"); - assert!(result.contains("new section"), "new content must be present"); - assert!(result.contains("some middle content"), "content between markers must survive"); + assert!( + result.contains("new section"), + "new content must be present" + ); + assert!( + result.contains("some middle content"), + "content between markers must survive" + ); // Exactly one BEGIN marker in the output (the orphan was stripped, new one appended). assert_eq!( @@ -550,7 +568,9 @@ mod tests { ); // The single BEGIN marker must have a matching END marker after it. - let begin_pos = result.find(BEGIN_MARKER).expect("BEGIN marker must be present"); + let begin_pos = result + .find(BEGIN_MARKER) + .expect("BEGIN marker must be present"); let end_pos = result[begin_pos..].find(END_MARKER).map(|p| begin_pos + p); assert!( end_pos.is_some(), @@ -578,10 +598,18 @@ mod tests { let result = fs::read_to_string(&file).unwrap(); assert!(result.contains("# Header"), "original header must survive"); - assert!(result.contains("some content"), "original body must survive"); - assert!(result.contains("fresh section"), "new content must be present"); + assert!( + result.contains("some content"), + "original body must survive" + ); + assert!( + result.contains("fresh section"), + "new content must be present" + ); - let begin_pos = result.find(BEGIN_MARKER).expect("BEGIN marker must be present"); + let begin_pos = result + .find(BEGIN_MARKER) + .expect("BEGIN marker must be present"); let end_pos = result.find(END_MARKER).expect("END marker must be present"); assert!( begin_pos < end_pos, @@ -610,10 +638,22 @@ mod tests { let result = fs::read_to_string(&file).unwrap(); - assert!(result.contains("replaced"), "replacement content must be present"); - assert!(!result.contains("first block"), "first block must be replaced"); - assert!(result.contains("second block"), "second pair content must survive"); - assert!(result.contains("between blocks"), "text between pairs must survive"); + assert!( + result.contains("replaced"), + "replacement content must be present" + ); + assert!( + !result.contains("first block"), + "first block must be replaced" + ); + assert!( + result.contains("second block"), + "second pair content must survive" + ); + assert!( + result.contains("between blocks"), + "text between pairs must survive" + ); } #[test] @@ -635,8 +675,14 @@ mod tests { result.contains(" "), "indented marker inside code block must be preserved verbatim" ); - assert!(result.contains("appended content"), "new content must be appended"); - assert!(result.contains("Real content here"), "existing body must survive"); + assert!( + result.contains("appended content"), + "new content must be appended" + ); + assert!( + result.contains("Real content here"), + "existing body must survive" + ); // The real markers appended at the end must be at line-start (column 0). let begin_pos = result From 8dd274825022dfeca8bfda243238a4d5f3bfd1d3 Mon Sep 17 00:00:00 2001 From: Will Pfleger Date: Fri, 15 May 2026 15:51:02 -0400 Subject: [PATCH 08/10] fix(desktop): add nest.rs to file size check overrides MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit nest.rs grew to 770 lines with regenerate_nest_context, marker helpers, and 19 unit tests. The 500-line default is too tight for this file — override to 800 following the established pattern. --- desktop/scripts/check-file-sizes.mjs | 1 + 1 file changed, 1 insertion(+) diff --git a/desktop/scripts/check-file-sizes.mjs b/desktop/scripts/check-file-sizes.mjs index 54c924c1..300021e0 100644 --- a/desktop/scripts/check-file-sizes.mjs +++ b/desktop/scripts/check-file-sizes.mjs @@ -52,6 +52,7 @@ const overrides = new Map([ ["src-tauri/src/nostr_convert.rs", 1150], // 12 Nostr event→model converters (channels, profiles, members, notes, search, agents, relay members) + rank_user_search_results helper for NIP-50 user search + 33 unit tests ["src-tauri/src/managed_agents/runtime.rs", 1110], // ... + respond-to gate env (SPROUT_ACP_RESPOND_TO[_ALLOWLIST]) + per-mode env builder + tests + persona/agent env_vars spawn merge (helper + tests now in env_vars.rs) ["src-tauri/src/managed_agents/types.rs", 715], // ManagedAgentRecord/Summary + Create/Update request structs + RespondTo enum + validate_respond_to_allowlist + tests + persona/agent env_vars field + ["src-tauri/src/managed_agents/nest.rs", 800], // regenerate_nest_context + render_dynamic_section + upsert_managed_section + marker helpers + 19 unit tests ["src-tauri/src/managed_agents/backend.rs", 700], // provider IPC, validation, discovery, binary resolution + tests + redact_secrets_with for user env values + env_secrets_from_request + redact_env_values_in (shared with model discovery) ["src/features/huddle/HuddleContext.tsx", 650], // huddle lifecycle context + joinHuddle + connectAndSetupMedia shared helper + activeSpeakers/isReconnecting state + PTT (reusable AudioContext) + TTS subscription + mic level analyser (10fps throttle) + agent pubkey refresh ["src/features/agents/hooks.ts", 540], // agent query/mutation surface now includes built-in persona library activation + useUpdateManagedAgentMutation From 6c3f070d8f4ea84bd6c496da634fc08afd8acc80 Mon Sep 17 00:00:00 2001 From: Will Pfleger Date: Fri, 15 May 2026 17:13:48 -0400 Subject: [PATCH 09/10] refactor: consolidate PR #583 into #584, remove CLI_QUICK_REFERENCE duplication base_prompt.md (injected every turn via [Base]) already has a comprehensive CLI reference table covering all 11 command groups. The CLI_QUICK_REFERENCE constant in nest.rs was a less complete duplicate (4 commands). AGENTS.md's dynamic section now focuses on what's unique to it: active agents and workspace info. Also bumps personas.rs file size override from 900 to 950 to accommodate the merge_personas inequality checks added in the review-fix commit. --- desktop/scripts/check-file-sizes.mjs | 2 +- desktop/src-tauri/src/managed_agents/nest.rs | 12 ++---------- 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/desktop/scripts/check-file-sizes.mjs b/desktop/scripts/check-file-sizes.mjs index 300021e0..5107f60c 100644 --- a/desktop/scripts/check-file-sizes.mjs +++ b/desktop/scripts/check-file-sizes.mjs @@ -30,7 +30,7 @@ const rules = [ // Exceptions should stay rare and temporary. Prefer splitting files instead. const overrides = new Map([ - ["src-tauri/src/managed_agents/personas.rs", 900], // built-in persona system prompts (Solo + Kit + Scout) + persona pack import/uninstall/list + uninstall safety check + ["src-tauri/src/managed_agents/personas.rs", 950], // built-in persona system prompts (Solo + Kit + Scout) + merge_personas inequality checks + persona pack import/uninstall/list + uninstall safety check ["src-tauri/src/managed_agents/teams.rs", 580], // built-in team registry (Kit & Scout) + merge_teams + validate_team_deletion + JSON export/import + tests ["src-tauri/src/managed_agents/persona_card.rs", 970], // PNG/ZIP/MD persona card codec + pack-zip detection + nested root finder + provider/model/namePool fields + 27 unit tests ["src/app/AppShell.tsx", 800], // message edit state + handlers + ChannelPane edit prop threading + scrollback pagination + workflows view + projects view + memory-leak safeguards diff --git a/desktop/src-tauri/src/managed_agents/nest.rs b/desktop/src-tauri/src/managed_agents/nest.rs index 0b9ab682..184364cd 100644 --- a/desktop/src-tauri/src/managed_agents/nest.rs +++ b/desktop/src-tauri/src/managed_agents/nest.rs @@ -132,14 +132,6 @@ pub fn ensure_nest_at(root: &Path) -> Result<(), String> { Ok(()) } -const CLI_QUICK_REFERENCE: &str = "\ -## CLI Quick Reference -`sprout messages send --channel --content ` — send a message -`sprout messages get --channel ` — read recent messages -`sprout channels list` — list available channels -`sprout workflows trigger --workflow ` — trigger a workflow -Run `sprout --help` for the full command reference."; - fn escape_md_cell(s: &str) -> String { s.replace('|', "\\|").replace('\n', " ") } @@ -170,7 +162,7 @@ pub fn render_dynamic_section( table }; - format!("{active_agents}\n\n## Workspace\n- Relay: {relay_url}\n\n{CLI_QUICK_REFERENCE}") + format!("{active_agents}\n\n## Workspace\n- Relay: {relay_url}") } /// Find a marker that appears at the start of a line (position 0 or preceded by `\n`). @@ -449,7 +441,7 @@ mod tests { let output = render_dynamic_section(&personas, &agents, "ws://example.com:3000"); assert!(output.contains("| Kit | Builder | @Kit |")); assert!(output.contains("| Name | Persona | How to address |")); - assert!(output.contains("## CLI Quick Reference")); + assert!(output.contains("## Workspace")); } #[test] From bd119385ca60546e8bd453a1868a4beed559492b Mon Sep 17 00:00:00 2001 From: Will Pfleger Date: Fri, 15 May 2026 18:10:22 -0400 Subject: [PATCH 10/10] fix: address code review findings from crossfire review Review surfaced 10 findings across 5 independent sources (3 Claude specialists + Codex + Gemini). Key changes: - Fix incorrect CLI syntax in base_prompt.md (missing --channel flag, positional arg that should be named) and stale nest_agents.md reference - Apply line-start validation to END_MARKER in find_managed_markers, matching the BEGIN_MARKER contract documented in the function comment - Extract prepend_base_prompt helper to deduplicate [Base] injection across format_prompt, dispatch_heartbeat, and initial_message paths - Move base_prompt_file read from main.rs panic to Config::from_cli with proper ConfigError propagation and 1 MB size guard - Extract try_regenerate_nest helper to consolidate 11 identical error-handling blocks across command files - Sanitize relay_url before AGENTS.md injection (strip CR/LF) - Fix greedy newline strip in strip_orphan_begin_marker - Add doc comments for PromptContext.base_prompt lifetime constraint --- crates/sprout-acp/src/base_prompt.md | 6 ++--- crates/sprout-acp/src/config.rs | 26 ++++++++++++++++--- crates/sprout-acp/src/lib.rs | 14 +++++----- crates/sprout-acp/src/pool.rs | 12 ++++++--- crates/sprout-acp/src/queue.rs | 9 +++++++ .../src-tauri/src/commands/agent_models.rs | 10 +++---- desktop/src-tauri/src/commands/agents.rs | 16 +++++------- desktop/src-tauri/src/commands/personas.rs | 26 +++++-------------- desktop/src-tauri/src/commands/workspace.rs | 8 ++---- desktop/src-tauri/src/lib.rs | 8 +++--- desktop/src-tauri/src/managed_agents/nest.rs | 22 +++++++++++++--- 11 files changed, 89 insertions(+), 68 deletions(-) diff --git a/crates/sprout-acp/src/base_prompt.md b/crates/sprout-acp/src/base_prompt.md index 5f82e213..0bd8e601 100644 --- a/crates/sprout-acp/src/base_prompt.md +++ b/crates/sprout-acp/src/base_prompt.md @@ -26,12 +26,12 @@ MCP tools (via `sprout-mcp`) are also available but the CLI is preferred for bat - Address agents and humans with plain `@name` — do NOT bold or italicize mention text (formatting prevents alert delivery). - Use `sprout messages thread` or MCP `get_thread()` when responding in-thread; post new messages for new topics. -- No push notifications — poll with `sprout messages get --since=` or MCP `get_messages(since=)`. When `since` is set without `before`, results are oldest-first (chronological). +- No push notifications — poll with `sprout messages get --channel --since ` or MCP `get_messages(channel_id, since=)`. When `since` is set without `before`, results are oldest-first (chronological). ## Startup Recovery 1. `sprout feed get` (or MCP `get_feed()`) — surface pending mentions and action items. Filter by type: `mentions`, `needs_action`, `activity`, `agent_activity`. -2. `sprout messages get ` on assigned channels — catch up on recent history. +2. `sprout messages get --channel ` on assigned channels — catch up on recent history. 3. Check `AGENTS.md` in your working directory for team context. 4. Check `RESEARCH/`, `GUIDES/`, `PLANS/` before searching externally. Use `sprout messages search --query "..."` for cross-channel keyword lookups. @@ -49,4 +49,4 @@ Your persistent workspace is in your working directory: | `REPOS/` | Checked-out source repositories | | `.scratch/` | Ephemeral working files | -Knowledge files use `ALL_CAPS_WITH_UNDERSCORES.md` naming. `AGENTS.md` lists active agents and roles. See `nest_agents.md` in your working directory for full workspace conventions. +Knowledge files use `ALL_CAPS_WITH_UNDERSCORES.md` naming. `AGENTS.md` lists active agents and roles. See `AGENTS.md` in your working directory for full workspace conventions. diff --git a/crates/sprout-acp/src/config.rs b/crates/sprout-acp/src/config.rs index 3f8596ed..fac82545 100644 --- a/crates/sprout-acp/src/config.rs +++ b/crates/sprout-acp/src/config.rs @@ -448,8 +448,10 @@ pub struct Config { pub agent_owner: Option, /// Disable the [Base] platform-context section prepended to every prompt. pub no_base_prompt: bool, - /// Path to a custom base prompt file that overrides the compiled-in default. - pub base_prompt_file: Option, + /// Resolved content from `--base-prompt-file`, read and validated in + /// `from_cli()`. `None` when using the compiled-in default or when + /// `--no-base-prompt` is set. + pub base_prompt_content: Option, } /// Validate and deduplicate allowlist entries: each must be exactly 64 hex chars. @@ -579,6 +581,22 @@ impl Config { None }; + let base_prompt_content = if args.no_base_prompt { + None + } else if let Some(ref path) = args.base_prompt_file { + let content = std::fs::read_to_string(path)?; + if content.len() > 1_048_576 { + return Err(ConfigError::ConfigFile(format!( + "base prompt file {} exceeds 1 MB limit ({} bytes)", + path.display(), + content.len() + ))); + } + Some(content) + } else { + None + }; + if matches!(args.subscribe, SubscribeMode::Config) { if args.kinds.is_some() { tracing::warn!("--kinds is ignored in config mode"); @@ -787,7 +805,7 @@ impl Config { relay_observer: args.relay_observer, agent_owner: args.agent_owner.map(|s| s.trim().to_ascii_lowercase()), no_base_prompt: args.no_base_prompt, - base_prompt_file: args.base_prompt_file, + base_prompt_content, }; Ok(config) @@ -1150,7 +1168,7 @@ mod tests { relay_observer: false, agent_owner: None, no_base_prompt: false, - base_prompt_file: None, + base_prompt_content: None, } } diff --git a/crates/sprout-acp/src/lib.rs b/crates/sprout-acp/src/lib.rs index 3430bac2..136046d4 100644 --- a/crates/sprout-acp/src/lib.rs +++ b/crates/sprout-acp/src/lib.rs @@ -23,7 +23,7 @@ use pool::{ AgentPool, CancelMode, OwnedAgent, PromptContext, PromptOutcome, PromptResult, PromptSource, SessionState, }; -use queue::{EventQueue, QueuedEvent, ThreadTags}; +use queue::{prepend_base_prompt, EventQueue, QueuedEvent, ThreadTags}; use relay::{HarnessRelay, RelayEventPublisher}; use sprout_core::kind::{ KIND_MEMBER_ADDED_NOTIFICATION, KIND_MEMBER_REMOVED_NOTIFICATION, KIND_STREAM_MESSAGE, @@ -792,7 +792,7 @@ async fn tokio_main() -> Result<()> { .compact() .init(); - let config = Config::from_cli().map_err(|e| anyhow::anyhow!("configuration error: {e}"))?; + let mut config = Config::from_cli().map_err(|e| anyhow::anyhow!("configuration error: {e}"))?; tracing::info!("sprout-acp starting: {}", config.summary()); let observer = config @@ -1067,6 +1067,7 @@ async fn tokio_main() -> Result<()> { let dedup_mode = config.dedup_mode; let mut queue = EventQueue::new(dedup_mode); + let base_prompt_content = config.base_prompt_content.take(); let ctx = Arc::new(PromptContext { mcp_servers: build_mcp_servers(&config), initial_message: config.initial_message.clone(), @@ -1076,10 +1077,7 @@ async fn tokio_main() -> Result<()> { system_prompt: config.system_prompt.clone(), base_prompt: if config.no_base_prompt { None - } else if let Some(ref path) = config.base_prompt_file { - let content = std::fs::read_to_string(path).unwrap_or_else(|e| { - panic!("failed to read base prompt file {}: {e}", path.display()) - }); + } else if let Some(content) = base_prompt_content { Some(Box::leak(content.into_boxed_str())) } else { Some(include_str!("base_prompt.md")) @@ -2273,7 +2271,7 @@ fn dispatch_heartbeat( .clone() .unwrap_or_else(default_heartbeat_prompt); let prompt_text = match ctx.base_prompt { - Some(bp) => format!("[Base]\n{}\n\n{prompt_text}", bp.trim_end()), + Some(bp) => prepend_base_prompt(bp, &prompt_text), None => prompt_text, }; let result_tx = pool.result_tx(); @@ -2770,7 +2768,7 @@ mod build_mcp_servers_tests { relay_observer: false, agent_owner: None, no_base_prompt: false, - base_prompt_file: None, + base_prompt_content: None, } } diff --git a/crates/sprout-acp/src/pool.rs b/crates/sprout-acp/src/pool.rs index b5f9391c..81265f9b 100644 --- a/crates/sprout-acp/src/pool.rs +++ b/crates/sprout-acp/src/pool.rs @@ -35,8 +35,8 @@ use crate::acp::{ use crate::config::{DedupMode, PermissionMode}; use crate::observer; use crate::queue::{ - ContextMessage, ConversationContext, FlushBatch, PromptChannelInfo, PromptProfile, - PromptProfileLookup, + prepend_base_prompt, ContextMessage, ConversationContext, FlushBatch, PromptChannelInfo, + PromptProfile, PromptProfileLookup, }; use crate::relay::{ChannelInfo, RestClient}; @@ -187,6 +187,12 @@ pub struct PromptContext { pub dedup_mode: DedupMode, pub system_prompt: Option, pub heartbeat_prompt: Option, + /// Base prompt content, or `None` if `--no-base-prompt` was passed. + /// + /// `'static` because `PromptContext` is `Arc`-shared across async tasks. + /// Content from `--base-prompt-file` is promoted via `Box::leak` in `main.rs` + /// after validated file read in `Config::from_cli()`. The compiled-in default + /// (`include_str!`) is inherently `'static`. pub base_prompt: Option<&'static str>, pub cwd: String, /// REST client for pre-prompt context fetches (thread/DM history). @@ -749,7 +755,7 @@ pub async fn run_prompt_task( ); // Prepend base prompt to initial_message for platform orientation. let init_msg = match ctx.base_prompt { - Some(bp) => format!("[Base]\n{}\n\n{initial_msg}", bp.trim_end()), + Some(bp) => prepend_base_prompt(bp, initial_msg), None => initial_msg.to_string(), }; let init_result = agent diff --git a/crates/sprout-acp/src/queue.rs b/crates/sprout-acp/src/queue.rs index fa4b9ddc..70f9e087 100644 --- a/crates/sprout-acp/src/queue.rs +++ b/crates/sprout-acp/src/queue.rs @@ -952,6 +952,15 @@ pub struct FormatPromptArgs<'a> { pub profile_lookup: Option<&'a PromptProfileLookup>, } +/// Prepend the `[Base]` platform-context section to a prompt body. +/// +/// Used by the heartbeat and initial-message paths so the `[Base]` format +/// is defined in exactly one place. (`format_prompt` uses a sections-vec +/// approach instead, but the resulting `[Base]\n{content}` format is identical.) +pub fn prepend_base_prompt(base: &str, body: &str) -> String { + format!("[Base]\n{}\n\n{body}", base.trim_end()) +} + /// Format a [`FlushBatch`] into a prompt string for the agent. /// /// Produces a stable prompt with these sections (in order): diff --git a/desktop/src-tauri/src/commands/agent_models.rs b/desktop/src-tauri/src/commands/agent_models.rs index 50986b4d..fa8a4128 100644 --- a/desktop/src-tauri/src/commands/agent_models.rs +++ b/desktop/src-tauri/src/commands/agent_models.rs @@ -8,9 +8,9 @@ use crate::{ managed_agents::{ build_managed_agent_summary, default_agent_workdir, find_managed_agent_mut, load_managed_agents, managed_agent_avatar_url, missing_command_message, - normalize_agent_args, regenerate_nest_context, resolve_command, save_managed_agents, - sync_managed_agent_processes, AgentModelInfo, AgentModelsResponse, - UpdateManagedAgentRequest, UpdateManagedAgentResponse, + normalize_agent_args, resolve_command, save_managed_agents, sync_managed_agent_processes, + try_regenerate_nest, AgentModelInfo, AgentModelsResponse, UpdateManagedAgentRequest, + UpdateManagedAgentResponse, }, relay::{relay_ws_url_with_override, sync_managed_agent_profile}, util::now_iso, @@ -247,9 +247,7 @@ pub async fn update_managed_agent( (summary, sync_params) }; // lock dropped here - if let Err(error) = regenerate_nest_context(&app) { - eprintln!("sprout-desktop: nest context regeneration failed: {error}"); - } + try_regenerate_nest(&app); // Phase 2: relay profile sync (async, best-effort, outside lock) let profile_sync_error = diff --git a/desktop/src-tauri/src/commands/agents.rs b/desktop/src-tauri/src/commands/agents.rs index a81bc1e9..02a76b48 100644 --- a/desktop/src-tauri/src/commands/agents.rs +++ b/desktop/src-tauri/src/commands/agents.rs @@ -8,10 +8,10 @@ use crate::{ build_managed_agent_summary, discover_provider_candidates, ensure_persona_is_active, find_managed_agent_mut, invoke_provider, load_managed_agents, load_personas, managed_agent_avatar_url, managed_agent_log_path, managed_agents_base_dir, - normalize_agent_args, provider_deploy, read_log_tail, regenerate_nest_context, - resolve_provider_binary, save_managed_agents, start_managed_agent_process, - stop_managed_agent_process, sync_managed_agent_processes, validate_provider_config, - BackendKind, BackendProviderInfo, CreateManagedAgentRequest, CreateManagedAgentResponse, + normalize_agent_args, provider_deploy, read_log_tail, resolve_provider_binary, + save_managed_agents, start_managed_agent_process, stop_managed_agent_process, + sync_managed_agent_processes, try_regenerate_nest, validate_provider_config, BackendKind, + BackendProviderInfo, CreateManagedAgentRequest, CreateManagedAgentResponse, ManagedAgentLogResponse, ManagedAgentRecord, ManagedAgentSummary, DEFAULT_ACP_COMMAND, DEFAULT_AGENT_COMMAND, DEFAULT_AGENT_PARALLELISM, DEFAULT_AGENT_TURN_TIMEOUT_SECONDS, DEFAULT_MCP_COMMAND, @@ -454,9 +454,7 @@ pub async fn create_managed_agent( (agent, spawn_error) }; - if let Err(error) = regenerate_nest_context(&app) { - eprintln!("sprout-desktop: nest context regeneration failed: {error}"); - } + try_regenerate_nest(&app); // ── Phase 4: sync agent profile on relay (async, outside lock) ─────────── let avatar_url = input @@ -722,9 +720,7 @@ pub fn delete_managed_agent( } save_managed_agents(&app, &records)?; } - if let Err(error) = regenerate_nest_context(&app) { - eprintln!("sprout-desktop: nest context regeneration failed: {error}"); - } + try_regenerate_nest(&app); Ok(()) } diff --git a/desktop/src-tauri/src/commands/personas.rs b/desktop/src-tauri/src/commands/personas.rs index 39db8a98..b3d0d450 100644 --- a/desktop/src-tauri/src/commands/personas.rs +++ b/desktop/src-tauri/src/commands/personas.rs @@ -7,7 +7,7 @@ use crate::{ managed_agents::{ encode_persona_json, import_persona_pack, list_installed_packs, load_managed_agents, load_personas, load_teams, parse_json_persona, parse_md_persona, parse_png_persona, - parse_zip_personas, regenerate_nest_context, save_managed_agents, save_personas, + parse_zip_personas, save_managed_agents, save_personas, try_regenerate_nest, uninstall_persona_pack as do_uninstall_persona_pack, validate_persona_activation_change, validate_persona_deletion, CreatePersonaRequest, PackSummary, ParsePersonaFilesResult, PersonaRecord, UpdatePersonaRequest, @@ -85,9 +85,7 @@ pub fn create_persona( }; personas.push(persona.clone()); save_personas(&app, &personas)?; - if let Err(error) = regenerate_nest_context(&app) { - eprintln!("sprout-desktop: nest context regeneration failed: {error}"); - } + try_regenerate_nest(&app); Ok(persona) } @@ -138,9 +136,7 @@ pub fn update_persona( .into_iter() .find(|record| record.id == input.id) .ok_or_else(|| format!("persona {} disappeared unexpectedly", input.id))?; - if let Err(error) = regenerate_nest_context(&app) { - eprintln!("sprout-desktop: nest context regeneration failed: {error}"); - } + try_regenerate_nest(&app); Ok(result) } @@ -186,9 +182,7 @@ pub fn delete_persona( if changed_agents { save_managed_agents(&app, &agents)?; } - if let Err(error) = regenerate_nest_context(&app) { - eprintln!("sprout-desktop: nest context regeneration failed: {error}"); - } + try_regenerate_nest(&app); Ok(()) } @@ -237,9 +231,7 @@ pub fn set_persona_active( let updated = persona.clone(); save_personas(&app, &personas)?; - if let Err(error) = regenerate_nest_context(&app) { - eprintln!("sprout-desktop: nest context regeneration failed: {error}"); - } + try_regenerate_nest(&app); Ok(updated) } @@ -394,9 +386,7 @@ pub fn install_persona_pack( return Err(format!("pack path is not a directory: {path}")); } let result = import_persona_pack(&app, &source)?; - if let Err(error) = regenerate_nest_context(&app) { - eprintln!("sprout-desktop: nest context regeneration failed: {error}"); - } + try_regenerate_nest(&app); Ok(result) } @@ -411,9 +401,7 @@ pub fn uninstall_persona_pack( .lock() .map_err(|e| e.to_string())?; do_uninstall_persona_pack(&app, &pack_id)?; - if let Err(error) = regenerate_nest_context(&app) { - eprintln!("sprout-desktop: nest context regeneration failed: {error}"); - } + try_regenerate_nest(&app); Ok(()) } diff --git a/desktop/src-tauri/src/commands/workspace.rs b/desktop/src-tauri/src/commands/workspace.rs index 615dd232..79ca43f9 100644 --- a/desktop/src-tauri/src/commands/workspace.rs +++ b/desktop/src-tauri/src/commands/workspace.rs @@ -3,7 +3,7 @@ use serde::Serialize; use tauri::{AppHandle, State}; use crate::app_state::AppState; -use crate::managed_agents::regenerate_nest_context; +use crate::managed_agents::try_regenerate_nest; use crate::relay; #[derive(Serialize)] @@ -53,11 +53,7 @@ pub fn apply_workspace( *keys_guard = keys; } - if let Err(error) = regenerate_nest_context(&app) { - eprintln!( - "sprout-desktop: failed to regenerate nest context after workspace switch: {error}" - ); - } + try_regenerate_nest(&app); Ok(()) } diff --git a/desktop/src-tauri/src/lib.rs b/desktop/src-tauri/src/lib.rs index 375aaa4c..1bb1f513 100644 --- a/desktop/src-tauri/src/lib.rs +++ b/desktop/src-tauri/src/lib.rs @@ -24,9 +24,9 @@ use huddle::{ speak_agent_message, start_huddle, start_stt_pipeline, }; use managed_agents::{ - ensure_nest, kill_stale_tracked_processes, load_managed_agents, regenerate_nest_context, + ensure_nest, kill_stale_tracked_processes, load_managed_agents, restore_managed_agents_on_launch, save_managed_agents, sync_managed_agent_processes, - BackendKind, ManagedAgentProcess, + try_regenerate_nest, BackendKind, ManagedAgentProcess, }; use std::sync::{ atomic::{AtomicBool, Ordering}, @@ -393,9 +393,7 @@ pub fn run() { eprintln!("sprout-desktop: failed to create nest: {error}"); } - if let Err(error) = regenerate_nest_context(&app_handle) { - eprintln!("sprout-desktop: failed to regenerate nest context: {error}"); - } + try_regenerate_nest(&app_handle); // Pre-download voice models in the background so they're ready // when the user starts their first huddle. Idempotent — no-op if diff --git a/desktop/src-tauri/src/managed_agents/nest.rs b/desktop/src-tauri/src/managed_agents/nest.rs index 184364cd..57dc8f8b 100644 --- a/desktop/src-tauri/src/managed_agents/nest.rs +++ b/desktop/src-tauri/src/managed_agents/nest.rs @@ -162,6 +162,7 @@ pub fn render_dynamic_section( table }; + let relay_url = relay_url.replace(['\n', '\r'], ""); format!("{active_agents}\n\n## Workspace\n- Relay: {relay_url}") } @@ -183,9 +184,8 @@ fn find_marker_at_line_start(content: &str, marker: &str) -> Option { fn find_managed_markers(content: &str) -> Option<(usize, usize)> { let begin_pos = find_marker_at_line_start(content, BEGIN_MARKER)?; let begin_line_start = content[..begin_pos].rfind('\n').map(|p| p + 1).unwrap_or(0); - let end_pos = content[begin_pos..] - .find(END_MARKER) - .map(|p| p + begin_pos)?; + let end_pos = + find_marker_at_line_start(&content[begin_pos..], END_MARKER).map(|p| p + begin_pos)?; let end_of_end = end_pos + END_MARKER.len(); let after_end = if content[end_of_end..].starts_with('\n') { end_of_end + 1 @@ -206,7 +206,9 @@ fn strip_orphan_begin_marker(content: &str) -> String { format!( "{}{}", &content[..line_start], - content[line_end..].trim_start_matches('\n') + content[line_end..] + .strip_prefix('\n') + .unwrap_or(&content[line_end..]) ) } else { content.to_string() @@ -270,6 +272,16 @@ pub fn regenerate_nest_context(app: &AppHandle) -> Result<(), String> { Ok(()) } +/// Convenience wrapper: regenerates nest context, logging a warning on failure. +/// +/// All call sites treat regeneration as fire-and-forget — agents run fine with +/// a stale AGENTS.md, so we warn and continue rather than propagating the error. +pub fn try_regenerate_nest(app: &AppHandle) { + if let Err(error) = regenerate_nest_context(app) { + eprintln!("sprout-desktop: nest context regeneration failed: {error}"); + } +} + #[cfg(test)] mod tests { use super::*; @@ -392,6 +404,7 @@ mod tests { is_active: true, source_pack: None, source_pack_persona_slug: None, + env_vars: std::collections::BTreeMap::new(), created_at: String::new(), updated_at: String::new(), } @@ -431,6 +444,7 @@ mod tests { last_error: None, respond_to: RespondTo::default(), respond_to_allowlist: vec![], + env_vars: std::collections::BTreeMap::new(), } }