a8eac3c278
After story 871 the `agent` pin lives in the typed CRDT register (`PipelineItemView.agent`), not the YAML front matter — the YAML mutation was removed at the same time. Both spawn-resolution paths (`auto_assign::story_checks::read_story_front_matter_agent` and `start::validation::read_front_matter_agent`) still read only YAML via parse_front_matter, which returns None for any story whose pin was set via the post-871 typed setter. The spawn then falls back to "first available coder," silently downgrading opus-pinned stories to the first available sonnet — which is why 855/864/866 kept hitting the 80-turn watchdog limit despite the user's explicit opus pin. Now: both paths consult `crdt_state::read_item()` first and use `view.agent` if non-empty. YAML parsing remains as a fallback so older stories whose CRDT entry doesn't yet have the field still resolve. Adds a regression test that seeds an item with empty YAML, sets the typed CRDT register via `set_agent`, and asserts `read_story_front_matter_agent` returns the CRDT value.
78 lines
2.8 KiB
Rust
78 lines
2.8 KiB
Rust
//! Pre-lock validation helpers for `AgentPool::start_agent`.
|
|
|
|
use std::path::Path;
|
|
|
|
use crate::config::ProjectConfig;
|
|
use crate::pipeline_state::Stage;
|
|
|
|
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) = find_active_story_stage(project_root, story_id) else {
|
|
return Ok(());
|
|
};
|
|
let expected_stage = match story_stage {
|
|
Stage::Coding => PipelineStage::Coder,
|
|
Stage::Qa => PipelineStage::Qa,
|
|
Stage::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 {}/ (requires stage: {expected_stage:?})",
|
|
story_stage.dir_name()
|
|
));
|
|
}
|
|
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;
|
|
}
|
|
// After story 871 the pin lives in the CRDT typed register; fall back
|
|
// to legacy YAML parsing for stories whose CRDT entry doesn't yet have
|
|
// the field populated.
|
|
if let Some(view) = crate::crdt_state::read_item(story_id)
|
|
&& let Some(agent) = view.agent.as_ref()
|
|
&& !agent.is_empty()
|
|
{
|
|
return Some(agent.clone());
|
|
}
|
|
crate::db::read_content(story_id).and_then(|contents| {
|
|
crate::io::story_metadata::parse_front_matter(&contents)
|
|
.ok()?
|
|
.agent
|
|
})
|
|
}
|