story-kit: merge 307_story_configurable_coder_pool_size_and_default_model_in_project_toml

This commit is contained in:
Dave
2026-03-19 15:58:32 +00:00
parent 101b365354
commit 429597cbce
4 changed files with 386 additions and 2 deletions
+87
View File
@@ -15,6 +15,17 @@ pub struct ProjectConfig {
/// Per-story `qa` front matter overrides this. Default: "server".
#[serde(default = "default_qa")]
pub default_qa: String,
/// Default model for coder-stage agents (e.g. "sonnet").
/// When set, `find_free_agent_for_stage` only considers coder agents whose
/// model matches this value, so opus agents are only used when explicitly
/// requested via story front matter `agent:` field.
#[serde(default)]
pub default_coder_model: Option<String>,
/// Maximum number of concurrent coder-stage agents.
/// When set, `auto_assign_available_work` will not start more than this many
/// coder agents at once. Stories wait in `2_current/` until a slot frees up.
#[serde(default)]
pub max_coders: Option<usize>,
}
/// Configuration for the filesystem watcher's sweep behaviour.
@@ -134,6 +145,10 @@ struct LegacyProjectConfig {
watcher: WatcherConfig,
#[serde(default = "default_qa")]
default_qa: String,
#[serde(default)]
default_coder_model: Option<String>,
#[serde(default)]
max_coders: Option<usize>,
}
impl Default for ProjectConfig {
@@ -156,6 +171,8 @@ impl Default for ProjectConfig {
}],
watcher: WatcherConfig::default(),
default_qa: default_qa(),
default_coder_model: None,
max_coders: None,
}
}
}
@@ -198,6 +215,8 @@ impl ProjectConfig {
agent: vec![agent],
watcher: legacy.watcher,
default_qa: legacy.default_qa,
default_coder_model: legacy.default_coder_model,
max_coders: legacy.max_coders,
};
validate_agents(&config.agent)?;
return Ok(config);
@@ -219,6 +238,8 @@ impl ProjectConfig {
agent: vec![agent],
watcher: legacy.watcher,
default_qa: legacy.default_qa,
default_coder_model: legacy.default_coder_model,
max_coders: legacy.max_coders,
};
validate_agents(&config.agent)?;
Ok(config)
@@ -228,6 +249,8 @@ impl ProjectConfig {
agent: Vec::new(),
watcher: legacy.watcher,
default_qa: legacy.default_qa,
default_coder_model: legacy.default_coder_model,
max_coders: legacy.max_coders,
})
}
}
@@ -710,4 +733,68 @@ command = "claude"
assert_eq!(config.watcher.done_retention_secs, 900);
assert_eq!(config.agent.len(), 1);
}
// ── default_coder_model & max_coders ─────────────────────────────────
#[test]
fn parse_default_coder_model_and_max_coders() {
let toml_str = r#"
default_coder_model = "sonnet"
max_coders = 3
[[agent]]
name = "coder-1"
stage = "coder"
model = "sonnet"
[[agent]]
name = "coder-opus"
stage = "coder"
model = "opus"
"#;
let config = ProjectConfig::parse(toml_str).unwrap();
assert_eq!(config.default_coder_model, Some("sonnet".to_string()));
assert_eq!(config.max_coders, Some(3));
}
#[test]
fn default_coder_model_and_max_coders_default_to_none() {
let toml_str = r#"
[[agent]]
name = "coder-1"
"#;
let config = ProjectConfig::parse(toml_str).unwrap();
assert_eq!(config.default_coder_model, None);
assert_eq!(config.max_coders, None);
}
#[test]
fn project_toml_has_default_coder_model_and_max_coders() {
// Verify the actual project.toml has the new settings.
let manifest_dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR"));
let project_root = manifest_dir.parent().unwrap();
let config = ProjectConfig::load(project_root).unwrap();
assert_eq!(config.default_coder_model, Some("sonnet".to_string()));
assert_eq!(config.max_coders, Some(3));
}
#[test]
fn project_toml_has_three_sonnet_coders() {
let manifest_dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR"));
let project_root = manifest_dir.parent().unwrap();
let config = ProjectConfig::load(project_root).unwrap();
let sonnet_coders: Vec<_> = config
.agent
.iter()
.filter(|a| a.stage.as_deref() == Some("coder") && a.model.as_deref() == Some("sonnet"))
.collect();
assert_eq!(
sonnet_coders.len(),
3,
"Expected 3 sonnet coders (coder-1, coder-2, coder-3), found {}",
sonnet_coders.len()
);
}
}