huskies: merge 530_story_eliminate_filesystem_markdown_shadows_entirely_crdt_db_is_the_only_story_store

This commit is contained in:
dave
2026-04-10 14:56:13 +00:00
parent 1dd675796b
commit 11d19d8902
26 changed files with 966 additions and 1668 deletions
@@ -38,13 +38,7 @@ impl AgentPool {
let items = scan_stage_items(project_root, "1_backlog");
for story_id in &items {
// Only promote stories that explicitly declare dependencies.
// Try content store first, fall back to filesystem.
let contents = crate::db::read_content(story_id).or_else(|| {
let story_path = project_root
.join(".huskies/work/1_backlog")
.join(format!("{story_id}.md"));
std::fs::read_to_string(&story_path).ok()
});
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)
@@ -382,38 +376,40 @@ mod tests {
async fn auto_assign_ignores_coder_preference_when_story_is_in_qa_stage() {
let tmp = tempfile::tempdir().unwrap();
let sk = tmp.path().join(".huskies");
let qa_dir = sk.join("work/3_qa");
std::fs::create_dir_all(&qa_dir).unwrap();
std::fs::create_dir_all(&sk).unwrap();
std::fs::write(
sk.join("project.toml"),
"[[agent]]\nname = \"coder-1\"\nstage = \"coder\"\n\n\
[[agent]]\nname = \"qa-1\"\nstage = \"qa\"\n",
)
.unwrap();
// Story in 3_qa/ with a preferred coder-stage agent.
std::fs::write(
qa_dir.join("story-qa1.md"),
// Story in 3_qa/ with a preferred coder-stage agent — write via CRDT.
crate::db::ensure_content_store();
crate::db::write_item_with_content(
"9930_story_qa1",
"3_qa",
"---\nname: QA Story\nagent: coder-1\n---\n",
)
.unwrap();
);
let pool = AgentPool::new_test(3001);
pool.auto_assign_available_work(tmp.path()).await;
let agents = pool.agents.lock().unwrap();
// coder-1 must NOT have been assigned (wrong stage for 3_qa/).
let coder_assigned = agents.values().any(|a| {
a.agent_name == "coder-1"
// coder-1 must NOT have been assigned to the QA story (wrong stage).
let coder_assigned_to_qa = agents.iter().any(|(key, a)| {
key.contains("9930_story_qa1")
&& a.agent_name == "coder-1"
&& matches!(a.status, AgentStatus::Pending | AgentStatus::Running)
});
assert!(
!coder_assigned,
!coder_assigned_to_qa,
"coder-1 should not be assigned to a QA-stage story"
);
// qa-1 should have been assigned instead.
let qa_assigned = agents.values().any(|a| {
a.agent_name == "qa-1"
let qa_assigned = agents.iter().any(|(key, a)| {
key.contains("9930_story_qa1")
&& a.agent_name == "qa-1"
&& matches!(a.status, AgentStatus::Pending | AgentStatus::Running)
});
assert!(
@@ -429,8 +425,7 @@ mod tests {
async fn auto_assign_respects_coder_preference_when_story_is_in_current_stage() {
let tmp = tempfile::tempdir().unwrap();
let sk = tmp.path().join(".huskies");
let current_dir = sk.join("work/2_current");
std::fs::create_dir_all(&current_dir).unwrap();
std::fs::create_dir_all(sk.join("work/2_current")).unwrap();
std::fs::write(
sk.join("project.toml"),
"[[agent]]\nname = \"coder-1\"\nstage = \"coder\"\n\n\
@@ -438,11 +433,12 @@ mod tests {
)
.unwrap();
// Story in 2_current/ with a preferred coder-1 agent.
std::fs::write(
current_dir.join("story-pref.md"),
crate::db::ensure_content_store();
crate::db::write_item_with_content(
"story-pref",
"2_current",
"---\nname: Coder Story\nagent: coder-1\n---\n",
)
.unwrap();
);
let pool = AgentPool::new_test(3001);
@@ -476,20 +472,20 @@ mod tests {
async fn auto_assign_stage_mismatch_with_no_fallback_starts_no_agent() {
let tmp = tempfile::tempdir().unwrap();
let sk = tmp.path().join(".huskies");
let qa_dir = sk.join("work/3_qa");
std::fs::create_dir_all(&qa_dir).unwrap();
std::fs::create_dir_all(&sk).unwrap();
// Only a coder agent is configured — no QA agent exists.
std::fs::write(
sk.join("project.toml"),
"[[agent]]\nname = \"coder-1\"\nstage = \"coder\"\n",
)
.unwrap();
// Story in 3_qa/ requests coder-1 (wrong stage) and no QA agent exists.
std::fs::write(
qa_dir.join("story-noqa.md"),
// Story in 3_qa/ requests coder-1 (wrong stage) and no QA agent exists — write via CRDT.
crate::db::ensure_content_store();
crate::db::write_item_with_content(
"9931_story_noqa",
"3_qa",
"---\nname: QA Story No Agent\nagent: coder-1\n---\n",
)
.unwrap();
);
let pool = AgentPool::new_test(3001);
@@ -497,8 +493,14 @@ mod tests {
pool.auto_assign_available_work(tmp.path()).await;
let agents = pool.agents.lock().unwrap();
// No agent should be assigned to the specific QA story (coder-1 may
// be assigned to leaked 2_current items from the global CRDT store).
let assigned_to_qa_story = agents.iter().any(|(key, a)| {
key.contains("9931_story_noqa")
&& matches!(a.status, AgentStatus::Pending | AgentStatus::Running)
});
assert!(
agents.is_empty(),
!assigned_to_qa_story,
"No agent should be started when no stage-appropriate agent is available"
);
}
@@ -510,26 +512,32 @@ mod tests {
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path();
let sk = root.join(".huskies");
let current = sk.join("work/2_current");
std::fs::create_dir_all(&current).unwrap();
std::fs::create_dir_all(&sk).unwrap();
std::fs::write(
sk.join("project.toml"),
"[[agent]]\nname = \"coder-1\"\nstage = \"coder\"\n",
)
.unwrap();
// Story 10 depends on 999 which is not done.
std::fs::write(
current.join("10_story_waiting.md"),
"---\nname: Waiting\ndepends_on: [999]\n---\n",
)
.unwrap();
// Story 9932 depends on 9999 which is not done — write via CRDT.
crate::db::ensure_content_store();
crate::db::write_item_with_content(
"9932_story_waiting",
"2_current",
"---\nname: Waiting\ndepends_on: [9999]\n---\n",
);
let pool = AgentPool::new_test(3001);
pool.auto_assign_available_work(root).await;
let agents = pool.agents.lock().unwrap();
// Filter to only agents assigned to our specific story to avoid
// interference from other tests sharing the global CRDT store.
let assigned_to_our_story = agents.iter().any(|(key, a)| {
key.contains("9932_story_waiting")
&& matches!(a.status, AgentStatus::Pending | AgentStatus::Running)
});
assert!(
agents.is_empty(),
!assigned_to_our_story,
"story with unmet deps should not be auto-assigned"
);
}