diff --git a/crates/sprout-acp/README.md b/crates/sprout-acp/README.md index ecd66bd66..b72c2f6ad 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 new file mode 100644 index 000000000..5bec46770 --- /dev/null +++ b/crates/sprout-acp/src/base_prompt.md @@ -0,0 +1,50 @@ +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. + +## Sprout CLI + +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. + +## Communication Patterns + +- Address agents and humans with plain `@name` — do NOT bold or italicize mention text (formatting prevents alert delivery). +- Use `sprout messages thread` when responding in-thread; post new messages for new topics. +- No push notifications — poll with `sprout messages get --channel --since `. When `since` is set without `before`, results are oldest-first (chronological). + +## Startup Recovery + +1. `sprout feed get` — surface pending mentions and action items. Filter by type: `mentions`, `needs_action`, `activity`, `agent_activity`. +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. + +## Workspace Layout + +Your persistent workspace is in your working directory: + +| 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. `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 fe2d2fd80..2a5053a05 100644 --- a/crates/sprout-acp/src/config.rs +++ b/crates/sprout-acp/src/config.rs @@ -352,6 +352,20 @@ pub struct CliArgs { )] pub no_memory: 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")] @@ -461,6 +475,12 @@ 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, + /// 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. @@ -590,6 +610,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"); @@ -798,6 +834,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_content, }; Ok(config) @@ -1161,6 +1199,8 @@ mod tests { persona_env_vars: vec![], relay_observer: false, agent_owner: None, + no_base_prompt: false, + base_prompt_content: None, } } diff --git a/crates/sprout-acp/src/lib.rs b/crates/sprout-acp/src/lib.rs index 9de5586bd..01b3496f3 100644 --- a/crates/sprout-acp/src/lib.rs +++ b/crates/sprout-acp/src/lib.rs @@ -24,7 +24,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, @@ -794,7 +794,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 @@ -1069,6 +1069,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,6 +1077,13 @@ 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 config.no_base_prompt { + None + } else if let Some(content) = base_prompt_content { + Some(Box::leak(content.into_boxed_str())) + } else { + Some(include_str!("base_prompt.md")) + }, heartbeat_prompt: config.heartbeat_prompt.clone(), cwd: std::env::current_dir() .unwrap_or_else(|_| std::path::PathBuf::from("/")) @@ -2314,6 +2322,10 @@ fn dispatch_heartbeat( .heartbeat_prompt .clone() .unwrap_or_else(default_heartbeat_prompt); + let prompt_text = match ctx.base_prompt { + Some(bp) => prepend_base_prompt(bp, &prompt_text), + None => prompt_text, + }; let result_tx = pool.result_tx(); let ctx_clone = Arc::clone(ctx); let agent_index = agent.index; @@ -2344,14 +2356,15 @@ fn default_heartbeat_prompt() -> String { You have been awakened for a routine heartbeat. You have NO incoming messages or\n\ active channel context for this turn.\n\n\ Your tasks:\n\ - 1. Call `get_feed(types='needs_action')` to check for pending workflow approvals or\n\ + 1. Run `sprout feed get --types needs_action` to check for pending workflow approvals or\n\ high-priority requests addressed to you.\n\ - 2. Call `get_feed(types='mentions')` to check for unanswered @mentions.\n\ - 3. If you find actionable items, address them using the appropriate tools\n\ - (e.g., `approve_step`, `send_message`, `send_message(parent_event_id=...)`).\n\ + 2. Run `sprout feed get --types mentions` to check for unanswered @mentions.\n\ + 3. If you find actionable items, address them using the appropriate CLI commands\n\ + (e.g., `sprout workflows approve --token `, `sprout messages send`,\n\ + `sprout messages send --reply-to `).\n\ 4. If there are no pending actions or mentions, end your turn immediately.\n\n\ - Do not call `list_channels()` or `search()` unless you have a specific reason.\n\ - Do not invent work — only act on items surfaced by the feed tools." + Do not run `sprout channels list` or `sprout messages search` unless you have a specific reason.\n\ + Do not invent work — only act on items surfaced by the feed commands." ) } @@ -2598,6 +2611,9 @@ async fn run_models(args: ModelsArgs) -> Result<()> { } fn build_mcp_servers(config: &Config) -> Vec { + if config.mcp_command.is_empty() { + return vec![]; + } vec![McpServer { name: "sprout-mcp".to_string(), command: config.mcp_command.clone(), @@ -2808,6 +2824,8 @@ mod build_mcp_servers_tests { persona_env_vars: vec![], relay_observer: false, agent_owner: None, + no_base_prompt: false, + base_prompt_content: None, } } @@ -2862,4 +2880,15 @@ mod build_mcp_servers_tests { "empty SPROUT_AUTH_TAG should not be forwarded" ); } + + #[test] + fn empty_mcp_command_returns_no_servers() { + let mut config = test_config(); + config.mcp_command = "".into(); + let servers = build_mcp_servers(&config); + assert!( + servers.is_empty(), + "empty mcp_command should produce no MCP servers" + ); + } } diff --git a/crates/sprout-acp/src/pool.rs b/crates/sprout-acp/src/pool.rs index 1397e7f23..b55eb481e 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}; @@ -192,6 +192,13 @@ 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). pub rest_client: RestClient, @@ -824,11 +831,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) => prepend_base_prompt(bp, initial_msg), + 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, ) @@ -952,11 +964,14 @@ pub async fn run_prompt_task( let agent_core_section = agent.state.core_sections.get(&b.channel_id).cloned(); crate::queue::format_prompt( b, - ctx.system_prompt.as_deref(), - agent_core_section.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(), + agent_core: agent_core_section.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 4fb5865d6..97afa316c 100644 --- a/crates/sprout-acp/src/queue.rs +++ b/crates/sprout-acp/src/queue.rs @@ -816,14 +816,14 @@ fn format_event_block( /// Append a reply instruction when the agent is responding to a thread event. /// -/// Tells the agent to pass the triggering event's ID as `parent_event_id` on -/// every tool call in this turn, and to leave `broadcast_to_channel` unset so -/// replies stay inside the thread. +/// Tells the agent to pass `--reply-to ` on every `sprout messages +/// send` call in this turn, and not to broadcast to the channel so replies +/// stay inside the thread. fn append_reply_instruction(s: &mut String, event_id: &str) { s.push_str(&format!( - "\nIMPORTANT: When responding, pass parent_event_id=\"{event_id}\" \ - on EVERY send_message and send_diff_message call in this turn. \ - Do not set broadcast_to_channel." + "\nIMPORTANT: When responding, use `--reply-to {event_id}` \ + on EVERY `sprout messages send` call in this turn. \ + Do not broadcast to the channel." )); } @@ -845,16 +845,16 @@ fn format_context_hints( // and the scope should be "dm" (not "thread") because the agent is in a DM. 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 replies use thread command because /messages excludes thread replies. + // DM non-replies use get for recent conversation. let ctx_hint = if has_conversation_context && is_reply { - "Thread context included below. Use get_thread() for full history if truncated." + "Thread context included below. Use `sprout messages thread --channel --event ` 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 `sprout messages get --channel ` for full history if truncated." } else if is_reply { - "Use get_thread() to fetch the reply chain." + "Use `sprout messages thread --channel --event ` to fetch the reply chain." } else { - "Use get_channel_history() for conversation context." + "Use `sprout messages get --channel ` for conversation context." }; let mut s = format!( "[Context]\n\ @@ -877,9 +877,9 @@ fn format_context_hints( s } else if let Some(ref root) = thread_tags.root_event_id { let ctx_hint = if has_conversation_context { - "Thread context included below. Use get_thread() for full history if truncated." + "Thread context included below. Use `sprout messages thread --channel --event ` for full history if truncated." } else { - "Use get_thread() to fetch thread context." + "Use `sprout messages thread --channel --event ` to fetch thread 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 `sprout messages get --channel ` for recent messages if needed." ) } } @@ -942,21 +942,35 @@ 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 agent_core: Option<&'a str>, + pub channel_info: Option<&'a PromptChannelInfo>, + pub conversation_context: Option<&'a ConversationContext>, + 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): -/// 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, - system_prompt: Option<&str>, - agent_core: 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 @@ -969,19 +983,25 @@ 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(4); + let mut sections: Vec = Vec::with_capacity(7); + + // 0. Base prompt (platform-level, always first). + 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}")); } // 1b. NIP-AE agent core memory (rendered by `engram_fetch::build_core_section`). - if let Some(core) = agent_core { + if let Some(core) = args.agent_core { sections.push(core.to_string()); } @@ -993,16 +1013,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). @@ -1013,7 +1033,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); @@ -1027,13 +1047,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 { @@ -1051,7 +1071,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 +1325,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 +1421,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 +1452,10 @@ mod tests { let prompt = format_prompt( &batch, - Some("You are a triage bot."), - None, - 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]")); } @@ -1457,7 +1476,14 @@ mod tests { cancelled_events: vec![], }; let core = "[Agent Memory — core]\nbe helpful"; - let prompt = format_prompt(&batch, Some("sys"), Some(core), None, None, None); + let prompt = format_prompt( + &batch, + &FormatPromptArgs { + system_prompt: Some("sys"), + agent_core: Some(core), + ..Default::default() + }, + ); assert!( prompt.contains("[System]\nsys\n\n[Agent Memory — core]\nbe helpful"), "expected core block after [System]; got: {prompt}" @@ -1478,10 +1504,112 @@ mod tests { cancelled_events: vec![], }; let core = "[Agent Memory — core]\nbe helpful"; - let prompt = format_prompt(&batch, None, Some(core), None, None, None); + let prompt = format_prompt( + &batch, + &FormatPromptArgs { + agent_core: Some(core), + ..Default::default() + }, + ); assert!(prompt.starts_with("[Agent Memory — core]\nbe helpful\n\n[Context]")); } + // ── Test 11c: 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, + &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, + &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, &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 ───────────────── #[test] @@ -1959,7 +2087,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")); } @@ -1982,7 +2116,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")); } @@ -2008,7 +2148,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")); } @@ -2051,7 +2191,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")); @@ -2084,7 +2230,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?")); @@ -2136,7 +2289,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( @@ -2231,16 +2391,23 @@ 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 the thread command, not get. assert!( - prompt.contains("get_thread()"), - "DM reply hint should mention get_thread(), got:\n{prompt}" + prompt.contains("sprout messages thread"), + "DM reply hint should mention `sprout messages thread`, got:\n{prompt}" ); // Thread structural info should be present. assert!( @@ -2252,7 +2419,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 { @@ -2270,15 +2437,21 @@ 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("sprout messages get"), + "DM non-reply hint should mention `sprout messages get`" ); assert!( - !prompt.contains("get_thread()"), - "DM non-reply should NOT mention get_thread()" + !prompt.contains("sprout messages thread"), + "DM non-reply should NOT mention `sprout messages thread`" ); } @@ -2297,7 +2470,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" @@ -2320,7 +2493,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" @@ -2342,7 +2515,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" @@ -2666,13 +2839,13 @@ 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}\"")), + prompt.contains(&format!("--reply-to {event_id}")), "channel thread reply should include reply instruction with triggering event ID" ); assert!( - prompt.contains("Do not set broadcast_to_channel"), + prompt.contains("Do not broadcast to the channel"), "channel thread reply should include broadcast suppression hint" ); } @@ -2700,9 +2873,15 @@ 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}\"")), + prompt.contains(&format!("--reply-to {event_id}")), "DM thread reply should include reply instruction" ); } @@ -2721,9 +2900,9 @@ 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"), + !prompt.contains("--reply-to"), "top-level message should NOT include reply instruction" ); } @@ -2746,9 +2925,15 @@ 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"), + !prompt.contains("--reply-to"), "DM non-reply should NOT include reply instruction" ); } @@ -2776,18 +2961,18 @@ 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}\"")), + prompt.contains(&format!("--reply-to {event_id}")), "nested reply instruction should use the triggering event ID" ); assert!( - !prompt.contains(&format!("parent_event_id=\"{root_id}\"")), + !prompt.contains(&format!("--reply-to {root_id}")), "instruction should NOT use root_event_id" ); assert!( - !prompt.contains(&format!("parent_event_id=\"{parent_id}\"")), + !prompt.contains(&format!("--reply-to {parent_id}")), "instruction should NOT use parent_event_id from tags" ); } @@ -2819,9 +3004,9 @@ 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}\"")), + prompt.contains(&format!("--reply-to {threaded_id}")), "batched prompt should use last (threaded) event's ID" ); } @@ -2852,9 +3037,9 @@ 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"), + !prompt.contains("--reply-to"), "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 cbbeaecff..f29fa3220 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 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 + +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/scripts/check-file-sizes.mjs b/desktop/scripts/check-file-sizes.mjs index d93c83bf6..ca00655cc 100644 --- a/desktop/scripts/check-file-sizes.mjs +++ b/desktop/scripts/check-file-sizes.mjs @@ -30,7 +30,8 @@ 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/nest.rs", 1420], // version-gated AGENTS.md + SKILL.md refresh + .agents/.claude symlink migration + ensure_skill_symlinks (all known providers) + managed section upsert + dynamic agent context + tests + ["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", 815], // message edit state + handlers + ChannelPane edit prop threading + scrollback pagination + workflows view + projects view + memory-leak safeguards + home-badge state lifted here so it consumes the same NIP-RS read-state as the sidebar (single ReadStateManager) diff --git a/desktop/src-tauri/src/commands/agent_models.rs b/desktop/src-tauri/src/commands/agent_models.rs index 55b6207b4..fa8a41281 100644 --- a/desktop/src-tauri/src/commands/agent_models.rs +++ b/desktop/src-tauri/src/commands/agent_models.rs @@ -9,7 +9,8 @@ use crate::{ 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, + try_regenerate_nest, AgentModelInfo, AgentModelsResponse, UpdateManagedAgentRequest, + UpdateManagedAgentResponse, }, relay::{relay_ws_url_with_override, sync_managed_agent_profile}, util::now_iso, @@ -246,6 +247,8 @@ pub async fn update_managed_agent( (summary, sync_params) }; // lock dropped here + try_regenerate_nest(&app); + // 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 7e663ed41..c666af663 100644 --- a/desktop/src-tauri/src/commands/agents.rs +++ b/desktop/src-tauri/src/commands/agents.rs @@ -9,10 +9,11 @@ use crate::{ 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, + 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, }, relay::{relay_ws_url_with_override, sync_managed_agent_profile}, util::now_iso, @@ -334,6 +335,19 @@ pub async fn create_managed_agent( .collect::>(), ); + let mcp_command = input + .mcp_command + .as_deref() + .map(str::trim) + .filter(|value| !value.is_empty()) + .map(str::to_string) + .unwrap_or_else( + || match crate::managed_agents::known_acp_provider(&agent_command) { + Some(p) => p.mcp_command.unwrap_or("").to_string(), + None => DEFAULT_MCP_COMMAND.to_string(), + }, + ); + // For pack-backed personas, resolve the installed pack path and the // persona's internal name (slug). ACP's resolve_persona_by_name() // matches on this internal name, NOT display_name. @@ -366,13 +380,7 @@ pub async fn create_managed_agent( .to_string(), agent_command, agent_args, - mcp_command: input - .mcp_command - .as_deref() - .map(str::trim) - .filter(|value| !value.is_empty()) - .unwrap_or(DEFAULT_MCP_COMMAND) - .to_string(), + mcp_command, turn_timeout_seconds: input .turn_timeout_seconds .filter(|seconds| *seconds > 0) @@ -452,6 +460,8 @@ pub async fn create_managed_agent( (agent, spawn_error) }; + try_regenerate_nest(&app); + // ── Phase 4: sync agent profile on relay (async, outside lock) ─────────── let avatar_url = input .avatar_url @@ -672,48 +682,52 @@ 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) + try_regenerate_nest(&app); + Ok(()) } #[tauri::command] diff --git a/desktop/src-tauri/src/commands/personas.rs b/desktop/src-tauri/src/commands/personas.rs index f5ece7f01..b3d0d4504 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, 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,6 +85,7 @@ pub fn create_persona( }; personas.push(persona.clone()); save_personas(&app, &personas)?; + try_regenerate_nest(&app); Ok(persona) } @@ -125,19 +126,18 @@ 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)?; - personas + let result = personas .into_iter() .find(|record| record.id == input.id) - .ok_or_else(|| format!("persona {} disappeared unexpectedly", input.id)) + .ok_or_else(|| format!("persona {} disappeared unexpectedly", input.id))?; + try_regenerate_nest(&app); + Ok(result) } #[tauri::command] @@ -182,6 +182,7 @@ pub fn delete_persona( if changed_agents { save_managed_agents(&app, &agents)?; } + try_regenerate_nest(&app); Ok(()) } @@ -230,6 +231,7 @@ pub fn set_persona_active( let updated = persona.clone(); save_personas(&app, &personas)?; + try_regenerate_nest(&app); Ok(updated) } @@ -383,7 +385,9 @@ 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)?; + try_regenerate_nest(&app); + Ok(result) } #[tauri::command] @@ -396,7 +400,9 @@ 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)?; + try_regenerate_nest(&app); + Ok(()) } #[tauri::command] diff --git a/desktop/src-tauri/src/commands/workspace.rs b/desktop/src-tauri/src/commands/workspace.rs index e75d1801d..79ca43f97 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::try_regenerate_nest; 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,7 @@ pub fn apply_workspace( *keys_guard = keys; } + try_regenerate_nest(&app); + Ok(()) } diff --git a/desktop/src-tauri/src/lib.rs b/desktop/src-tauri/src/lib.rs index ffc3efafb..eb204ff54 100644 --- a/desktop/src-tauri/src/lib.rs +++ b/desktop/src-tauri/src/lib.rs @@ -26,7 +26,7 @@ use huddle::{ use managed_agents::{ 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}, @@ -403,6 +403,8 @@ pub fn run() { } } + 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 // already downloaded. ~289 MB total (~100 MB Parakeet STT + ~189 MB Pocket TTS). diff --git a/desktop/src-tauri/src/managed_agents/discovery.rs b/desktop/src-tauri/src/managed_agents/discovery.rs index 4a47f061c..d58fdef2f 100644 --- a/desktop/src-tauri/src/managed_agents/discovery.rs +++ b/desktop/src-tauri/src/managed_agents/discovery.rs @@ -14,6 +14,8 @@ pub(crate) struct KnownAcpProvider { pub aliases: &'static [&'static str], pub avatar_url: &'static str, /// MCP server binary to use instead of the default `sprout-mcp-server`. + /// `None` means this provider does not need a Sprout MCP server — + /// no MCP tools will be registered for the agent session. pub mcp_command: Option<&'static str>, /// Whether to enable MCP hook tools (`_Stop`, `_PostCompact`) for this agent. pub mcp_hooks: bool, @@ -29,6 +31,11 @@ pub(crate) struct KnownAcpProvider { pub cli_install_hint: &'static str, /// Human-readable hint about installing the ACP adapter. pub adapter_install_hint: &'static str, + /// Harness-specific skill discovery directory (e.g. `.goose/skills`). + /// `Some(dir)` → Sprout creates a symlink at `//sprout-cli` + /// pointing to the canonical `.agents/skills/sprout-cli`. `None` → this + /// provider reads the canonical path directly or has no skill support. + pub skill_dir: Option<&'static str>, } const GOOSE_AVATAR_URL: &str = "https://goose-docs.ai/img/logo_dark.png"; @@ -74,6 +81,7 @@ const KNOWN_ACP_PROVIDERS: &[KnownAcpProvider] = &[ install_instructions_url: "https://block.github.io/goose/", cli_install_hint: "Install Goose via the official install script.", adapter_install_hint: "", + skill_dir: Some(".goose/skills"), }, KnownAcpProvider { id: "claude", @@ -89,6 +97,7 @@ const KNOWN_ACP_PROVIDERS: &[KnownAcpProvider] = &[ install_instructions_url: "https://github.com/agentclientprotocol/claude-agent-acp", cli_install_hint: "Install the Claude Code CLI via the official install script.", adapter_install_hint: "Install the Claude Code ACP adapter via npm.", + skill_dir: Some(".claude/skills"), }, KnownAcpProvider { id: "codex", @@ -104,6 +113,7 @@ const KNOWN_ACP_PROVIDERS: &[KnownAcpProvider] = &[ install_instructions_url: "https://github.com/zed-industries/codex-acp", cli_install_hint: "Install the Codex CLI via the official install script.", adapter_install_hint: "Install the Codex ACP adapter via npm.", + skill_dir: Some(".codex/skills"), }, KnownAcpProvider { id: "sprout-agent", @@ -119,9 +129,15 @@ const KNOWN_ACP_PROVIDERS: &[KnownAcpProvider] = &[ install_instructions_url: "https://github.com/block/sprout", cli_install_hint: "Ships with the Sprout desktop app.", adapter_install_hint: "", + skill_dir: None, }, ]; +/// Skill discovery directories declared by known providers. +pub(crate) fn known_skill_dirs() -> impl Iterator { + KNOWN_ACP_PROVIDERS.iter().filter_map(|p| p.skill_dir) +} + fn workspace_root_dir() -> PathBuf { PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../..") } diff --git a/desktop/src-tauri/src/managed_agents/nest.rs b/desktop/src-tauri/src/managed_agents/nest.rs index 25639b8d9..563ac462d 100644 --- a/desktop/src-tauri/src/managed_agents/nest.rs +++ b/desktop/src-tauri/src/managed_agents/nest.rs @@ -4,10 +4,20 @@ //! Sprout-spawned agent starts with orientation (AGENTS.md) and a //! place to accumulate research, plans, and logs across sessions. //! -//! Idempotent: existing files and directories are never overwritten. +//! Static template content in AGENTS.md (above the managed-section markers) +//! and SKILL.md is refreshed when the embedded template version changes. +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; +use std::io; use std::path::{Path, PathBuf}; +use tauri::{AppHandle, Manager}; + +use crate::managed_agents::discovery::known_skill_dirs; /// Subdirectories created inside the nest. const NEST_DIRS: &[&str] = &[ @@ -24,10 +34,24 @@ const NEST_DIRS: &[&str] = &[ /// Fully static — no runtime interpolation, no secrets, no user paths. const AGENTS_MD: &str = include_str!("nest_agents.md"); -/// Default SKILL.md content for the sprout-cli Claude Code skill. -/// Written to ~/.sprout/.claude/skills/sprout-cli/SKILL.md on first init. +/// Default SKILL.md content for the sprout-cli skill. +/// Written to ~/.sprout/.agents/skills/sprout-cli/SKILL.md on first init. const SPROUT_CLI_SKILL_MD: &str = include_str!("nest_skill.md"); +/// Template content version for AGENTS.md static content (above managed markers). +/// Bump this when changing `nest_agents.md` to trigger refresh on existing installs. +/// Version 1 is implicitly "before this mechanism existed" (no version file). +const NEST_AGENTS_VERSION: u32 = 3; + +/// Template content version for SKILL.md. +/// Bump this when changing `nest_skill.md` to trigger refresh on existing installs. +const NEST_SKILL_VERSION: u32 = 3; + +const BEGIN_MARKER: &str = ""; + +/// Canonical skill directory path relative to the nest root. +const CANONICAL_SKILL_DIR: &str = ".agents/skills/sprout-cli"; /// Returns the nest root path (`~/.sprout`), or `None` if the home /// directory cannot be resolved. pub fn nest_dir() -> Option { @@ -47,12 +71,16 @@ pub fn ensure_nest() -> Result<(), String> { /// /// - Creates the root directory and all subdirectories. /// - Writes `AGENTS.md` only if it doesn't already exist. -/// - Writes `.claude/skills/sprout-cli/SKILL.md` only if it doesn't already exist. +/// - Writes `.agents/skills/sprout-cli/SKILL.md` only if it doesn't already exist. +/// - Creates harness-specific symlinks pointing to the canonical +/// `.agents/skills/sprout-cli` directory for each known provider. /// - Sets 700 permissions on the root, all subdirectories, and the skill /// directory tree (Unix). /// -/// Idempotent: safe to call on every launch. Existing files are never -/// overwritten — users can freely edit AGENTS.md or SKILL.md and they persist. +/// Idempotent: safe to call on every launch. Static template content in +/// AGENTS.md (above the managed-section markers) and SKILL.md is refreshed +/// when the embedded template version changes. The managed section in AGENTS.md +/// and any user content below it are preserved. /// /// Rejects symlinks at the root path to prevent redirect attacks. /// @@ -104,11 +132,14 @@ pub fn ensure_nest_at(root: &Path) -> Result<(), String> { } } - // Write sprout-cli skill alongside AGENTS.md (same idempotent pattern). - let skill_dir = root.join(".claude/skills/sprout-cli"); - fs::create_dir_all(&skill_dir).map_err(|e| format!("create {}: {e}", skill_dir.display()))?; + // Write sprout-cli skill to the harness-agnostic .agents path. + // The first-init write uses the new canonical path; migration from + // the old .claude path is handled in refresh_skill_md_if_stale. + let agents_skill_dir = root.join(CANONICAL_SKILL_DIR); + fs::create_dir_all(&agents_skill_dir) + .map_err(|e| format!("create {}: {e}", agents_skill_dir.display()))?; - let skill_md = root.join(".claude/skills/sprout-cli/SKILL.md"); + let skill_md = agents_skill_dir.join("SKILL.md"); match fs::OpenOptions::new() .write(true) .create_new(true) @@ -125,6 +156,16 @@ pub fn ensure_nest_at(root: &Path) -> Result<(), String> { } } + // Create harness-specific symlinks for all known providers. + // Migration of the old .claude/skills/sprout-cli real dir is handled in + // refresh_skill_md_if_stale; ensure_skill_symlinks skips paths that already exist. + ensure_skill_symlinks(root)?; + + // Refresh static content if the embedded template version is newer. + refresh_agents_md_if_stale(root)?; + refresh_skill_md_if_stale(root)?; + + // Set owner-only permissions on root and all subdirectories. // Skip any path that is a symlink — chmod would affect the target. #[cfg(unix)] @@ -144,14 +185,25 @@ pub fn ensure_nest_at(root: &Path) -> Result<(), String> { .map_err(|e| format!("set permissions on {}: {e}", path.display()))?; } } - // Skill directory and its intermediate parents inside root get 700. - // create_dir_all creates .claude/ and .claude/skills/ with umask - // defaults — lock them down the same way we do NEST_DIRS. - for dir in [ - root.join(".claude"), - root.join(".claude/skills"), - skill_dir.clone(), - ] { + // Skill directory trees inside root get 700. + // Build the list from canonical path + all known provider skill dirs. + let mut skill_perm_dirs = Vec::new(); + { + let mut accumulated = std::path::PathBuf::new(); + for component in std::path::Path::new(CANONICAL_SKILL_DIR).components() { + accumulated.push(component); + skill_perm_dirs.push(root.join(&accumulated)); + } + } + for skill_dir in known_skill_dirs() { + // Ensure every ancestor dir gets 700, not just the leaf. + let mut accumulated = std::path::PathBuf::new(); + for component in std::path::Path::new(skill_dir).components() { + accumulated.push(component); + skill_perm_dirs.push(root.join(&accumulated)); + } + } + for dir in skill_perm_dirs { let is_symlink = dir .symlink_metadata() .map(|m| m.file_type().is_symlink()) @@ -166,6 +218,32 @@ pub fn ensure_nest_at(root: &Path) -> Result<(), String> { Ok(()) } +/// Create harness-specific skill symlinks for each known provider. +/// Idempotent: skips any path where `symlink_metadata` succeeds — real +/// directories, valid symlinks, and dangling symlinks are all left alone. +#[cfg(unix)] +fn ensure_skill_symlinks(root: &Path) -> Result<(), String> { + for skill_dir in known_skill_dirs() { + let parent = root.join(skill_dir); + fs::create_dir_all(&parent).map_err(|e| format!("create {}: {e}", parent.display()))?; + let link = parent.join("sprout-cli"); + if link.symlink_metadata().is_ok() { + continue; // symlink or real path exists — skip + } + let depth = std::path::Path::new(skill_dir).components().count(); + let prefix = "../".repeat(depth); + let target = format!("{prefix}{CANONICAL_SKILL_DIR}"); + std::os::unix::fs::symlink(&target, &link) + .map_err(|e| format!("symlink {} → {}: {e}", link.display(), target))?; + } + Ok(()) +} + +#[cfg(not(unix))] +fn ensure_skill_symlinks(_root: &Path) -> Result<(), String> { + Ok(()) +} + /// Ensures `~/.local/bin/sprout` is a symlink to the bundled CLI binary. /// /// Creates the symlink if it doesn't exist, updates it if it already points @@ -224,6 +302,301 @@ pub fn ensure_cli_symlink(_exe_parent: &Path) -> Result<(), String> { Ok(()) } +/// Read a version number from a file. Returns 0 if the file doesn't exist or can't be parsed. +fn read_version_file(path: &Path) -> u32 { + fs::read_to_string(path) + .ok() + .and_then(|s| s.trim().parse().ok()) + .unwrap_or(0) +} + +/// Refresh AGENTS.md static content if the template version has changed. +/// +/// Preserves everything from the `\n{new_section_content}\n{END_MARKER}\n" + ); + + let new_content = match find_managed_markers(¤t) { + Some((begin_line_start, after_end)) => { + format!( + "{}{}{}", + ¤t[..begin_line_start], + replacement, + ¤t[after_end..] + ) + } + None => { + let cleaned = strip_orphan_begin_marker(¤t); + format!("{}\n\n{}", cleaned.trim_end_matches('\n'), replacement) + } + }; + + // Skip write when content is unchanged — avoids bumping mtime on every launch. + if new_content == current { + return Ok(()); + } + + 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(()) +} + +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(()) +} + +/// 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::*; @@ -309,10 +682,28 @@ mod tests { let tmp = tempfile::tempdir().unwrap(); let root = tmp.path().join(".sprout"); ensure_nest_at(&root).unwrap(); - let skill = root.join(".claude/skills/sprout-cli/SKILL.md"); - assert!(skill.exists(), "SKILL.md should exist"); + + // Canonical location under .agents. + let skill = root.join(".agents/skills/sprout-cli/SKILL.md"); + assert!(skill.exists(), "SKILL.md should exist at .agents path"); let content = fs::read_to_string(&skill).unwrap(); assert_eq!(content, SPROUT_CLI_SKILL_MD); + + // On unix, harness-specific symlinks should resolve to the canonical dir. + #[cfg(unix)] + { + for dir in [".goose/skills", ".claude/skills", ".codex/skills"] { + let link = root.join(dir).join("sprout-cli"); + assert!( + link.symlink_metadata().unwrap().file_type().is_symlink(), + "{dir}/sprout-cli should be a symlink" + ); + assert!( + link.join("SKILL.md").exists(), + "symlink at {dir}/sprout-cli should resolve to dir with SKILL.md" + ); + } + } } #[test] @@ -320,8 +711,10 @@ mod tests { let tmp = tempfile::tempdir().unwrap(); let root = tmp.path().join(".sprout"); ensure_nest_at(&root).unwrap(); - let skill = root.join(".claude/skills/sprout-cli/SKILL.md"); + + let skill = root.join(".agents/skills/sprout-cli/SKILL.md"); fs::write(&skill, "custom skill content").unwrap(); + ensure_nest_at(&root).unwrap(); assert_eq!(fs::read_to_string(&skill).unwrap(), "custom skill content"); } @@ -333,8 +726,19 @@ mod tests { let tmp = tempfile::tempdir().unwrap(); let root = tmp.path().join(".sprout"); ensure_nest_at(&root).unwrap(); - // All three dirs in the skill path should be locked down. - for dir in [".claude", ".claude/skills", ".claude/skills/sprout-cli"] { + // Canonical path and all provider parent dirs should be locked down. + // Symlinks (e.g. .goose/skills/sprout-cli) are skipped by the chmod loop. + for dir in [ + ".agents", + ".agents/skills", + ".agents/skills/sprout-cli", + ".goose", + ".goose/skills", + ".claude", + ".claude/skills", + ".codex", + ".codex/skills", + ] { let path = root.join(dir); let mode = fs::metadata(&path).unwrap().permissions().mode() & 0o777; assert_eq!(mode, 0o700, "{dir} should be 700"); @@ -370,6 +774,131 @@ mod tests { ); } + #[cfg(unix)] + #[test] + fn ensure_nest_migrates_old_skill_dir() { + let tmp = tempfile::tempdir().unwrap(); + let root = tmp.path().join(".sprout"); + + // Simulate a pre-migration install: real directory at old path. + // Create the nest first to get all dirs, then simulate old layout. + ensure_nest_at(&root).unwrap(); + + // Remove the symlink and new skill dir, recreate old real dir. + let _ = fs::remove_file(root.join(".claude/skills/sprout-cli")); + let _ = fs::remove_dir_all(root.join(".agents/skills/sprout-cli")); + let old_skill_dir = root.join(".claude/skills/sprout-cli"); + fs::create_dir_all(&old_skill_dir).unwrap(); + fs::write(old_skill_dir.join("SKILL.md"), "user edited skill").unwrap(); + + // Delete version file to force refresh. + let _ = fs::remove_file(root.join(".agents/skills/sprout-cli/.skill-version")); + + // Re-run ensure_nest_at — should trigger migration in refresh_skill_md_if_stale. + ensure_nest_at(&root).unwrap(); + + // New canonical location exists with user's content preserved. + let new_skill = root.join(".agents/skills/sprout-cli/SKILL.md"); + assert!(new_skill.exists(), "SKILL.md should exist at new path"); + assert_eq!(fs::read_to_string(&new_skill).unwrap(), "user edited skill"); + + // Old path is now a symlink, not a real directory. + let old_path = root.join(".claude/skills/sprout-cli"); + assert!( + old_path + .symlink_metadata() + .unwrap() + .file_type() + .is_symlink(), + "old path should now be a symlink" + ); + } + + #[cfg(unix)] + #[test] + fn ensure_skill_symlinks_are_idempotent() { + let tmp = tempfile::tempdir().unwrap(); + let root = tmp.path().join(".sprout"); + ensure_nest_at(&root).unwrap(); + // Second call should succeed without errors. + ensure_nest_at(&root).unwrap(); + // All symlinks still valid and point to relative targets. + for dir in [".goose/skills", ".claude/skills", ".codex/skills"] { + let link = root.join(dir).join("sprout-cli"); + assert!(link.symlink_metadata().unwrap().file_type().is_symlink()); + assert!( + link.join("SKILL.md").exists(), + "symlink at {dir}/sprout-cli should resolve to dir with SKILL.md" + ); + let target = fs::read_link(&link).unwrap(); + assert_eq!( + target.to_str().unwrap(), + format!("../../{CANONICAL_SKILL_DIR}"), + "symlink at {dir}/sprout-cli should use relative target" + ); + } + } + + #[cfg(unix)] + #[test] + fn ensure_skill_symlinks_skips_existing_path_during_initial_pass() { + // ensure_skill_symlinks skips any path where symlink_metadata succeeds. + // However, refresh_skill_md_if_stale (called after ensure_skill_symlinks) + // migrates pre-existing real directories at .claude/skills/sprout-cli to + // symlinks. This test verifies the end-to-end behavior: a pre-existing real + // dir at the claude path is migrated to a symlink. + let tmp = tempfile::tempdir().unwrap(); + let root = tmp.path().join(".sprout"); + // Pre-create a real directory where a symlink would go. + let real_dir = root.join(".claude/skills/sprout-cli"); + fs::create_dir_all(&real_dir).unwrap(); + // Place SKILL.md so migration preserves it. + fs::write(real_dir.join("SKILL.md"), "custom skill content").unwrap(); + + ensure_nest_at(&root).unwrap(); + + // Migration converts the real dir to a symlink; content is moved to canonical path. + assert!( + real_dir + .symlink_metadata() + .unwrap() + .file_type() + .is_symlink(), + ".claude/skills/sprout-cli should be migrated to a symlink" + ); + // The canonical path now holds the migrated content. + let canonical = root.join(".agents/skills/sprout-cli/SKILL.md"); + assert_eq!( + fs::read_to_string(&canonical).unwrap(), + "custom skill content" + ); + } + + #[cfg(unix)] + #[test] + fn ensure_skill_symlinks_skip_dangling_symlink() { + let tmp = tempfile::tempdir().unwrap(); + let root = tmp.path().join(".sprout"); + // Pre-create a dangling symlink where the .codex link would go. + let codex_skills = root.join(".codex/skills"); + fs::create_dir_all(&codex_skills).unwrap(); + let dangling = codex_skills.join("sprout-cli"); + std::os::unix::fs::symlink("/nonexistent/target", &dangling).unwrap(); + + ensure_nest_at(&root).unwrap(); + + // Dangling symlink should be left alone (not clobbered). + assert!(dangling + .symlink_metadata() + .unwrap() + .file_type() + .is_symlink()); + assert_eq!( + fs::read_link(&dangling).unwrap().to_str().unwrap(), + "/nonexistent/target" + ); + } + #[cfg(unix)] #[test] fn ensure_cli_symlink_creates_symlink() { @@ -406,4 +935,481 @@ mod tests { // in the Ok(_) branch of ensure_cli_symlink skips regular files). assert_eq!(fs::read_to_string(&link).unwrap(), "user-installed binary"); } + + 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, + env_vars: std::collections::BTreeMap::new(), + 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![], + env_vars: std::collections::BTreeMap::new(), + } + } + + #[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("| Name | Persona | How to address |")); + assert!(output.contains("## Workspace")); + } + + #[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("\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!( + 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" + ); + } + + #[test] + fn refresh_agents_md_writes_version_file() { + let tmp = tempfile::tempdir().unwrap(); + let root = tmp.path().join(".sprout"); + ensure_nest_at(&root).unwrap(); + let version = fs::read_to_string(root.join(".nest-agents-version")).unwrap(); + assert_eq!(version.trim(), NEST_AGENTS_VERSION.to_string()); + } + + #[test] + fn refresh_skill_md_writes_version_file() { + let tmp = tempfile::tempdir().unwrap(); + let root = tmp.path().join(".sprout"); + ensure_nest_at(&root).unwrap(); + let version = + fs::read_to_string(root.join(".agents/skills/sprout-cli/.skill-version")).unwrap(); + assert_eq!(version.trim(), NEST_SKILL_VERSION.to_string()); + } + + #[test] + fn refresh_agents_md_preserves_managed_section() { + let tmp = tempfile::tempdir().unwrap(); + let root = tmp.path().join(".sprout"); + ensure_nest_at(&root).unwrap(); + + // Simulate a managed section update. + let agents_md = root.join("AGENTS.md"); + upsert_managed_section( + &agents_md, + "## Active Agents\n\n| Name | Role |\n|------|------|\n| Kit | Builder |", + ) + .unwrap(); + + // Remove version file to simulate an upgrade. + fs::remove_file(root.join(".nest-agents-version")).unwrap(); + + // Re-run ensure_nest_at (triggers refresh). + ensure_nest_at(&root).unwrap(); + + let content = fs::read_to_string(&agents_md).unwrap(); + // Static content should be refreshed (from template). + assert!( + content.starts_with("# Sprout Nest"), + "template header must be present" + ); + // Managed section should be preserved. + assert!( + content.contains("Kit"), + "managed section agent table must survive refresh" + ); + assert!(content.contains(BEGIN_MARKER), "BEGIN marker must survive"); + assert!(content.contains(END_MARKER), "END marker must survive"); + } + + #[test] + fn refresh_skips_when_version_current() { + let tmp = tempfile::tempdir().unwrap(); + let root = tmp.path().join(".sprout"); + ensure_nest_at(&root).unwrap(); + + // Manually change AGENTS.md content after version file is written. + let agents_md = root.join("AGENTS.md"); + fs::write(&agents_md, "user modified content").unwrap(); + + // Re-run ensure_nest_at — version file is current, so no refresh. + ensure_nest_at(&root).unwrap(); + + let content = fs::read_to_string(&agents_md).unwrap(); + assert_eq!( + content, "user modified content", + "should not overwrite when version is current" + ); + } + + #[test] + fn refresh_skill_overwrites_on_version_bump() { + let tmp = tempfile::tempdir().unwrap(); + let root = tmp.path().join(".sprout"); + ensure_nest_at(&root).unwrap(); + + let skill_md = root.join(".agents/skills/sprout-cli/SKILL.md"); + fs::write(&skill_md, "stale skill content").unwrap(); + + // Remove version file to simulate upgrade. + let _ = fs::remove_file(root.join(".agents/skills/sprout-cli/.skill-version")); + + ensure_nest_at(&root).unwrap(); + + let content = fs::read_to_string(&skill_md).unwrap(); + assert_eq!( + content, SPROUT_CLI_SKILL_MD, + "SKILL.md must be refreshed on version bump" + ); + } } diff --git a/desktop/src-tauri/src/managed_agents/nest_agents.md b/desktop/src-tauri/src/managed_agents/nest_agents.md index 463a6b068..0a8f1ef02 100644 --- a/desktop/src-tauri/src/managed_agents/nest_agents.md +++ b/desktop/src-tauri/src/managed_agents/nest_agents.md @@ -1,6 +1,6 @@ # Sprout Nest -Your persistent workspace. Created once by the Sprout desktop app — never overwritten. Edit freely. +Your persistent workspace. Created once by the Sprout desktop app. The static content above the managed-section markers is regenerated on upgrades — add custom notes below the markers or in separate files. ## Directory Layout @@ -16,32 +16,7 @@ Your persistent workspace. Created once by the Sprout desktop app — never over Filenames: `ALL_CAPS_WITH_UNDERSCORES.md` (e.g., `OAUTH_FLOW_NOTES.md`). -## Communicating via Sprout - -You have MCP tools for channels. Use them. - -**Read messages:** -- `get_messages(channel_id, limit=50)` — recent history (max 200) -- `get_thread(channel_id, event_id)` — drill into a thread -- `get_feed()` — personalized: your mentions, needs-action items - -**Post messages:** -- `send_message(channel_id, content)` — new message -- `send_message(channel_id, content, parent_event_id)` — threaded reply - -**Poll for new messages** (no push — poll with sleep): -- Call `get_messages(channel_id, since=)` where the value is the `created_at` timestamp of the last message you saw -- When `since` is set without `before`, results are **oldest-first** (chronological) -- Sleep 10–30 seconds between polls - -**Search:** -- `search(q="your query")` — searches across all channels - -## Recovering Context on Startup - -1. Call `get_feed()` — surface mentions and items needing your action -2. Call `get_messages` on your assigned channel(s) to read recent history -3. Check `RESEARCH/`, `PLANS/`, `GUIDES/` before researching from scratch +The `sprout` CLI is your primary tool interface — run `sprout --help` for commands. The CLI skill file has the full reference. ## Knowledge File Conventions @@ -69,4 +44,10 @@ created: 2026-01-15 - **`.scratch/` is disposable** — don't rely on it across sessions - **Never push without approval** — do not `git push` to any remote - **Stay on task** — only stage files relevant to your current work -- **Tagging or @mentioning others** — you can mention other bots or users by simply @'ing them in your message, but you cannot bold, italicize, or otherwise format the mention text if you want them to actually be alerted + + +## Active Agents + +*(No agents deployed yet. Add agents in the Sprout desktop app.)* + + diff --git a/desktop/src-tauri/src/managed_agents/personas.rs b/desktop/src-tauri/src/managed_agents/personas.rs index b3cc61ae6..2bd74c3bc 100644 --- a/desktop/src-tauri/src/managed_agents/personas.rs +++ b/desktop/src-tauri/src/managed_agents/personas.rs @@ -14,6 +14,8 @@ struct BuiltInPersona { avatar_url: Option<&'static str>, 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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"; @@ -110,7 +112,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 +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: None, + provider: None, }, BuiltInPersona { id: "builtin:kit", @@ -269,7 +272,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 +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: None, + provider: None, }, BuiltInPersona { id: "builtin:scout", @@ -452,6 +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: None, + provider: None, }, ]; @@ -467,8 +473,8 @@ 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, - model: 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, is_active: true, @@ -517,13 +523,16 @@ fn merge_personas(mut stored: Vec, now: &str) -> (Vec = if record.mcp_command.is_empty() { + None + } else { + Some( + resolve_command(&record.mcp_command, Some(app)).ok_or_else(|| { + missing_command_message(&record.mcp_command, "MCP server command") + })?, + ) + }; // Resolve agent command to a full path (DMG launches have minimal PATH). let resolved_agent_command = resolve_command(&record.agent_command, Some(app)) .map(|p| p.display().to_string()) @@ -575,7 +582,14 @@ pub fn spawn_agent_child( command.env("SPROUT_RELAY_URL", &record.relay_url); command.env("SPROUT_ACP_AGENT_COMMAND", &resolved_agent_command); command.env("SPROUT_ACP_AGENT_ARGS", agent_args.join(",")); - command.env("SPROUT_ACP_MCP_COMMAND", &resolved_mcp_command); + match &resolved_mcp_command { + Some(mcp_cmd) => { + command.env("SPROUT_ACP_MCP_COMMAND", mcp_cmd); + } + None => { + command.env("SPROUT_ACP_MCP_COMMAND", ""); + } + } // Enable MCP hook tools (_Stop, _PostCompact) for agents that need them. // Uses "*" because build_mcp_servers() hard-codes the server name to "sprout-mcp". if known_acp_provider(&record.agent_command).is_some_and(|p| p.mcp_hooks) {