huskies: merge 497_bug_dependency_promotion_loop_missing_stories_with_met_deps_never_move_from_backlog_to_current

This commit is contained in:
dave
2026-04-08 01:28:53 +00:00
parent bc429edf49
commit eba933e21e
@@ -19,7 +19,47 @@ use super::story_checks::{
}; };
impl AgentPool { impl AgentPool {
/// Scan `1_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 `5_done` or `6_archived`. Stories with no
/// `depends_on` are left in the backlog for human scheduling.
fn promote_ready_backlog_stories(&self, project_root: &Path) {
use crate::io::story_metadata::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 story_path = project_root
.join(".huskies/work/1_backlog")
.join(format!("{story_id}.md"));
let has_deps = std::fs::read_to_string(&story_path)
.ok()
.and_then(|c| parse_front_matter(&c).ok())
.and_then(|m| m.depends_on)
.map(|d| !d.is_empty())
.unwrap_or(false);
if !has_deps {
continue;
}
// Check whether any dependencies are still unmet.
if has_unmet_dependencies(project_root, "1_backlog", story_id) {
continue;
}
// 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(project_root, story_id)
{
slog!("[auto-assign] Failed to promote '{story_id}' to current: {e}");
}
}
}
pub async fn auto_assign_available_work(&self, project_root: &Path) { pub async fn auto_assign_available_work(&self, project_root: &Path) {
// Promote any backlog stories whose dependencies are all done.
self.promote_ready_backlog_stories(project_root);
let config = match ProjectConfig::load(project_root) { let config = match ProjectConfig::load(project_root) {
Ok(c) => c, Ok(c) => c,
Err(e) => { Err(e) => {
@@ -497,6 +537,105 @@ mod tests {
); );
} }
// ── Bug 497: backlog dependency promotion ───────────────────────────────
/// Stories in backlog with `depends_on` that are all in 5_done must be
/// promoted to 2_current when auto_assign_available_work runs.
#[tokio::test]
async fn auto_assign_promotes_backlog_story_when_all_deps_done() {
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path();
let sk = root.join(".huskies");
let backlog = sk.join("work/1_backlog");
let current = sk.join("work/2_current");
let done = sk.join("work/5_done");
std::fs::create_dir_all(&backlog).unwrap();
std::fs::create_dir_all(&current).unwrap();
std::fs::create_dir_all(&done).unwrap();
std::fs::write(
sk.join("project.toml"),
"[[agent]]\nname = \"coder-1\"\nstage = \"coder\"\n",
)
.unwrap();
// Dep 1 is done.
std::fs::write(done.join("1_story_dep.md"), "---\nname: Dep\n---\n").unwrap();
// Story B depends on story 1.
std::fs::write(
backlog.join("2_story_b.md"),
"---\nname: B\ndepends_on: [1]\n---\n",
)
.unwrap();
let pool = AgentPool::new_test(3001);
pool.auto_assign_available_work(root).await;
assert!(
current.join("2_story_b.md").exists(),
"story B should be promoted to 2_current/ once dep 1 is done"
);
assert!(
!backlog.join("2_story_b.md").exists(),
"story B must be removed from 1_backlog/ after promotion"
);
}
/// Stories in backlog with unmet dependencies must NOT be promoted.
#[tokio::test]
async fn auto_assign_does_not_promote_backlog_story_with_unmet_deps() {
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path();
let sk = root.join(".huskies");
let backlog = sk.join("work/1_backlog");
std::fs::create_dir_all(&backlog).unwrap();
std::fs::write(
sk.join("project.toml"),
"[[agent]]\nname = \"coder-1\"\nstage = \"coder\"\n",
)
.unwrap();
// Dep 99 is NOT done.
std::fs::write(
backlog.join("5_story_c.md"),
"---\nname: C\ndepends_on: [99]\n---\n",
)
.unwrap();
let pool = AgentPool::new_test(3001);
pool.auto_assign_available_work(root).await;
assert!(
backlog.join("5_story_c.md").exists(),
"story C should stay in 1_backlog/ when dep 99 is not done"
);
}
/// Stories in backlog with NO depends_on must NOT be auto-promoted.
#[tokio::test]
async fn auto_assign_does_not_promote_backlog_story_without_deps() {
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path();
let sk = root.join(".huskies");
let backlog = sk.join("work/1_backlog");
std::fs::create_dir_all(&backlog).unwrap();
std::fs::write(
sk.join("project.toml"),
"[[agent]]\nname = \"coder-1\"\nstage = \"coder\"\n",
)
.unwrap();
std::fs::write(
backlog.join("7_story_nodeps.md"),
"---\nname: No deps\n---\n",
)
.unwrap();
let pool = AgentPool::new_test(3001);
pool.auto_assign_available_work(root).await;
assert!(
backlog.join("7_story_nodeps.md").exists(),
"story with no depends_on should stay in 1_backlog/ — human schedules it"
);
}
/// Two concurrent auto_assign_available_work calls must not assign the same /// Two concurrent auto_assign_available_work calls must not assign the same
/// agent to two stories simultaneously. After both complete, at most one /// agent to two stories simultaneously. After both complete, at most one
/// Pending/Running entry must exist per agent name. /// Pending/Running entry must exist per agent name.