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
+27 -34
View File
@@ -92,7 +92,7 @@ pub async fn handle_assign(
agents: &AgentPool,
) -> String {
// Find the story by numeric prefix: CRDT → content store → filesystem.
let (story_id, _stage_dir, path, content) =
let (story_id, _stage_dir, _path, content) =
match crate::chat::lookup::find_story_by_number(project_root, story_number) {
Some(found) => found,
None => {
@@ -102,21 +102,24 @@ pub async fn handle_assign(
}
};
let story_name = content
.or_else(|| std::fs::read_to_string(&path).ok())
.and_then(|contents| parse_front_matter(&contents).ok().and_then(|m| m.name))
let current_content = content.or_else(|| crate::db::read_content(&story_id));
let story_name = current_content
.as_ref()
.and_then(|c| parse_front_matter(c).ok().and_then(|m| m.name))
.unwrap_or_else(|| story_id.clone());
let agent_name = resolve_agent_name(model_str);
// Write `agent: <agent_name>` into the story's front matter.
let write_result = std::fs::read_to_string(&path)
.map_err(|e| format!("Failed to read story file: {e}"))
.and_then(|contents| {
// Write `agent: <agent_name>` into the story's front matter via content store.
let write_result = match current_content {
Some(contents) => {
let updated = set_front_matter_field(&contents, "agent", &agent_name);
std::fs::write(&path, &updated)
.map_err(|e| format!("Failed to write story file: {e}"))
});
crate::db::write_item_with_content(&story_id, &_stage_dir, &updated);
Ok(())
}
None => Err(format!("Story content not found for {story_id}")),
};
if let Err(e) = write_result {
return format!("Failed to assign model to **{story_name}**: {e}");
@@ -304,15 +307,11 @@ mod tests {
// -- handle_assign (no running coder) ------------------------------------
use crate::chat::lookup::STAGES;
use crate::chat::test_helpers::write_story_file;
#[tokio::test]
async fn handle_assign_returns_not_found_for_unknown_number() {
let tmp = tempfile::tempdir().unwrap();
for stage in STAGES {
std::fs::create_dir_all(tmp.path().join(".huskies/work").join(stage)).unwrap();
}
let agents = std::sync::Arc::new(AgentPool::new_test(3000));
let response = handle_assign("Timmy", "999", "opus", tmp.path(), &agents).await;
assert!(
@@ -327,12 +326,12 @@ mod tests {
write_story_file(
tmp.path(),
"1_backlog",
"42_story_test.md",
"---\nname: Test Feature\n---\n\n# Story 42\n",
"9972_story_test.md",
"---\nname: Test Feature\n---\n\n# Story 9972\n",
);
let agents = std::sync::Arc::new(AgentPool::new_test(3000));
let response = handle_assign("Timmy", "42", "opus", tmp.path(), &agents).await;
let response = handle_assign("Timmy", "9972", "opus", tmp.path(), &agents).await;
assert!(
response.contains("coder-opus"),
@@ -348,10 +347,8 @@ mod tests {
"response should indicate assignment for future start: {response}"
);
let contents = std::fs::read_to_string(
tmp.path().join(".huskies/work/1_backlog/42_story_test.md"),
)
.unwrap();
let contents = crate::db::read_content("9972_story_test")
.expect("content store should have updated content");
assert!(
contents.contains("agent: coder-opus"),
"front matter should contain agent field: {contents}"
@@ -364,12 +361,12 @@ mod tests {
write_story_file(
tmp.path(),
"1_backlog",
"7_story_small.md",
"9973_story_small.md",
"---\nname: Small Story\n---\n",
);
let agents = std::sync::Arc::new(AgentPool::new_test(3000));
let response = handle_assign("Timmy", "7", "coder-opus", tmp.path(), &agents).await;
let response = handle_assign("Timmy", "9973", "coder-opus", tmp.path(), &agents).await;
assert!(
response.contains("coder-opus"),
@@ -380,10 +377,8 @@ mod tests {
"must not double-prefix: {response}"
);
let contents = std::fs::read_to_string(
tmp.path().join(".huskies/work/1_backlog/7_story_small.md"),
)
.unwrap();
let contents = crate::db::read_content("9973_story_small")
.expect("content store should have updated content");
assert!(
contents.contains("agent: coder-opus"),
"must write coder-opus, not coder-coder-opus: {contents}"
@@ -396,17 +391,15 @@ mod tests {
write_story_file(
tmp.path(),
"1_backlog",
"5_story_existing.md",
"9974_story_existing.md",
"---\nname: Existing\nagent: coder-sonnet\n---\n",
);
let agents = std::sync::Arc::new(AgentPool::new_test(3000));
handle_assign("Timmy", "5", "opus", tmp.path(), &agents).await;
handle_assign("Timmy", "9974", "opus", tmp.path(), &agents).await;
let contents = std::fs::read_to_string(
tmp.path().join(".huskies/work/1_backlog/5_story_existing.md"),
)
.unwrap();
let contents = crate::db::read_content("9974_story_existing")
.expect("content store should have updated content");
assert!(
contents.contains("agent: coder-opus"),
"should overwrite old agent: {contents}"
+16 -54
View File
@@ -61,7 +61,7 @@ pub async fn handle_delete(
agents: &AgentPool,
) -> String {
// Find the story by numeric prefix: CRDT → content store → filesystem.
let (story_id, stage, path, content) =
let (story_id, stage, _path, content) =
match crate::chat::lookup::find_story_by_number(project_root, story_number) {
Some(found) => found,
None => {
@@ -72,7 +72,6 @@ pub async fn handle_delete(
};
let story_name = content
.or_else(|| std::fs::read_to_string(&path).ok())
.and_then(|contents| {
crate::io::story_metadata::parse_front_matter(&contents)
.ok()
@@ -103,23 +102,9 @@ pub async fn handle_delete(
// Remove the worktree if one exists (best-effort; ignore errors).
let _ = crate::worktree::prune_worktree_sync(project_root, &story_id);
// Delete the story file.
if let Err(e) = std::fs::remove_file(&path) {
return format!("Failed to delete story {story_number}: {e}");
}
// Commit the deletion to git.
let commit_msg = format!("huskies: delete {story_id}");
let work_rel = std::path::PathBuf::from(".huskies").join("work");
let _ = std::process::Command::new("git")
.args(["add", "-A"])
.arg(&work_rel)
.current_dir(project_root)
.output();
let _ = std::process::Command::new("git")
.args(["commit", "-m", &commit_msg])
.current_dir(project_root)
.output();
// Delete from the content store and CRDT.
crate::db::delete_content(&story_id);
crate::db::delete_item(&story_id);
// Build the response.
let stage_label = stage_display_name(&stage);
@@ -265,47 +250,24 @@ mod tests {
let tmp = tempfile::tempdir().unwrap();
let project_root = tmp.path();
// Init a bare git repo so the commit step doesn't fail fatally.
std::process::Command::new("git")
.args(["init"])
.current_dir(project_root)
.output()
.unwrap();
std::process::Command::new("git")
.args(["config", "user.email", "test@test.com"])
.current_dir(project_root)
.output()
.unwrap();
std::process::Command::new("git")
.args(["config", "user.name", "Test"])
.current_dir(project_root)
.output()
.unwrap();
let backlog_dir = project_root.join(".huskies").join("work").join("1_backlog");
std::fs::create_dir_all(&backlog_dir).unwrap();
let story_path = backlog_dir.join("42_story_some_feature.md");
std::fs::write(&story_path, "---\nname: Some Feature\n---\n\n# Story 42\n").unwrap();
// Initial commit so git doesn't complain about no commits.
std::process::Command::new("git")
.args(["add", "-A"])
.current_dir(project_root)
.output()
.unwrap();
std::process::Command::new("git")
.args(["commit", "-m", "init"])
.current_dir(project_root)
.output()
.unwrap();
// Seed the story in the content store + CRDT (no filesystem needed).
crate::db::ensure_content_store();
crate::db::write_item_with_content(
"9975_story_some_feature",
"1_backlog",
"---\nname: Some Feature\n---\n\n# Story 9975\n",
);
let agents = std::sync::Arc::new(crate::agents::AgentPool::new_test(3000));
let response = handle_delete("Timmy", "42", project_root, &agents).await;
let response = handle_delete("Timmy", "9975", project_root, &agents).await;
assert!(
response.contains("Some Feature") && response.contains("backlog"),
"unexpected response: {response}"
);
assert!(!story_path.exists(), "story file should have been deleted");
assert!(
crate::db::read_content("9975_story_some_feature").is_none(),
"content store should no longer contain the deleted story"
);
}
}
+10 -9
View File
@@ -80,7 +80,7 @@ pub async fn handle_start(
agents: &AgentPool,
) -> String {
// Find the story by numeric prefix: CRDT → content store → filesystem.
let (story_id, _stage_dir, path, content) =
let (story_id, _stage_dir, _path, content) =
match crate::chat::lookup::find_story_by_number(project_root, story_number) {
Some(found) => found,
None => {
@@ -91,7 +91,6 @@ pub async fn handle_start(
};
let story_name = content
.or_else(|| std::fs::read_to_string(&path).ok())
.and_then(|contents| {
crate::io::story_metadata::parse_front_matter(&contents)
.ok()
@@ -252,23 +251,25 @@ mod tests {
let tmp = tempfile::tempdir().unwrap();
let project_root = tmp.path();
let sk = project_root.join(".huskies");
let backlog = sk.join("work/1_backlog");
std::fs::create_dir_all(&backlog).unwrap();
std::fs::create_dir_all(&sk).unwrap();
std::fs::write(
sk.join("project.toml"),
"[[agent]]\nname = \"coder-1\"\nstage = \"coder\"\n",
)
.unwrap();
std::fs::write(
backlog.join("356_story_test.md"),
// Seed the story in the content store + CRDT (no filesystem needed).
crate::db::ensure_content_store();
crate::db::write_item_with_content(
"9976_story_test",
"1_backlog",
"---\nname: Test Story\n---\n",
)
.unwrap();
);
let agents = Arc::new(AgentPool::new_test(3000));
agents.inject_test_agent("other-story", "coder-1", AgentStatus::Running);
let response = handle_start("Timmy", "356", None, project_root, &agents).await;
let response = handle_start("Timmy", "9976", None, project_root, &agents).await;
assert!(
!response.contains("Failed"),