refactor: split agents/pool/start.rs into mod.rs + validation.rs + spawn.rs
The 1630-line start.rs is split into a sub-module directory: - validation.rs: validate_agent_stage + read_front_matter_agent helpers (69 lines) - spawn.rs: run_agent_spawn — the background async work that was inlined as a tokio::spawn closure body inside start_agent (359 lines) - mod.rs: AgentPool::start_agent orchestrator + tests (1062 lines) Stage validation and front-matter agent reading are pre-lock pure helpers that naturally extract. The spawn closure body becomes a free async fn that takes the previously-cloned values as parameters; rebound to the original _clone / _owned names at the top of the body so the actual work code is a verbatim copy. No behaviour change. All 23 start tests pass; full suite green.
This commit is contained in:
@@ -0,0 +1,69 @@
|
||||
//! Pre-lock validation helpers for `AgentPool::start_agent`.
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
use crate::config::ProjectConfig;
|
||||
|
||||
use super::super::super::{PipelineStage, agent_config_stage, pipeline_stage};
|
||||
use super::super::worktree::find_active_story_stage;
|
||||
|
||||
/// Validate that an explicit `agent_name` is allowed to attach to `story_id`'s
|
||||
/// current pipeline stage.
|
||||
///
|
||||
/// Prevents wrong-stage assignments like a mergemaster on a coding-stage story
|
||||
/// (bug 312). Returns `Ok(())` if the agent has no specific stage (e.g.
|
||||
/// supervisor) or the story is not in an active stage; `Err` with a descriptive
|
||||
/// message on a stage mismatch.
|
||||
pub(super) fn validate_agent_stage(
|
||||
config: &ProjectConfig,
|
||||
project_root: &Path,
|
||||
story_id: &str,
|
||||
agent_name: Option<&str>,
|
||||
) -> Result<(), String> {
|
||||
let Some(name) = agent_name else {
|
||||
return Ok(());
|
||||
};
|
||||
let agent_stage = config
|
||||
.find_agent(name)
|
||||
.map(agent_config_stage)
|
||||
.unwrap_or_else(|| pipeline_stage(name));
|
||||
if agent_stage == PipelineStage::Other {
|
||||
return Ok(());
|
||||
}
|
||||
let Some(story_stage_dir) = find_active_story_stage(project_root, story_id) else {
|
||||
return Ok(());
|
||||
};
|
||||
let expected_stage = match story_stage_dir {
|
||||
"2_current" => PipelineStage::Coder,
|
||||
"3_qa" => PipelineStage::Qa,
|
||||
"4_merge" => PipelineStage::Mergemaster,
|
||||
_ => PipelineStage::Other,
|
||||
};
|
||||
if expected_stage != PipelineStage::Other && expected_stage != agent_stage {
|
||||
return Err(format!(
|
||||
"Agent '{name}' (stage: {agent_stage:?}) cannot be assigned to \
|
||||
story '{story_id}' in {story_stage_dir}/ (requires stage: {expected_stage:?})"
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Read the preferred `agent:` field from the story's front matter.
|
||||
///
|
||||
/// When `agent_name` is `None` (caller is auto-selecting), this lets
|
||||
/// `start_agent` honour an explicit `agent: coder-opus` written by the
|
||||
/// `assign` command (bug 379). Returns `None` when an explicit agent_name
|
||||
/// was already supplied or when the story has no front-matter preference.
|
||||
pub(super) fn read_front_matter_agent(
|
||||
story_id: &str,
|
||||
agent_name: Option<&str>,
|
||||
) -> Option<String> {
|
||||
if agent_name.is_some() {
|
||||
return None;
|
||||
}
|
||||
crate::db::read_content(story_id).and_then(|contents| {
|
||||
crate::io::story_metadata::parse_front_matter(&contents)
|
||||
.ok()?
|
||||
.agent
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user