//! Backlog promotion: scan items in `Pipeline::Backlog` and promote stories whose `depends_on` are all met. use crate::pipeline_state::Pipeline; use crate::slog; use crate::slog_warn; use super::super::AgentPool; use super::story_checks::{check_archived_dependencies, has_unmet_dependencies}; impl AgentPool { /// Scan items in `Pipeline::Backlog` and promote any story whose `depends_on` are all met. /// /// A story is only promoted if it explicitly lists `depends_on` AND every /// listed dependency has reached `Pipeline::Done` or `Pipeline::Archived`. /// Stories with no `depends_on` are left in the backlog for human scheduling. /// /// **Archived dep semantics:** a dep in `Pipeline::Archived` counts as satisfied /// (since stories auto-sweep from `Done` to `Archived` after 4 hours, and the /// dependent story would normally already be promoted by then). However, if a /// dep was already archived when the dependent story was created (e.g. it /// was abandoned/superseded before the dependent existed), a prominent warning is /// 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) { // Story 1086: scan by Pipeline column, not Stage variant. Pipeline::Backlog // covers Stage::Upcoming and Stage::Backlog uniformly. let items: Vec = { use std::collections::BTreeSet; let mut ids = BTreeSet::new(); for item in crate::pipeline_state::read_all_typed() { if item.stage.pipeline() == Pipeline::Backlog { ids.insert(item.story_id.0.clone()); } } ids.into_iter().collect() }; for story_id in &items { // 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; } // Check whether any dependencies are still unmet. if has_unmet_dependencies(story_id) { continue; } // Warn if any deps were satisfied via archive rather than via clean done. let archived_deps = check_archived_dependencies(story_id); if !archived_deps.is_empty() { slog_warn!( "[auto-assign] Story '{story_id}' is being promoted because deps \ {archived_deps:?} are in 6_archived (not cleanly completed via 5_done). \ These deps may have been abandoned or superseded. If this promotion is \ unintentional, remove the depends_on or manually move the story back to \ 1_backlog." ); } // All deps met — promote from backlog to current. slog!("[auto-assign] Story '{story_id}' deps met; promoting from backlog to current."); if let Err(e) = crate::agents::lifecycle::move_story_to_current(story_id) { slog!("[auto-assign] Failed to promote '{story_id}' to current: {e}"); } } } }