From f775f4cfb90de263154b9ddb5a71faff198bc9db Mon Sep 17 00:00:00 2001 From: Timmy Date: Tue, 12 May 2026 19:03:51 +0100 Subject: [PATCH] =?UTF-8?q?wip(929):=20stage=204=20=E2=80=94=20migrate=20a?= =?UTF-8?q?gents/pool/*=20+=20lifecycle.rs=20read=20sides=20off=20yaml=5Fl?= =?UTF-8?q?egacy?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Read-side migrations: - agents/pool/auto_assign/backlog.rs: depends_on check now reads from WorkItem.depends_on() instead of parse_front_matter. - agents/pool/auto_assign/story_checks.rs: read_story_front_matter_agent drops its YAML fallback — post-891 the CRDT entry is reliable, and removing the fallback makes the contract honest. The now-unused read_story_contents helper goes too. - agents/pool/start/validation.rs: same shape — YAML fallback removed, CRDT register is the only source for agent pinning. - agents/pool/start/spawn.rs: epic-context injection wraps the parse_front_matter call in `yaml_residue(...)` since `meta.epic` has no CRDT analog (sub-story 933). - agents/lifecycle.rs: item_type_from_id (numeric-only ID path) wraps its parse_front_matter in `yaml_residue(...)` for the same reason (933). The write-side `fields_to_clear_transform` calls in lifecycle.rs are left for stage 8, when FS-shadow writes are deleted wholesale. Test fix: - start_agent_returns_error_when_front_matter_agent_busy now seeds the CRDT entry (write_item with agent="coder-opus") instead of relying on parse_front_matter reading the YAML on disk. Filed earlier: - 932 (review_hold register) — note: this turns out to be a real class-1 bug: write_review_hold_to_store still writes YAML but has_review_hold reads Stage::Frozen, so the write goes into a void. 932 is the correct fix. All 2861 tests pass; fmt + clippy clean. Co-Authored-By: Claude Opus 4.7 (1M context) --- server/src/agents/lifecycle.rs | 6 ++++- server/src/agents/pool/auto_assign/backlog.rs | 12 ++++------ .../agents/pool/auto_assign/story_checks.rs | 22 ++++++------------- server/src/agents/pool/start/spawn.rs | 7 +++++- .../src/agents/pool/start/tests_selection.rs | 14 ++++++++++++ server/src/agents/pool/start/validation.rs | 20 +++++------------ 6 files changed, 42 insertions(+), 39 deletions(-) diff --git a/server/src/agents/lifecycle.rs b/server/src/agents/lifecycle.rs index 3fe7b4a0..b9f6a45e 100644 --- a/server/src/agents/lifecycle.rs +++ b/server/src/agents/lifecycle.rs @@ -32,9 +32,13 @@ pub(crate) fn item_type_from_id(item_id: &str) -> &'static str { return "refactor"; } // Numeric-only ID: check content store front matter for explicit type. + // `item_type` has no CRDT register yet (story 933 — epic mechanism). + // Marked with yaml_residue so the gap is grep-findable. if after_num.is_empty() && let Some(content) = crate::db::read_content(item_id) - && let Ok(meta) = crate::db::yaml_legacy::parse_front_matter(&content) + && let Ok(meta) = crate::db::yaml_legacy::yaml_residue( + crate::db::yaml_legacy::parse_front_matter(&content), + ) && let Some(t) = meta.item_type.as_deref() { return match t { diff --git a/server/src/agents/pool/auto_assign/backlog.rs b/server/src/agents/pool/auto_assign/backlog.rs index 4c0e13ce..995a1616 100644 --- a/server/src/agents/pool/auto_assign/backlog.rs +++ b/server/src/agents/pool/auto_assign/backlog.rs @@ -24,16 +24,12 @@ impl AgentPool { /// logged so the user can see the promotion was triggered by an archived dep, not /// a clean completion. pub(super) fn promote_ready_backlog_stories(&self, project_root: &Path) { - use crate::db::yaml_legacy::parse_front_matter; - let items = scan_stage_items(project_root, "1_backlog"); for story_id in &items { - // Only promote stories that explicitly declare dependencies. - let contents = crate::db::read_content(story_id); - let has_deps = contents - .and_then(|c| parse_front_matter(&c).ok()) - .and_then(|m| m.depends_on) - .map(|d| !d.is_empty()) + // Only promote stories that explicitly declare dependencies + // (story 929: read from the CRDT register, not YAML). + let has_deps = crate::crdt_state::read_item(story_id) + .map(|w| !w.depends_on().is_empty()) .unwrap_or(false); if !has_deps { continue; diff --git a/server/src/agents/pool/auto_assign/story_checks.rs b/server/src/agents/pool/auto_assign/story_checks.rs index 7c74946b..3f3ca24a 100644 --- a/server/src/agents/pool/auto_assign/story_checks.rs +++ b/server/src/agents/pool/auto_assign/story_checks.rs @@ -2,11 +2,6 @@ use std::path::Path; -/// Read story contents from the DB content store (CRDT-backed). -fn read_story_contents(_project_root: &Path, story_id: &str) -> Option { - crate::db::read_content(story_id) -} - /// Read the optional `agent:` pin for a story. /// /// After story 871 the agent assignment lives in the CRDT typed register @@ -14,19 +9,16 @@ fn read_story_contents(_project_root: &Path, story_id: &str) -> Option { /// 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, + _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() - && !agent.is_empty() - { - return Some(agent.to_string()); - } - use crate::db::yaml_legacy::parse_front_matter; - let contents = read_story_contents(project_root, story_id)?; - parse_front_matter(&contents).ok()?.agent + // Story 929: agent name comes from the CRDT register. The previous + // YAML fallback is gone — post-891 every story has its CRDT entry, + // and any story without one is treated as having no pinned agent. + crate::crdt_state::read_item(story_id) + .and_then(|w| w.agent().map(str::to_string)) + .filter(|s| !s.is_empty()) } /// Return `true` if the story is in the `Frozen` pipeline stage. diff --git a/server/src/agents/pool/start/spawn.rs b/server/src/agents/pool/start/spawn.rs index e53c683d..196a6879 100644 --- a/server/src/agents/pool/start/spawn.rs +++ b/server/src/agents/pool/start/spawn.rs @@ -230,8 +230,13 @@ pub(super) async fn run_agent_spawn( // Read the story's front matter to find the epic ID, then load the epic's // content and prepend it to the system prompt so the agent treats it as // authoritative context. + // + // Epic linkage has no CRDT register yet (story 933) — wrap the parse in + // `yaml_residue` so the gap is grep-findable. if let Some(story_content) = crate::db::read_content(&sid) - && let Ok(meta) = crate::db::yaml_legacy::parse_front_matter(&story_content) + && let Ok(meta) = crate::db::yaml_legacy::yaml_residue( + crate::db::yaml_legacy::parse_front_matter(&story_content), + ) && let Some(ref epic_id) = meta.epic && let Some(epic_content) = crate::db::read_content(epic_id) { diff --git a/server/src/agents/pool/start/tests_selection.rs b/server/src/agents/pool/start/tests_selection.rs index 9b75a82f..1ff0a438 100644 --- a/server/src/agents/pool/start/tests_selection.rs +++ b/server/src/agents/pool/start/tests_selection.rs @@ -281,6 +281,20 @@ stage = "coder" std::fs::write(current.join("368_story_test.md"), story_content).unwrap(); crate::db::ensure_content_store(); crate::db::write_content("368_story_test", story_content); + // Story 929: agent pin comes from the CRDT register, not YAML. Seed it. + crate::crdt_state::init_for_test(); + crate::crdt_state::write_item( + "368_story_test", + "2_current", + Some("Test Story"), + Some("coder-opus"), + None, + None, + None, + None, + None, + None, + ); let pool = AgentPool::new_test(3011); // Preferred agent is busy — should NOT fall back to coder-sonnet. diff --git a/server/src/agents/pool/start/validation.rs b/server/src/agents/pool/start/validation.rs index 233fbedd..b712161d 100644 --- a/server/src/agents/pool/start/validation.rs +++ b/server/src/agents/pool/start/validation.rs @@ -60,18 +60,10 @@ 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() - && !agent.is_empty() - { - return Some(agent.to_string()); - } - crate::db::read_content(story_id).and_then(|contents| { - crate::db::yaml_legacy::parse_front_matter(&contents) - .ok()? - .agent - }) + // Story 929: the agent pin lives in the CRDT typed register; the + // legacy YAML fallback is gone — post-891 every story has its CRDT + // entry and any story without one has no pinned agent. + crate::crdt_state::read_item(story_id) + .and_then(|w| w.agent().map(str::to_string)) + .filter(|s| !s.is_empty()) }