Skip to main content

aether_cli/agent/new_agent_wizard/
draft_agent_entry.rs

1use aether_project::{AgentEntry, McpServerEntry, Settings};
2use std::{
3    fs::{create_dir_all, read_to_string, write},
4    path::{Path, PathBuf},
5};
6
7use super::new_agent_step::{NewAgentMode, PromptFile};
8use crate::error::CliError;
9
10pub struct DraftAgentEntry {
11    pub entry: AgentEntry,
12    pub system_md_content: String,
13    pub system_md_edited: bool,
14    pub workspace_mcp_configs: Vec<String>,
15}
16
17impl DraftAgentEntry {
18    pub fn slug(&self) -> String {
19        self.entry.name.to_lowercase().replace(' ', "-")
20    }
21
22    pub fn generated_paths(&self, mode: &NewAgentMode) -> GeneratedPaths {
23        let filename = format!("{}.md", self.slug().to_uppercase());
24        match mode {
25            NewAgentMode::ScaffoldProject => GeneratedPaths {
26                system_md: PathBuf::from(format!(".aether/{filename}")),
27                mcp_json: PathBuf::from(".aether/mcp.json"),
28            },
29            NewAgentMode::AddAgentToExistingProject => {
30                let slug = self.slug();
31                GeneratedPaths {
32                    system_md: PathBuf::from(format!(".aether/agents/{slug}/{filename}")),
33                    mcp_json: PathBuf::from(format!(".aether/agents/{slug}/mcp.json")),
34                }
35            }
36        }
37    }
38
39    pub fn to_agent_entry(&self, mode: &NewAgentMode, inherited_prompts: &[String]) -> AgentEntry {
40        let paths = self.generated_paths(mode);
41
42        let mut prompts = vec![paths.system_md.to_string_lossy().to_string()];
43        match mode {
44            NewAgentMode::ScaffoldProject => {
45                prompts.extend(self.entry.prompts.iter().cloned());
46            }
47            NewAgentMode::AddAgentToExistingProject => {
48                for name in &self.entry.prompts {
49                    if !inherited_prompts.iter().any(|d| d == name) {
50                        prompts.push(name.clone());
51                    }
52                }
53            }
54        }
55
56        let mut mcp_servers: Vec<McpServerEntry> = if self.entry.mcp_servers.is_empty() {
57            vec![]
58        } else {
59            vec![McpServerEntry::Path(paths.mcp_json.to_string_lossy().to_string())]
60        };
61
62        mcp_servers.extend(self.workspace_mcp_configs.iter().map(|s| McpServerEntry::Path(s.clone())));
63
64        AgentEntry { prompts, mcp_servers, ..self.entry.clone() }
65    }
66
67    pub fn to_settings(&self, mode: &NewAgentMode, existing: Option<&str>) -> Settings {
68        match mode {
69            NewAgentMode::ScaffoldProject => {
70                let entry = self.to_agent_entry(mode, &[]);
71                Settings { prompts: vec![], mcp_servers: vec![], agents: vec![entry] }
72            }
73            NewAgentMode::AddAgentToExistingProject => {
74                let inherited = inherited_prompts_from_existing(existing);
75                let entry = self.to_agent_entry(mode, &inherited);
76
77                let mut settings: Settings = existing.and_then(|s| serde_json::from_str(s).ok()).unwrap_or_default();
78                settings.agents.push(entry);
79                settings
80            }
81        }
82    }
83
84    pub fn to_mcp_json(&self) -> String {
85        use mcp_utils::client::config::{RawMcpConfig, RawMcpServerConfig};
86        use std::collections::BTreeMap;
87
88        let servers = self
89            .entry
90            .mcp_servers
91            .iter()
92            .map(|entry| {
93                let name = entry.path_str();
94                let args = if name == "skills" { vec!["--dir".into(), "$HOME/.aether".into()] } else { vec![] };
95                (name.to_string(), RawMcpServerConfig::InMemory { args, input: None })
96            })
97            .collect::<BTreeMap<_, _>>();
98
99        let config = RawMcpConfig { servers };
100        serde_json::to_string_pretty(&config).expect("mcp serialization cannot fail")
101    }
102}
103
104pub struct GeneratedPaths {
105    pub system_md: PathBuf,
106    pub mcp_json: PathBuf,
107}
108
109fn inherited_prompts_from_existing(existing: Option<&str>) -> Vec<String> {
110    existing
111        .and_then(|s| serde_json::from_str::<Settings>(s).ok())
112        .map(|s| s.prompts.into_iter().filter(|p| PromptFile::all().iter().any(|d| d.filename() == p)).collect())
113        .unwrap_or_default()
114}
115
116pub fn build_system_md(draft: &DraftAgentEntry) -> String {
117    format!(
118        "# {name}
119
120{description}
121
122## System Env
123
124Working directory: !`pwd`\\
125Platform: !`uname -s`\\
126Today's date: !`date +%Y-%m-%d`\\
127Git branch: !`git rev-parse --abbrev-ref HEAD`
128",
129        name = draft.entry.name,
130        description = draft.entry.description,
131    )
132}
133
134pub fn build_agents_md(draft: &DraftAgentEntry) -> String {
135    format!("# {}\n\n{}\n\nYou are an expert coding assistant.\n", draft.entry.name, draft.entry.description)
136}
137
138pub fn scaffold(project_root: &Path, draft: &DraftAgentEntry) -> Result<(), CliError> {
139    create_dir_all(project_root).map_err(CliError::IoError)?;
140
141    let paths = draft.generated_paths(&NewAgentMode::ScaffoldProject);
142    write_if_absent(&project_root.join(&paths.system_md), &draft.system_md_content)?;
143    write_if_absent(&project_root.join(".aether/mcp.json"), &draft.to_mcp_json())?;
144    if draft.entry.prompts.iter().any(|n| n == PromptFile::Agents.filename()) {
145        write_if_absent(&project_root.join("AGENTS.md"), &build_agents_md(draft))?;
146    }
147    let settings = draft.to_settings(&NewAgentMode::ScaffoldProject, None);
148    let json = serde_json::to_string_pretty(&settings).expect("settings serialization cannot fail");
149    write_if_absent(&project_root.join(".aether/settings.json"), &json)?;
150
151    Ok(())
152}
153
154pub fn add_agent(settings_path: &Path, draft: &DraftAgentEntry) -> Result<(), CliError> {
155    let content = read_to_string(settings_path).map_err(CliError::IoError)?;
156    let slug_dir = settings_path.parent().unwrap().join("agents").join(draft.slug());
157    create_dir_all(&slug_dir).map_err(CliError::IoError)?;
158
159    let filename = format!("{}.md", draft.slug().to_uppercase());
160    write(slug_dir.join(filename), &draft.system_md_content).map_err(CliError::IoError)?;
161
162    if !draft.entry.mcp_servers.is_empty() {
163        write(slug_dir.join("mcp.json"), draft.to_mcp_json()).map_err(CliError::IoError)?;
164    }
165
166    let settings = draft.to_settings(&NewAgentMode::AddAgentToExistingProject, Some(&content));
167    let json = serde_json::to_string_pretty(&settings).expect("settings serialization cannot fail");
168    write(settings_path, json).map_err(CliError::IoError)?;
169
170    Ok(())
171}
172
173fn write_if_absent(path: &Path, content: &str) -> Result<(), CliError> {
174    if path.exists() {
175        return Ok(());
176    }
177    if let Some(parent) = path.parent() {
178        std::fs::create_dir_all(parent).map_err(CliError::IoError)?;
179    }
180    std::fs::write(path, content).map_err(CliError::IoError)?;
181    Ok(())
182}
183
184#[cfg(test)]
185mod tests {
186    use super::*;
187    use llm::ReasoningEffort;
188    use mcp_utils::client::config::RawMcpConfig;
189
190    fn default_draft() -> DraftAgentEntry {
191        let mut draft = DraftAgentEntry {
192            entry: AgentEntry {
193                name: "Default".to_string(),
194                description: "Default coding agent".to_string(),
195                user_invocable: true,
196                agent_invocable: true,
197                model: "anthropic:claude-sonnet-4-5".to_string(),
198                prompts: vec!["AGENTS.md".to_string()],
199                mcp_servers: vec!["coding".into(), "skills".into(), "tasks".into()],
200                ..AgentEntry::default()
201            },
202            system_md_content: String::new(),
203            system_md_edited: false,
204            workspace_mcp_configs: vec![],
205        };
206        draft.system_md_content = build_system_md(&draft);
207        draft
208    }
209
210    #[test]
211    fn scaffold_writes_all_files() {
212        let dir = tempfile::tempdir().unwrap();
213        scaffold(dir.path(), &default_draft()).unwrap();
214
215        assert!(dir.path().join(".aether/settings.json").exists());
216        assert!(dir.path().join(".aether/mcp.json").exists());
217        assert!(dir.path().join(".aether/DEFAULT.md").exists());
218        assert!(dir.path().join("AGENTS.md").exists());
219    }
220
221    #[test]
222    fn scaffold_settings_json_is_valid() {
223        let dir = tempfile::tempdir().unwrap();
224        scaffold(dir.path(), &default_draft()).unwrap();
225
226        let catalog = aether_project::load_agent_catalog(dir.path()).unwrap();
227        assert_eq!(catalog.all().len(), 1);
228        assert_eq!(catalog.all()[0].name, "Default");
229    }
230
231    #[test]
232    fn scaffold_mcp_json_is_valid() {
233        let dir = tempfile::tempdir().unwrap();
234        scaffold(dir.path(), &default_draft()).unwrap();
235
236        let mcp_path = dir.path().join(".aether/mcp.json");
237        let raw = RawMcpConfig::from_json_file(&mcp_path).unwrap();
238        assert_eq!(raw.servers.len(), 3);
239        assert!(raw.servers.contains_key("coding"));
240        assert!(raw.servers.contains_key("skills"));
241        assert!(raw.servers.contains_key("tasks"));
242    }
243
244    #[test]
245    fn scaffold_skips_existing_files() {
246        let dir = tempfile::tempdir().unwrap();
247        let agents_path = dir.path().join("AGENTS.md");
248        std::fs::write(&agents_path, "My custom prompt").unwrap();
249
250        scaffold(dir.path(), &default_draft()).unwrap();
251
252        let content = std::fs::read_to_string(&agents_path).unwrap();
253        assert_eq!(content, "My custom prompt");
254    }
255
256    #[test]
257    fn scaffold_creates_parent_dirs() {
258        let dir = tempfile::tempdir().unwrap();
259        let nested = dir.path().join("deep/nested/project");
260        scaffold(&nested, &default_draft()).unwrap();
261
262        assert!(nested.join(".aether/settings.json").exists());
263        assert!(nested.join(".aether/mcp.json").exists());
264        assert!(nested.join(".aether/DEFAULT.md").exists());
265        assert!(nested.join("AGENTS.md").exists());
266    }
267
268    #[test]
269    fn scaffold_is_idempotent() {
270        let dir = tempfile::tempdir().unwrap();
271        let draft = default_draft();
272        scaffold(dir.path(), &draft).unwrap();
273        scaffold(dir.path(), &draft).unwrap();
274        assert!(dir.path().join(".aether/settings.json").exists());
275    }
276
277    #[test]
278    fn scaffold_system_md_matches_draft_content() {
279        let dir = tempfile::tempdir().unwrap();
280        let draft = default_draft();
281        scaffold(dir.path(), &draft).unwrap();
282
283        let content = std::fs::read_to_string(dir.path().join(".aether/DEFAULT.md")).unwrap();
284        assert_eq!(content, draft.system_md_content);
285    }
286
287    #[test]
288    fn generated_settings_reference_aether_paths() {
289        let dir = tempfile::tempdir().unwrap();
290        scaffold(dir.path(), &default_draft()).unwrap();
291
292        let content = std::fs::read_to_string(dir.path().join(".aether/settings.json")).unwrap();
293        let settings: Settings = serde_json::from_str(&content).unwrap();
294
295        assert!(settings.prompts.is_empty());
296        assert!(settings.mcp_servers.is_empty());
297
298        assert_eq!(settings.agents.len(), 1);
299        assert!(settings.agents[0].prompts.contains(&".aether/DEFAULT.md".to_string()));
300        assert!(settings.agents[0].prompts.contains(&"AGENTS.md".to_string()));
301        assert!(settings.agents[0].mcp_servers.contains(&".aether/mcp.json".into()));
302    }
303
304    #[test]
305    fn scaffold_without_agents_md() {
306        let dir = tempfile::tempdir().unwrap();
307        let mut draft = default_draft();
308        draft.entry.prompts = vec![];
309        scaffold(dir.path(), &draft).unwrap();
310
311        assert!(!dir.path().join("AGENTS.md").exists());
312
313        let content = std::fs::read_to_string(dir.path().join(".aether/settings.json")).unwrap();
314        let settings: Settings = serde_json::from_str(&content).unwrap();
315        assert!(!settings.prompts.contains(&"AGENTS.md".to_string()));
316    }
317
318    #[test]
319    fn scaffold_includes_reasoning_effort() {
320        let dir = tempfile::tempdir().unwrap();
321        let mut draft = default_draft();
322        draft.entry.reasoning_effort = Some(ReasoningEffort::High);
323        scaffold(dir.path(), &draft).unwrap();
324
325        let catalog = aether_project::load_agent_catalog(dir.path()).unwrap();
326        assert_eq!(catalog.all()[0].reasoning_effort, Some(ReasoningEffort::High));
327    }
328
329    #[test]
330    fn scaffold_omits_reasoning_effort_when_none() {
331        let dir = tempfile::tempdir().unwrap();
332        scaffold(dir.path(), &default_draft()).unwrap();
333
334        let content = std::fs::read_to_string(dir.path().join(".aether/settings.json")).unwrap();
335        assert!(!content.contains("reasoningEffort"));
336    }
337
338    #[test]
339    fn scaffold_custom_servers() {
340        let dir = tempfile::tempdir().unwrap();
341        let mut draft = default_draft();
342        draft.entry.mcp_servers = vec!["coding".into(), "lsp".into()];
343        scaffold(dir.path(), &draft).unwrap();
344
345        let raw = RawMcpConfig::from_json_file(dir.path().join(".aether/mcp.json")).unwrap();
346        assert_eq!(raw.servers.len(), 2);
347        assert!(raw.servers.contains_key("coding"));
348        assert!(raw.servers.contains_key("lsp"));
349        assert!(!raw.servers.contains_key("tasks"));
350    }
351
352    #[test]
353    fn scaffold_no_servers_no_mcp_json_ref() {
354        let dir = tempfile::tempdir().unwrap();
355        let mut draft = default_draft();
356        draft.entry.mcp_servers = vec![];
357        scaffold(dir.path(), &draft).unwrap();
358
359        let content = std::fs::read_to_string(dir.path().join(".aether/settings.json")).unwrap();
360        let settings: Settings = serde_json::from_str(&content).unwrap();
361        assert!(settings.mcp_servers.is_empty());
362    }
363
364    fn researcher_draft() -> DraftAgentEntry {
365        let mut draft = default_draft();
366        draft.entry.name = "Researcher".to_string();
367        draft.entry.description = "Research agent".to_string();
368        draft.entry.mcp_servers = vec![];
369        draft.workspace_mcp_configs = vec![];
370        draft.system_md_content = build_system_md(&draft);
371        draft
372    }
373
374    #[test]
375    fn add_agent_appends_to_existing_settings() {
376        let dir = tempfile::tempdir().unwrap();
377        scaffold(dir.path(), &default_draft()).unwrap();
378
379        let settings_path = dir.path().join(".aether/settings.json");
380        add_agent(&settings_path, &researcher_draft()).unwrap();
381
382        let catalog = aether_project::load_agent_catalog(dir.path()).unwrap();
383        assert_eq!(catalog.all().len(), 2);
384        assert_eq!(catalog.all()[0].name, "Default");
385        assert_eq!(catalog.all()[1].name, "Researcher");
386    }
387
388    #[test]
389    fn add_agent_writes_per_agent_system_md() {
390        let dir = tempfile::tempdir().unwrap();
391        scaffold(dir.path(), &default_draft()).unwrap();
392
393        let settings_path = dir.path().join(".aether/settings.json");
394        let mut new_draft = researcher_draft();
395        new_draft.entry.prompts = vec![];
396        let expected_per_agent = new_draft.system_md_content.clone();
397        add_agent(&settings_path, &new_draft).unwrap();
398
399        let agent_md = dir.path().join(".aether/agents/researcher/RESEARCHER.md");
400        assert!(agent_md.exists());
401        assert_eq!(std::fs::read_to_string(agent_md).unwrap(), expected_per_agent);
402
403        let shared_md = dir.path().join(".aether/DEFAULT.md");
404        assert_eq!(std::fs::read_to_string(&shared_md).unwrap(), default_draft().system_md_content);
405    }
406
407    #[test]
408    fn add_agent_does_not_rewrite_shared_files() {
409        let dir = tempfile::tempdir().unwrap();
410        scaffold(dir.path(), &default_draft()).unwrap();
411
412        let shared_system = dir.path().join(".aether/DEFAULT.md");
413        std::fs::write(&shared_system, "custom shared prompt").unwrap();
414        let shared_mcp = dir.path().join(".aether/mcp.json");
415        let original_mcp = std::fs::read_to_string(&shared_mcp).unwrap();
416        let agents_md = dir.path().join("AGENTS.md");
417        std::fs::write(&agents_md, "custom agents md").unwrap();
418
419        let settings_path = dir.path().join(".aether/settings.json");
420        let mut new_draft = researcher_draft();
421        new_draft.entry.mcp_servers = vec!["coding".into()];
422        add_agent(&settings_path, &new_draft).unwrap();
423
424        assert_eq!(std::fs::read_to_string(&shared_system).unwrap(), "custom shared prompt");
425        assert_eq!(std::fs::read_to_string(&shared_mcp).unwrap(), original_mcp);
426        assert_eq!(std::fs::read_to_string(&agents_md).unwrap(), "custom agents md");
427    }
428
429    #[test]
430    fn add_agent_writes_per_agent_mcp_json() {
431        let dir = tempfile::tempdir().unwrap();
432        scaffold(dir.path(), &default_draft()).unwrap();
433
434        let settings_path = dir.path().join(".aether/settings.json");
435        let mut new_draft = researcher_draft();
436        new_draft.entry.prompts = vec![];
437        new_draft.entry.mcp_servers = vec!["coding".into(), "lsp".into()];
438        add_agent(&settings_path, &new_draft).unwrap();
439
440        let agent_mcp = dir.path().join(".aether/agents/researcher/mcp.json");
441        assert!(agent_mcp.exists());
442
443        let raw = RawMcpConfig::from_json_file(&agent_mcp).unwrap();
444        assert_eq!(raw.servers.len(), 2);
445        assert!(raw.servers.contains_key("coding"));
446        assert!(raw.servers.contains_key("lsp"));
447    }
448
449    #[test]
450    fn add_agent_agent_entry_references_local_assets() {
451        let dir = tempfile::tempdir().unwrap();
452        scaffold(dir.path(), &default_draft()).unwrap();
453
454        let settings_path = dir.path().join(".aether/settings.json");
455        let mut new_draft = researcher_draft();
456        new_draft.entry.user_invocable = false;
457        new_draft.entry.prompts = vec![];
458        new_draft.entry.mcp_servers = vec!["coding".into()];
459        add_agent(&settings_path, &new_draft).unwrap();
460
461        let content = std::fs::read_to_string(&settings_path).unwrap();
462        let settings: Settings = serde_json::from_str(&content).unwrap();
463        let researcher = &settings.agents[1];
464
465        assert_eq!(researcher.name, "Researcher");
466        assert!(!researcher.user_invocable);
467        assert!(researcher.agent_invocable);
468        assert!(researcher.prompts.contains(&".aether/agents/researcher/RESEARCHER.md".to_string()));
469        assert!(researcher.mcp_servers.contains(&".aether/agents/researcher/mcp.json".into()));
470    }
471
472    #[test]
473    fn generated_paths_scaffold() {
474        let draft = default_draft();
475        let paths = draft.generated_paths(&NewAgentMode::ScaffoldProject);
476        assert_eq!(paths.system_md, PathBuf::from(".aether/DEFAULT.md"));
477        assert_eq!(paths.mcp_json, PathBuf::from(".aether/mcp.json"));
478    }
479
480    #[test]
481    fn generated_paths_add_agent() {
482        let draft = default_draft();
483        let paths = draft.generated_paths(&NewAgentMode::AddAgentToExistingProject);
484        assert_eq!(paths.system_md, PathBuf::from(".aether/agents/default/DEFAULT.md"));
485        assert_eq!(paths.mcp_json, PathBuf::from(".aether/agents/default/mcp.json"));
486    }
487
488    #[test]
489    fn slug_from_name() {
490        let mut draft = default_draft();
491        draft.entry.name = "Codebase Explorer".to_string();
492        assert_eq!(draft.slug(), "codebase-explorer");
493    }
494
495    #[test]
496    fn build_system_md_uses_name_description_and_bash_block() {
497        let mut draft = default_draft();
498        draft.entry.name = "Researcher".to_string();
499        draft.entry.description = "Digs through the codebase".to_string();
500        let body = build_system_md(&draft);
501        assert!(body.starts_with("# Researcher\n"));
502        assert!(body.contains("Digs through the codebase"));
503        assert!(body.contains("## System Env"));
504        assert!(body.contains("Working directory: !`pwd`\\"));
505        assert!(body.contains("Platform: !`uname -s`\\"));
506        assert!(body.contains("Today's date: !`date +%Y-%m-%d`\\"));
507        assert!(body.contains("Git branch: !`git rev-parse --abbrev-ref HEAD`"));
508    }
509
510    #[test]
511    fn build_settings_json_scaffold_emits_all_selected_prompts() {
512        let mut draft = default_draft();
513        draft.entry.prompts = vec!["AGENTS.md".into(), "CLAUDE.md".into()];
514        let settings = draft.to_settings(&NewAgentMode::ScaffoldProject, None);
515
516        assert!(settings.prompts.is_empty());
517        assert!(settings.agents[0].prompts.contains(&".aether/DEFAULT.md".to_string()));
518        assert!(settings.agents[0].prompts.contains(&"AGENTS.md".to_string()));
519        assert!(settings.agents[0].prompts.contains(&"CLAUDE.md".to_string()));
520    }
521
522    #[test]
523    fn build_settings_json_add_agent_skips_inherited_prompts() {
524        let existing = serde_json::to_string_pretty(&Settings {
525            prompts: vec!["AGENTS.md".into()],
526            mcp_servers: vec![],
527            agents: vec![default_draft().to_agent_entry(&NewAgentMode::ScaffoldProject, &[])],
528        })
529        .unwrap();
530
531        let mut new_draft = researcher_draft();
532        new_draft.entry.prompts = vec!["AGENTS.md".into(), "CLAUDE.md".into()];
533        let settings = new_draft.to_settings(&NewAgentMode::AddAgentToExistingProject, Some(&existing));
534
535        let researcher = &settings.agents[1];
536        assert_eq!(researcher.name, "Researcher");
537        assert!(
538            !researcher.prompts.contains(&"AGENTS.md".to_string()),
539            "AGENTS.md is inherited from top-level prompts"
540        );
541        assert!(researcher.prompts.contains(&"CLAUDE.md".to_string()));
542    }
543
544    #[test]
545    fn scaffold_writes_agents_md_when_selected() {
546        let dir = tempfile::tempdir().unwrap();
547        let mut draft = default_draft();
548        draft.entry.prompts = vec!["AGENTS.md".into()];
549        scaffold(dir.path(), &draft).unwrap();
550        assert!(dir.path().join("AGENTS.md").exists());
551    }
552
553    #[test]
554    fn scaffold_includes_workspace_mcp_configs() {
555        let dir = tempfile::tempdir().unwrap();
556        std::fs::write(dir.path().join("mcp.json"), r#"{"servers":{}}"#).unwrap();
557
558        let mut draft = default_draft();
559        draft.workspace_mcp_configs = vec!["mcp.json".to_string()];
560        scaffold(dir.path(), &draft).unwrap();
561
562        let content = std::fs::read_to_string(dir.path().join(".aether/settings.json")).unwrap();
563        let settings: Settings = serde_json::from_str(&content).unwrap();
564
565        assert!(settings.mcp_servers.is_empty());
566        assert!(settings.agents[0].mcp_servers.contains(&"mcp.json".into()));
567    }
568
569    #[test]
570    fn add_agent_includes_workspace_mcp_configs() {
571        let dir = tempfile::tempdir().unwrap();
572        scaffold(dir.path(), &default_draft()).unwrap();
573
574        let settings_path = dir.path().join(".aether/settings.json");
575        let mut new_draft = researcher_draft();
576        new_draft.entry.mcp_servers = vec!["coding".into()];
577        new_draft.workspace_mcp_configs = vec![".mcp.json".to_string()];
578        add_agent(&settings_path, &new_draft).unwrap();
579
580        let content = std::fs::read_to_string(&settings_path).unwrap();
581        let settings: Settings = serde_json::from_str(&content).unwrap();
582        let researcher = &settings.agents[1];
583
584        assert!(researcher.mcp_servers.contains(&".mcp.json".into()));
585    }
586
587    #[test]
588    fn scaffold_never_writes_claude_or_gemini_md() {
589        let dir = tempfile::tempdir().unwrap();
590        let mut draft = default_draft();
591        draft.entry.prompts = vec!["AGENTS.md".into(), "CLAUDE.md".into(), "GEMINI.md".into()];
592        scaffold(dir.path(), &draft).unwrap();
593
594        assert!(dir.path().join("AGENTS.md").exists());
595        assert!(!dir.path().join("CLAUDE.md").exists());
596        assert!(!dir.path().join("GEMINI.md").exists());
597    }
598}