huskies: merge 482_refactor_split_agent_definitions_from_project_toml_into_agents_toml
This commit is contained in:
+89
-9
@@ -224,20 +224,47 @@ impl Default for ProjectConfig {
|
||||
}
|
||||
}
|
||||
|
||||
/// Config parsed from `.huskies/agents.toml` — agent definitions only.
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct AgentsConfig {
|
||||
#[serde(default)]
|
||||
agent: Vec<AgentConfig>,
|
||||
}
|
||||
|
||||
impl ProjectConfig {
|
||||
/// Load from `.huskies/project.toml` relative to the given root.
|
||||
/// Falls back to sensible defaults if the file doesn't exist.
|
||||
/// Load from `.huskies/project.toml` relative to the given root,
|
||||
/// then overlay agents from `.huskies/agents.toml` if present.
|
||||
///
|
||||
/// Supports both the new `[[agent]]` array format and the legacy
|
||||
/// `[agent]` single-table format (with a deprecation warning).
|
||||
/// Loading order:
|
||||
/// 1. Project settings (watcher, default_qa, etc.) always come from `project.toml`.
|
||||
/// 2. Agent definitions come from `agents.toml` when that file exists.
|
||||
/// 3. Falls back to inline `[[agent]]` blocks in `project.toml` for backwards
|
||||
/// compatibility with projects that haven't migrated yet.
|
||||
/// 4. Falls back to a single default agent when neither file defines agents.
|
||||
pub fn load(project_root: &Path) -> Result<Self, String> {
|
||||
let config_path = project_root.join(".huskies/project.toml");
|
||||
if !config_path.exists() {
|
||||
return Ok(Self::default());
|
||||
let mut config = if config_path.exists() {
|
||||
let content =
|
||||
std::fs::read_to_string(&config_path).map_err(|e| format!("Read config: {e}"))?;
|
||||
Self::parse(&content)?
|
||||
} else {
|
||||
Self::default()
|
||||
};
|
||||
|
||||
// agents.toml takes priority over inline [[agent]] in project.toml.
|
||||
let agents_path = project_root.join(".huskies/agents.toml");
|
||||
if agents_path.exists() {
|
||||
let content = std::fs::read_to_string(&agents_path)
|
||||
.map_err(|e| format!("Read agents.toml: {e}"))?;
|
||||
let agents_cfg: AgentsConfig =
|
||||
toml::from_str(&content).map_err(|e| format!("Parse agents.toml: {e}"))?;
|
||||
if !agents_cfg.agent.is_empty() {
|
||||
validate_agents(&agents_cfg.agent)?;
|
||||
config.agent = agents_cfg.agent;
|
||||
}
|
||||
}
|
||||
let content =
|
||||
std::fs::read_to_string(&config_path).map_err(|e| format!("Read config: {e}"))?;
|
||||
Self::parse(&content)
|
||||
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
/// Parse config from a TOML string, supporting both new and legacy formats.
|
||||
@@ -675,6 +702,59 @@ model = "sonnet"
|
||||
assert_eq!(config.agent[0].model, Some("sonnet".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn agents_toml_overrides_project_toml_agents() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let sk = tmp.path().join(".huskies");
|
||||
fs::create_dir_all(&sk).unwrap();
|
||||
// project.toml has inline agents
|
||||
fs::write(
|
||||
sk.join("project.toml"),
|
||||
r#"
|
||||
[[agent]]
|
||||
name = "from-project-toml"
|
||||
model = "sonnet"
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
// agents.toml overrides with different agents
|
||||
fs::write(
|
||||
sk.join("agents.toml"),
|
||||
r#"
|
||||
[[agent]]
|
||||
name = "from-agents-toml"
|
||||
model = "opus"
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let config = ProjectConfig::load(tmp.path()).unwrap();
|
||||
assert_eq!(config.agent.len(), 1);
|
||||
assert_eq!(config.agent[0].name, "from-agents-toml");
|
||||
assert_eq!(config.agent[0].model, Some("opus".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn agents_toml_absent_falls_back_to_project_toml() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let sk = tmp.path().join(".huskies");
|
||||
fs::create_dir_all(&sk).unwrap();
|
||||
fs::write(
|
||||
sk.join("project.toml"),
|
||||
r#"
|
||||
[[agent]]
|
||||
name = "inline-agent"
|
||||
model = "sonnet"
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
// No agents.toml — should use inline agents from project.toml
|
||||
|
||||
let config = ProjectConfig::load(tmp.path()).unwrap();
|
||||
assert_eq!(config.agent.len(), 1);
|
||||
assert_eq!(config.agent[0].name, "inline-agent");
|
||||
}
|
||||
|
||||
// ── WatcherConfig ──────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user