diff --git a/frontend/src/slashCommands.ts b/frontend/src/slashCommands.ts index f19b76c7..2879295e 100644 --- a/frontend/src/slashCommands.ts +++ b/frontend/src/slashCommands.ts @@ -10,7 +10,8 @@ export const SLASH_COMMANDS: SlashCommand[] = [ }, { name: "/backlog", - description: "Show all items in the backlog with dependency satisfaction status.", + description: + "Show all items in the backlog with dependency satisfaction status.", }, { name: "/status", diff --git a/script/lint b/script/lint index 28ef25d2..dfbb027e 100755 --- a/script/lint +++ b/script/lint @@ -6,7 +6,7 @@ PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" echo "=== Checking Rust formatting ===" if cargo fmt --version &>/dev/null; then - cargo fmt --manifest-path "$PROJECT_ROOT/Cargo.toml" --check + cargo fmt --manifest-path "$PROJECT_ROOT/Cargo.toml" --all --check else echo "Skipping Rust formatting check (rustfmt not installed)" fi diff --git a/server/src/agents/pool/auto_assign/auto_assign.rs b/server/src/agents/pool/auto_assign/auto_assign.rs index 52431713..8eb96a8d 100644 --- a/server/src/agents/pool/auto_assign/auto_assign.rs +++ b/server/src/agents/pool/auto_assign/auto_assign.rs @@ -300,15 +300,15 @@ mod tests { async fn auto_assign_picks_up_story_queued_in_current() { let tmp = tempfile::tempdir().unwrap(); let sk = tmp.path().join(".huskies"); - let current = sk.join("work/2_current"); - std::fs::create_dir_all(¤t).unwrap(); + std::fs::create_dir_all(&sk).unwrap(); std::fs::write( sk.join("project.toml"), "[[agent]]\nname = \"coder-1\"\nstage = \"coder\"\n", ) .unwrap(); - // Place the story in 2_current/ (simulating the "queued" state). - std::fs::write(current.join("story-3.md"), "---\nname: Story 3\n---\n").unwrap(); + // Place the story in 2_current/ via CRDT (the only source of truth). + crate::db::ensure_content_store(); + crate::db::write_item_with_content("story-3", "2_current", "---\nname: Story 3\n---\n"); let pool = AgentPool::new_test(3001); // No agents are running — coder-1 is free. @@ -549,23 +549,22 @@ mod tests { let tmp = tempfile::tempdir().unwrap(); let root = tmp.path(); let sk = root.join(".huskies"); - let current = sk.join("work/2_current"); - let done = sk.join("work/5_done"); - std::fs::create_dir_all(¤t).unwrap(); - std::fs::create_dir_all(&done).unwrap(); + std::fs::create_dir_all(&sk).unwrap(); std::fs::write( sk.join("project.toml"), "[[agent]]\nname = \"coder-1\"\nstage = \"coder\"\n", ) .unwrap(); + // Seed stories via CRDT (the only source of truth). + crate::db::ensure_content_store(); // Dep 999 is now done. - std::fs::write(done.join("999_story_dep.md"), "---\nname: Dep\n---\n").unwrap(); + crate::db::write_item_with_content("999_story_dep", "5_done", "---\nname: Dep\n---\n"); // Story 10 depends on 999 which is done. - std::fs::write( - current.join("10_story_unblocked.md"), + crate::db::write_item_with_content( + "10_story_unblocked", + "2_current", "---\nname: Unblocked\ndepends_on: [999]\n---\n", - ) - .unwrap(); + ); let pool = AgentPool::new_test(3001); pool.auto_assign_available_work(root).await; diff --git a/server/src/agents/pool/pipeline/advance.rs b/server/src/agents/pool/pipeline/advance.rs index 8c75007b..01ce02e8 100644 --- a/server/src/agents/pool/pipeline/advance.rs +++ b/server/src/agents/pool/pipeline/advance.rs @@ -690,14 +690,9 @@ mod tests { let tmp = tempfile::tempdir().unwrap(); let root = tmp.path(); - // Set up story in 2_current/ - let current = root.join(".huskies/work/2_current"); - fs::create_dir_all(¤t).unwrap(); - fs::write(current.join("173_story_test.md"), "test").unwrap(); - // Ensure 3_qa/ exists for the move target - fs::create_dir_all(root.join(".huskies/work/3_qa")).unwrap(); - // Ensure 1_backlog/ exists (start_agent calls move_story_to_current) - fs::create_dir_all(root.join(".huskies/work/1_backlog")).unwrap(); + // Seed story via CRDT (the only source of truth). + crate::db::ensure_content_store(); + crate::db::write_item_with_content("173_story_test", "2_current", "---\nname: test\n---\n"); // Write a project.toml with a qa agent so start_agent can resolve it. fs::create_dir_all(root.join(".huskies")).unwrap(); @@ -880,8 +875,7 @@ stage = "qa" let root = tmp.path(); let sk = root.join(".huskies"); - let qa_dir = sk.join("work/3_qa"); - fs::create_dir_all(&qa_dir).unwrap(); + fs::create_dir_all(&sk).unwrap(); // Configure a single QA agent. fs::write( @@ -894,19 +888,21 @@ stage = "qa" ) .unwrap(); + // Seed stories via CRDT (the only source of truth). + crate::db::ensure_content_store(); // Story 292 is in QA with QA agent running (will "complete" via // run_pipeline_advance below). Story 293 is in QA with NO agent — // simulating the "stuck" state from bug 295. - fs::write( - qa_dir.join("292_story_first.md"), + crate::db::write_item_with_content( + "292_story_first", + "3_qa", "---\nname: First\nqa: human\n---\n", - ) - .unwrap(); - fs::write( - qa_dir.join("293_story_second.md"), + ); + crate::db::write_item_with_content( + "293_story_second", + "3_qa", "---\nname: Second\nqa: human\n---\n", - ) - .unwrap(); + ); let pool = AgentPool::new_test(3001); // QA is currently running on story 292. diff --git a/server/src/chat/transport/matrix/notifications.rs b/server/src/chat/transport/matrix/notifications.rs index c166bc04..3e389627 100644 --- a/server/src/chat/transport/matrix/notifications.rs +++ b/server/src/chat/transport/matrix/notifications.rs @@ -35,16 +35,11 @@ pub fn extract_story_number(item_id: &str) -> Option<&str> { .filter(|s| !s.is_empty() && s.chars().all(|c| c.is_ascii_digit())) } -/// Read the story name from the work item file's YAML front matter. +/// Read the story name from the CRDT content store's YAML front matter. /// -/// Returns `None` if the file doesn't exist or has no parseable name. -pub fn read_story_name(project_root: &Path, stage: &str, item_id: &str) -> Option { - let path = project_root - .join(".huskies") - .join("work") - .join(stage) - .join(format!("{item_id}.md")); - let contents = std::fs::read_to_string(&path).ok()?; +/// Returns `None` if the item is not in the content store or has no parseable name. +pub fn read_story_name(_project_root: &Path, _stage: &str, item_id: &str) -> Option { + let contents = crate::db::read_content(item_id)?; let meta = parse_front_matter(&contents).ok()?; meta.name } @@ -85,18 +80,11 @@ pub fn format_error_notification( (plain, html) } -/// Search all pipeline stages for a story name. +/// Look up a story name from the CRDT content store. /// -/// Tries each known pipeline stage directory in order and returns the first -/// name found. Used for events (like rate-limit warnings) that arrive without -/// a known stage. +/// Used for events (like rate-limit warnings) that arrive without a known stage. fn find_story_name_any_stage(project_root: &Path, item_id: &str) -> Option { - for stage in &["2_current", "3_qa", "4_merge", "1_backlog", "5_done"] { - if let Some(name) = read_story_name(project_root, stage, item_id) { - return Some(name); - } - } - None + read_story_name(project_root, "", item_id) } /// Format a blocked-story notification message. @@ -432,13 +420,13 @@ mod tests { #[tokio::test] async fn rate_limit_warning_sends_notification_with_agent_and_story() { let tmp = tempfile::tempdir().unwrap(); - let stage_dir = tmp.path().join(".huskies").join("work").join("2_current"); - std::fs::create_dir_all(&stage_dir).unwrap(); - std::fs::write( - stage_dir.join("365_story_rate_limit.md"), + // Seed story via CRDT (the only source of truth). + crate::db::ensure_content_store(); + crate::db::write_item_with_content( + "365_story_rate_limit", + "2_current", "---\nname: Rate Limit Test Story\n---\n", - ) - .unwrap(); + ); let (watcher_tx, watcher_rx) = broadcast::channel::(16); let (transport, calls) = MockTransport::new(); @@ -560,13 +548,9 @@ mod tests { #[tokio::test] async fn stage_notification_uses_dynamic_room_ids() { let tmp = tempfile::tempdir().unwrap(); - let stage_dir = tmp.path().join(".huskies").join("work").join("3_qa"); - std::fs::create_dir_all(&stage_dir).unwrap(); - std::fs::write( - stage_dir.join("10_story_foo.md"), - "---\nname: Foo Story\n---\n", - ) - .unwrap(); + // Seed story via CRDT (the only source of truth). + crate::db::ensure_content_store(); + crate::db::write_item_with_content("10_story_foo", "3_qa", "---\nname: Foo Story\n---\n"); let (watcher_tx, watcher_rx) = broadcast::channel::(16); let (transport, calls) = MockTransport::new(); @@ -681,38 +665,37 @@ mod tests { #[test] fn read_story_name_reads_from_front_matter() { - let tmp = tempfile::tempdir().unwrap(); - let stage_dir = tmp.path().join(".huskies").join("work").join("2_current"); - std::fs::create_dir_all(&stage_dir).unwrap(); - std::fs::write( - stage_dir.join("42_story_my_feature.md"), + crate::db::ensure_content_store(); + crate::db::write_item_with_content( + "9942_story_my_feature", + "2_current", "---\nname: My Cool Feature\n---\n# Story\n", - ) - .unwrap(); + ); - let name = read_story_name(tmp.path(), "2_current", "42_story_my_feature"); + let tmp = tempfile::tempdir().unwrap(); + let name = read_story_name(tmp.path(), "2_current", "9942_story_my_feature"); assert_eq!(name.as_deref(), Some("My Cool Feature")); } #[test] fn read_story_name_returns_none_for_missing_file() { + crate::db::ensure_content_store(); let tmp = tempfile::tempdir().unwrap(); - let name = read_story_name(tmp.path(), "2_current", "99_story_missing"); + let name = read_story_name(tmp.path(), "2_current", "99_story_missing_notif_test"); assert_eq!(name, None); } #[test] fn read_story_name_returns_none_for_missing_name_field() { - let tmp = tempfile::tempdir().unwrap(); - let stage_dir = tmp.path().join(".huskies").join("work").join("2_current"); - std::fs::create_dir_all(&stage_dir).unwrap(); - std::fs::write( - stage_dir.join("42_story_no_name.md"), + crate::db::ensure_content_store(); + crate::db::write_item_with_content( + "9943_story_no_name", + "2_current", "---\ncoverage_baseline: 50%\n---\n# Story\n", - ) - .unwrap(); + ); - let name = read_story_name(tmp.path(), "2_current", "42_story_no_name"); + let tmp = tempfile::tempdir().unwrap(); + let name = read_story_name(tmp.path(), "2_current", "9943_story_no_name"); assert_eq!(name, None); } @@ -786,13 +769,13 @@ mod tests { #[tokio::test] async fn story_blocked_sends_notification_with_reason() { let tmp = tempfile::tempdir().unwrap(); - let stage_dir = tmp.path().join(".huskies").join("work").join("2_current"); - std::fs::create_dir_all(&stage_dir).unwrap(); - std::fs::write( - stage_dir.join("425_story_blocking_test.md"), + // Seed story via CRDT (the only source of truth). + crate::db::ensure_content_store(); + crate::db::write_item_with_content( + "425_story_blocking_test", + "2_current", "---\nname: Blocking Test Story\n---\n", - ) - .unwrap(); + ); let (watcher_tx, watcher_rx) = broadcast::channel::(16); let (transport, calls) = MockTransport::new();