huskies: merge 497_bug_dependency_promotion_loop_missing_stories_with_met_deps_never_move_from_backlog_to_current
This commit is contained in:
@@ -19,7 +19,47 @@ use super::story_checks::{
|
||||
};
|
||||
|
||||
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) {
|
||||
// Promote any backlog stories whose dependencies are all done.
|
||||
self.promote_ready_backlog_stories(project_root);
|
||||
|
||||
let config = match ProjectConfig::load(project_root) {
|
||||
Ok(c) => c,
|
||||
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(¤t).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
|
||||
/// agent to two stories simultaneously. After both complete, at most one
|
||||
/// Pending/Running entry must exist per agent name.
|
||||
|
||||
Reference in New Issue
Block a user