From a8eac3c27875bd216bd6eb884df4e31aff203bf1 Mon Sep 17 00:00:00 2001 From: dave Date: Thu, 30 Apr 2026 16:36:18 +0000 Subject: [PATCH] fix: read agent pin from CRDT register, not just YAML front matter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../agents/pool/auto_assign/story_checks.rs | 39 +++++++++++++++++-- server/src/agents/pool/start/validation.rs | 9 +++++ 2 files changed, 45 insertions(+), 3 deletions(-) diff --git a/server/src/agents/pool/auto_assign/story_checks.rs b/server/src/agents/pool/auto_assign/story_checks.rs index 151cc9ed..44b6f1f9 100644 --- a/server/src/agents/pool/auto_assign/story_checks.rs +++ b/server/src/agents/pool/auto_assign/story_checks.rs @@ -7,15 +7,23 @@ fn read_story_contents(_project_root: &Path, story_id: &str) -> Option { crate::db::read_content(story_id) } -/// Read the optional `agent:` field from the front matter of a story file. +/// Read the optional `agent:` pin for a story. /// -/// Returns `Some(agent_name)` if the front matter specifies an agent, or `None` -/// if the field is absent or the file cannot be read / parsed. +/// After story 871 the agent assignment lives in the CRDT typed register +/// (`PipelineItemView.agent`), not the YAML front matter. We check the CRDT +/// first; falling back to legacy YAML parsing keeps behaviour intact for any +/// stories whose CRDT entry doesn't yet have the field set. pub(super) fn read_story_front_matter_agent( project_root: &Path, _stage_dir: &str, story_id: &str, ) -> Option { + 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()); + } use crate::io::story_metadata::parse_front_matter; let contents = read_story_contents(project_root, story_id)?; parse_front_matter(&contents).ok()?.agent @@ -293,4 +301,29 @@ mod tests { check_archived_dependencies(tmp.path(), "1_backlog", "503_story_waiting"); assert!(archived_deps.is_empty()); } + + #[test] + fn read_story_front_matter_agent_prefers_crdt_typed_register_over_yaml() { + // Regression: after story 871 the agent pin lives in the CRDT typed + // register, not the YAML front matter. read_story_front_matter_agent + // must consult the register first so auto-assign honours the pin. + let tmp = tempfile::tempdir().unwrap(); + crate::db::ensure_content_store(); + + // Seed a story whose YAML has NO agent field. + crate::db::write_item_with_content( + "9971_story_pin_in_crdt", + "2_current", + "---\nname: Pin In CRDT\n---\n", + ); + + // Set the typed CRDT register (this is the path 871's migration uses). + let written = + crate::crdt_state::set_agent("9971_story_pin_in_crdt", Some("coder-opus")); + assert!(written, "set_agent should succeed for an existing item"); + + // The reader must return the CRDT register value, not None. + let agent = read_story_front_matter_agent(tmp.path(), "2_current", "9971_story_pin_in_crdt"); + assert_eq!(agent.as_deref(), Some("coder-opus")); + } } diff --git a/server/src/agents/pool/start/validation.rs b/server/src/agents/pool/start/validation.rs index eb800038..25b866ce 100644 --- a/server/src/agents/pool/start/validation.rs +++ b/server/src/agents/pool/start/validation.rs @@ -60,6 +60,15 @@ pub(super) fn read_front_matter_agent(story_id: &str, agent_name: Option<&str>) 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()?