Skip to main content

aether_cli/agent/new_agent_wizard/
draft_agent_entry.rs

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