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