From 8faf19f3ab811a779c11ef8b5d68019e12c2a474 Mon Sep 17 00:00:00 2001 From: dave Date: Thu, 14 May 2026 13:57:27 +0000 Subject: [PATCH] huskies: merge 1034 --- server/src/chat/commands/depends.rs | 40 ++--- server/src/chat/commands/diff.rs | 40 ++--- server/src/chat/commands/freeze.rs | 66 ++++++-- server/src/chat/commands/logs.rs | 39 ++--- server/src/chat/commands/mod.rs | 78 +++++++++ server/src/chat/commands/move_story.rs | 48 ++++-- server/src/chat/commands/overview.rs | 41 ++--- server/src/chat/commands/show.rs | 39 ++--- server/src/chat/commands/triage.rs | 32 ++-- server/src/chat/commands/unblock.rs | 31 ++-- .../matrix/bot/messages/on_room_message.rs | 154 ++++++++---------- 11 files changed, 362 insertions(+), 246 deletions(-) diff --git a/server/src/chat/commands/depends.rs b/server/src/chat/commands/depends.rs index c9d5c774..6ec4f46d 100644 --- a/server/src/chat/commands/depends.rs +++ b/server/src/chat/commands/depends.rs @@ -18,22 +18,14 @@ pub(super) fn handle_depends(ctx: &CommandContext) -> Option { let args = ctx.args.trim(); if args.is_empty() { - return Some(format!( - "Usage: `{} depends [dep1 dep2 ...]`\n\nExamples:\n\ - • `{0} depends 484 477 478` — set depends_on: [477, 478]\n\ - • `{0} depends 484` — clear all dependencies", - ctx.services.bot_name - )); + return None; } let mut parts = args.split_whitespace(); let num_str = parts.next().unwrap_or(""); - if !num_str.chars().all(|c| c.is_ascii_digit()) || num_str.is_empty() { - return Some(format!( - "Invalid story number: `{num_str}`. Usage: `{} depends [dep1 dep2 ...]`", - ctx.services.bot_name - )); + if num_str.is_empty() || !num_str.chars().all(|c| c.is_ascii_digit()) { + return None; } // Parse dependency numbers. @@ -129,22 +121,32 @@ mod tests { } #[test] - fn depends_no_args_returns_usage() { + fn depends_no_args_routes_to_timmy() { let tmp = tempfile::TempDir::new().unwrap(); - let output = depends_cmd_with_root(tmp.path(), "").unwrap(); + let result = depends_cmd_with_root(tmp.path(), ""); assert!( - output.contains("Usage"), - "no args should show usage: {output}" + result.is_none(), + "no args should route to LLM (None): {result:?}" ); } #[test] - fn depends_non_numeric_number_returns_error() { + fn depends_non_numeric_number_routes_to_timmy() { let tmp = tempfile::TempDir::new().unwrap(); - let output = depends_cmd_with_root(tmp.path(), "abc 1 2").unwrap(); + let result = depends_cmd_with_root(tmp.path(), "on 477 and 478"); assert!( - output.contains("Invalid story number"), - "non-numeric story number should error: {output}" + result.is_none(), + "natural-language args should route to LLM (None): {result:?}" + ); + } + + #[test] + fn depends_well_formed_runs_handler() { + let tmp = tempfile::TempDir::new().unwrap(); + let result = depends_cmd_with_root(tmp.path(), "999"); + assert!( + result.is_some(), + "well-formed numeric story number should run the command handler: {result:?}" ); } diff --git a/server/src/chat/commands/diff.rs b/server/src/chat/commands/diff.rs index d5d9523b..d828fd34 100644 --- a/server/src/chat/commands/diff.rs +++ b/server/src/chat/commands/diff.rs @@ -13,17 +13,8 @@ use std::process::Command; /// Usage: `diff ` pub(super) fn handle_diff(ctx: &CommandContext) -> Option { let num_str = ctx.args.trim(); - if num_str.is_empty() { - return Some(format!( - "Usage: `{} diff `\n\nShows the git diff from the main branch to the story's worktree HEAD.", - ctx.services.bot_name - )); - } - if !num_str.chars().all(|c| c.is_ascii_digit()) { - return Some(format!( - "Invalid story number: `{num_str}`. Usage: `{} diff `", - ctx.services.bot_name - )); + if num_str.is_empty() || !num_str.chars().all(|c| c.is_ascii_digit()) { + return None; } let story_id = match find_story_id(num_str) { @@ -169,22 +160,33 @@ mod tests { } #[test] - fn diff_command_no_args_returns_usage() { + fn diff_command_no_args_routes_to_timmy() { let tmp = tempfile::TempDir::new().unwrap(); - let output = diff_cmd(tmp.path(), "").unwrap(); + let result = diff_cmd(tmp.path(), ""); assert!( - output.contains("Usage"), - "no args should show usage: {output}" + result.is_none(), + "no args should route to LLM (None): {result:?}" ); } #[test] - fn diff_command_non_numeric_returns_error() { + fn diff_command_non_numeric_routes_to_timmy() { let tmp = tempfile::TempDir::new().unwrap(); - let output = diff_cmd(tmp.path(), "abc").unwrap(); + let result = diff_cmd(tmp.path(), "the login feature branch"); assert!( - output.contains("Invalid"), - "non-numeric arg should return error: {output}" + result.is_none(), + "natural-language args should route to LLM (None): {result:?}" + ); + } + + #[test] + fn diff_command_well_formed_runs_handler() { + crate::db::ensure_content_store(); + let tmp = tempfile::TempDir::new().unwrap(); + let result = diff_cmd(tmp.path(), "99994"); + assert!( + result.is_some(), + "well-formed numeric arg should run the command handler: {result:?}" ); } diff --git a/server/src/chat/commands/freeze.rs b/server/src/chat/commands/freeze.rs index 3dca2b07..d837c00d 100644 --- a/server/src/chat/commands/freeze.rs +++ b/server/src/chat/commands/freeze.rs @@ -13,10 +13,7 @@ use std::path::Path; pub(super) fn handle_freeze(ctx: &CommandContext) -> Option { let num_str = ctx.args.trim(); if num_str.is_empty() || !num_str.chars().all(|c| c.is_ascii_digit()) { - return Some(format!( - "Usage: `{} freeze ` (e.g. `freeze 42`)", - ctx.services.bot_name - )); + return None; } Some(freeze_by_number(ctx.effective_root(), num_str)) } @@ -57,10 +54,7 @@ fn freeze_by_story_id(story_id: &str) -> String { pub(super) fn handle_unfreeze(ctx: &CommandContext) -> Option { let num_str = ctx.args.trim(); if num_str.is_empty() || !num_str.chars().all(|c| c.is_ascii_digit()) { - return Some(format!( - "Usage: `{} unfreeze ` (e.g. `unfreeze 42`)", - ctx.services.bot_name - )); + return None; } Some(unfreeze_by_number(ctx.effective_root(), num_str)) } @@ -155,22 +149,62 @@ mod tests { } #[test] - fn freeze_command_no_args_returns_usage() { + fn freeze_command_no_args_routes_to_timmy() { let tmp = tempfile::TempDir::new().unwrap(); - let output = freeze_cmd_with_root(tmp.path(), "").unwrap(); + let result = freeze_cmd_with_root(tmp.path(), ""); assert!( - output.contains("Usage"), - "no args should show usage: {output}" + result.is_none(), + "no args should route to LLM (None): {result:?}" ); } #[test] - fn unfreeze_command_no_args_returns_usage() { + fn freeze_command_non_numeric_routes_to_timmy() { let tmp = tempfile::TempDir::new().unwrap(); - let output = unfreeze_cmd_with_root(tmp.path(), "").unwrap(); + let result = freeze_cmd_with_root(tmp.path(), "the login story"); assert!( - output.contains("Usage"), - "no args should show usage: {output}" + result.is_none(), + "natural-language args should route to LLM (None): {result:?}" + ); + } + + #[test] + fn freeze_command_well_formed_runs_handler() { + let tmp = tempfile::TempDir::new().unwrap(); + let result = freeze_cmd_with_root(tmp.path(), "999"); + assert!( + result.is_some(), + "well-formed numeric arg should run the command handler: {result:?}" + ); + } + + #[test] + fn unfreeze_command_no_args_routes_to_timmy() { + let tmp = tempfile::TempDir::new().unwrap(); + let result = unfreeze_cmd_with_root(tmp.path(), ""); + assert!( + result.is_none(), + "no args should route to LLM (None): {result:?}" + ); + } + + #[test] + fn unfreeze_command_non_numeric_routes_to_timmy() { + let tmp = tempfile::TempDir::new().unwrap(); + let result = unfreeze_cmd_with_root(tmp.path(), "the login story"); + assert!( + result.is_none(), + "natural-language args should route to LLM (None): {result:?}" + ); + } + + #[test] + fn unfreeze_command_well_formed_runs_handler() { + let tmp = tempfile::TempDir::new().unwrap(); + let result = unfreeze_cmd_with_root(tmp.path(), "999"); + assert!( + result.is_some(), + "well-formed numeric arg should run the command handler: {result:?}" ); } diff --git a/server/src/chat/commands/logs.rs b/server/src/chat/commands/logs.rs index de3dec37..6fe41ea5 100644 --- a/server/src/chat/commands/logs.rs +++ b/server/src/chat/commands/logs.rs @@ -13,17 +13,8 @@ use std::path::{Path, PathBuf}; /// recently modified agent log file for that story. pub(super) fn handle_logs(ctx: &CommandContext) -> Option { let num_str = ctx.args.trim(); - if num_str.is_empty() { - return Some(format!( - "Usage: `{} logs `\n\nShows the last agent log lines for a story.", - ctx.services.bot_name - )); - } - if !num_str.chars().all(|c| c.is_ascii_digit()) { - return Some(format!( - "Invalid story number: `{num_str}`. Usage: `{} logs `", - ctx.services.bot_name - )); + if num_str.is_empty() || !num_str.chars().all(|c| c.is_ascii_digit()) { + return None; } let story_id = match find_story_id_by_number(num_str) { @@ -155,22 +146,32 @@ mod tests { // -- input validation --------------------------------------------------- #[test] - fn logs_no_args_returns_usage() { + fn logs_no_args_routes_to_timmy() { let tmp = tempfile::TempDir::new().unwrap(); - let output = logs_cmd(tmp.path(), "").unwrap(); + let result = logs_cmd(tmp.path(), ""); assert!( - output.contains("Usage"), - "no args should show usage: {output}" + result.is_none(), + "no args should route to LLM (None): {result:?}" ); } #[test] - fn logs_non_numeric_returns_error() { + fn logs_non_numeric_routes_to_timmy() { let tmp = tempfile::TempDir::new().unwrap(); - let output = logs_cmd(tmp.path(), "abc").unwrap(); + let result = logs_cmd(tmp.path(), "for the login story"); assert!( - output.contains("Invalid"), - "non-numeric arg should return error: {output}" + result.is_none(), + "natural-language args should route to LLM (None): {result:?}" + ); + } + + #[test] + fn logs_well_formed_runs_handler() { + let tmp = tempfile::TempDir::new().unwrap(); + let result = logs_cmd(tmp.path(), "99999"); + assert!( + result.is_some(), + "well-formed numeric arg should run the command handler: {result:?}" ); } diff --git a/server/src/chat/commands/mod.rs b/server/src/chat/commands/mod.rs index 46306984..c1f49f12 100644 --- a/server/src/chat/commands/mod.rs +++ b/server/src/chat/commands/mod.rs @@ -495,6 +495,84 @@ pub(crate) mod tests { ); } + // -- malformed-args routing (story 1034) ----------------------------------- + + #[test] + fn malformed_unblock_args_route_to_timmy() { + // "unblock to fix the blocking issue" — verb recognised, args look like + // natural language rather than a numeric story ID. Must return None so + // the message is forwarded to the LLM instead of showing a usage error. + let result = try_cmd_addressed( + "Timmy", + "@timmy:homeserver.local", + "@timmy unblock to fix the blocking issue", + ); + assert!( + result.is_none(), + "unblock with natural-language args must route to LLM (None): {result:?}" + ); + } + + #[test] + fn malformed_start_args_route_to_timmy_via_registry() { + // "start to get the cheese grater working" — the registry entry for + // start always returns None (handled async), so even bad args produce None. + let result = try_cmd_addressed( + "Timmy", + "@timmy:homeserver.local", + "@timmy start to get the cheese grater working", + ); + assert!( + result.is_none(), + "start with natural-language args must route to LLM (None): {result:?}" + ); + } + + #[test] + fn malformed_show_args_route_to_timmy() { + let result = try_cmd_addressed( + "Timmy", + "@timmy:homeserver.local", + "@timmy show me the login bug", + ); + assert!( + result.is_none(), + "show with natural-language args must route to LLM (None): {result:?}" + ); + } + + #[test] + fn malformed_freeze_args_route_to_timmy() { + let result = try_cmd_addressed( + "Timmy", + "@timmy:homeserver.local", + "@timmy freeze the pipeline until Friday", + ); + assert!( + result.is_none(), + "freeze with natural-language args must route to LLM (None): {result:?}" + ); + } + + #[test] + fn well_formed_unblock_runs_handler() { + // Numeric story ID → command handler runs (story not found, but Some is returned). + let result = try_cmd_addressed("Timmy", "@timmy:homeserver.local", "@timmy unblock 1010"); + assert!( + result.is_some(), + "well-formed unblock command should run the handler: {result:?}" + ); + } + + #[test] + fn well_formed_show_runs_handler() { + let result = try_cmd_addressed("Timmy", "@timmy:homeserver.local", "@timmy show 984"); + assert!( + result.is_some(), + "well-formed show command should run the handler: {result:?}" + ); + } + // -- commands registry -------------------------------------------------- #[test] diff --git a/server/src/chat/commands/move_story.rs b/server/src/chat/commands/move_story.rs index 74e77abb..2edc5a4a 100644 --- a/server/src/chat/commands/move_story.rs +++ b/server/src/chat/commands/move_story.rs @@ -22,19 +22,21 @@ pub(super) fn handle_move(ctx: &CommandContext) -> Option { let (num_str, stage_raw) = match args.split_once(char::is_whitespace) { Some((n, s)) => (n.trim(), s.trim()), None => { - return Some(format!( - "Usage: `{} move `\n\nValid stages: {}", - ctx.services.bot_name, - VALID_STAGES.join(", ") - )); + // No stage argument: if args looks like a number show usage, otherwise + // fall through to the LLM (looks like natural language). + if !args.is_empty() && args.chars().all(|c| c.is_ascii_digit()) { + return Some(format!( + "Usage: `{} move `\n\nValid stages: {}", + ctx.services.bot_name, + VALID_STAGES.join(", ") + )); + } + return None; } }; if num_str.is_empty() || !num_str.chars().all(|c| c.is_ascii_digit()) { - return Some(format!( - "Invalid story number: `{num_str}`. Usage: `{} move `", - ctx.services.bot_name - )); + return None; } let target_stage = stage_raw.to_ascii_lowercase(); @@ -113,12 +115,22 @@ mod tests { } #[test] - fn move_command_no_args_returns_usage() { + fn move_command_no_args_routes_to_timmy() { let tmp = tempfile::TempDir::new().unwrap(); - let output = move_cmd_with_root(tmp.path(), "").unwrap(); + let result = move_cmd_with_root(tmp.path(), ""); assert!( - output.contains("Usage"), - "no args should show usage hint: {output}" + result.is_none(), + "no args should route to LLM (None): {result:?}" + ); + } + + #[test] + fn move_command_natural_language_args_routes_to_timmy() { + let tmp = tempfile::TempDir::new().unwrap(); + let result = move_cmd_with_root(tmp.path(), "the auth story to done"); + assert!( + result.is_none(), + "natural-language args should route to LLM (None): {result:?}" ); } @@ -128,7 +140,7 @@ mod tests { let output = move_cmd_with_root(tmp.path(), "42").unwrap(); assert!( output.contains("Usage"), - "missing stage should show usage hint: {output}" + "numeric number without stage should show usage hint: {output}" ); } @@ -143,12 +155,12 @@ mod tests { } #[test] - fn move_command_non_numeric_number_returns_error() { + fn move_command_non_numeric_number_routes_to_timmy() { let tmp = tempfile::TempDir::new().unwrap(); - let output = move_cmd_with_root(tmp.path(), "abc current").unwrap(); + let result = move_cmd_with_root(tmp.path(), "abc current"); assert!( - output.contains("Invalid story number"), - "non-numeric number should return error: {output}" + result.is_none(), + "non-numeric story number should route to LLM (None): {result:?}" ); } diff --git a/server/src/chat/commands/overview.rs b/server/src/chat/commands/overview.rs index 477ed13f..0afcf8a0 100644 --- a/server/src/chat/commands/overview.rs +++ b/server/src/chat/commands/overview.rs @@ -11,17 +11,8 @@ use super::CommandContext; #[allow(clippy::string_slice)] // commit_hash is hex (ASCII), min(8) always within bounds pub(super) fn handle_overview(ctx: &CommandContext) -> Option { let num_str = ctx.args.trim(); - if num_str.is_empty() { - return Some(format!( - "Usage: `{} overview `\n\nShows the implementation summary for a story.", - ctx.services.bot_name - )); - } - if !num_str.chars().all(|c| c.is_ascii_digit()) { - return Some(format!( - "Invalid story number: `{num_str}`. Usage: `{} overview `", - ctx.services.bot_name - )); + if num_str.is_empty() || !num_str.chars().all(|c| c.is_ascii_digit()) { + return None; } let commit_hash = match find_story_merge_commit(ctx.effective_root(), num_str) { @@ -232,22 +223,34 @@ mod tests { } #[test] - fn overview_command_no_args_returns_usage() { + fn overview_command_no_args_routes_to_timmy() { let tmp = tempfile::TempDir::new().unwrap(); - let output = overview_cmd_with_root(tmp.path(), "").unwrap(); + let result = overview_cmd_with_root(tmp.path(), ""); assert!( - output.contains("Usage"), - "no args should show usage hint: {output}" + result.is_none(), + "no args should route to LLM (None): {result:?}" ); } #[test] - fn overview_command_non_numeric_arg_returns_error() { + fn overview_command_non_numeric_arg_routes_to_timmy() { let tmp = tempfile::TempDir::new().unwrap(); - let output = overview_cmd_with_root(tmp.path(), "abc").unwrap(); + let result = overview_cmd_with_root(tmp.path(), "of the auth refactor"); assert!( - output.contains("Invalid"), - "non-numeric arg should return error: {output}" + result.is_none(), + "natural-language args should route to LLM (None): {result:?}" + ); + } + + #[test] + fn overview_command_well_formed_runs_handler() { + let repo_root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")) + .parent() + .unwrap_or(std::path::Path::new(".")); + let result = overview_cmd_with_root(repo_root, "99999"); + assert!( + result.is_some(), + "well-formed numeric arg should run the command handler: {result:?}" ); } diff --git a/server/src/chat/commands/show.rs b/server/src/chat/commands/show.rs index 67f4f52d..90a79f8f 100644 --- a/server/src/chat/commands/show.rs +++ b/server/src/chat/commands/show.rs @@ -69,17 +69,8 @@ fn strip_front_matter(text: &str) -> (String, String) { /// Returns a friendly message when no match is found. pub(super) fn handle_show(ctx: &CommandContext) -> Option { let num_str = ctx.args.trim(); - if num_str.is_empty() { - return Some(format!( - "Usage: `{} show `\n\nDisplays the full text of a story, bug, or spike.", - ctx.services.bot_name - )); - } - if !num_str.chars().all(|c| c.is_ascii_digit()) { - return Some(format!( - "Invalid story number: `{num_str}`. Usage: `{} show `", - ctx.services.bot_name - )); + if num_str.is_empty() || !num_str.chars().all(|c| c.is_ascii_digit()) { + return None; } // Find the story by numeric prefix: CRDT → content store. @@ -169,22 +160,32 @@ mod tests { } #[test] - fn show_command_no_args_returns_usage() { + fn show_command_no_args_routes_to_timmy() { let tmp = tempfile::TempDir::new().unwrap(); - let output = show_cmd_with_root(tmp.path(), "").unwrap(); + let result = show_cmd_with_root(tmp.path(), ""); assert!( - output.contains("Usage"), - "no args should show usage hint: {output}" + result.is_none(), + "no args should route to LLM (None), not return a usage error: {result:?}" ); } #[test] - fn show_command_non_numeric_args_returns_error() { + fn show_command_non_numeric_args_routes_to_timmy() { let tmp = tempfile::TempDir::new().unwrap(); - let output = show_cmd_with_root(tmp.path(), "abc").unwrap(); + let result = show_cmd_with_root(tmp.path(), "me the story about the login bug"); assert!( - output.contains("Invalid"), - "non-numeric arg should return error message: {output}" + result.is_none(), + "natural-language args should route to LLM (None): {result:?}" + ); + } + + #[test] + fn show_command_well_formed_runs_handler() { + let tmp = tempfile::TempDir::new().unwrap(); + let result = show_cmd_with_root(tmp.path(), "999"); + assert!( + result.is_some(), + "well-formed numeric arg should run the command handler: {result:?}" ); } diff --git a/server/src/chat/commands/triage.rs b/server/src/chat/commands/triage.rs index 52a6b124..e746ec5b 100644 --- a/server/src/chat/commands/triage.rs +++ b/server/src/chat/commands/triage.rs @@ -18,17 +18,8 @@ use std::process::Command; /// Handle `{bot_name} status {number}`. pub(super) fn handle_triage(ctx: &CommandContext) -> Option { let num_str = ctx.args.trim(); - if num_str.is_empty() { - return Some(format!( - "Usage: `{} status `\n\nShows pipeline info for a story: stage, ACs, git diff, recent commits.", - ctx.services.bot_name - )); - } - if !num_str.chars().all(|c| c.is_ascii_digit()) { - return Some(format!( - "Invalid story number: `{num_str}`. Usage: `{} status `", - ctx.services.bot_name - )); + if num_str.is_empty() || !num_str.chars().all(|c| c.is_ascii_digit()) { + return None; } match find_story_by_number(num_str) { @@ -276,12 +267,23 @@ mod tests { } #[test] - fn whatsup_non_numeric_returns_error() { + fn whatsup_non_numeric_routes_to_timmy() { let tmp = tempfile::TempDir::new().unwrap(); - let output = status_triage_cmd(tmp.path(), "abc").unwrap(); + let result = status_triage_cmd(tmp.path(), "what is going on"); assert!( - output.contains("Invalid"), - "non-numeric arg should return error: {output}" + result.is_none(), + "natural-language args should route to LLM (None): {result:?}" + ); + } + + #[test] + fn whatsup_well_formed_runs_handler() { + crate::db::ensure_content_store(); + let tmp = tempfile::TempDir::new().unwrap(); + let result = status_triage_cmd(tmp.path(), "99996"); + assert!( + result.is_some(), + "well-formed numeric arg should run the command handler: {result:?}" ); } diff --git a/server/src/chat/commands/unblock.rs b/server/src/chat/commands/unblock.rs index f62157fa..183226be 100644 --- a/server/src/chat/commands/unblock.rs +++ b/server/src/chat/commands/unblock.rs @@ -16,10 +16,7 @@ pub(super) fn handle_unblock(ctx: &CommandContext) -> Option { let num_str = ctx.args.trim(); if num_str.is_empty() || !num_str.chars().all(|c| c.is_ascii_digit()) { - return Some(format!( - "Usage: `{} unblock ` (e.g. `unblock 42`)", - ctx.services.bot_name - )); + return None; } Some(unblock_by_number(ctx.effective_root(), num_str)) @@ -152,22 +149,32 @@ mod tests { } #[test] - fn unblock_command_no_args_returns_usage() { + fn unblock_command_no_args_routes_to_timmy() { let tmp = tempfile::TempDir::new().unwrap(); - let output = unblock_cmd_with_root(tmp.path(), "").unwrap(); + let result = unblock_cmd_with_root(tmp.path(), ""); assert!( - output.contains("Usage"), - "no args should show usage hint: {output}" + result.is_none(), + "no args should route to LLM (None), not return a usage error: {result:?}" ); } #[test] - fn unblock_command_non_numeric_returns_usage() { + fn unblock_command_non_numeric_routes_to_timmy() { let tmp = tempfile::TempDir::new().unwrap(); - let output = unblock_cmd_with_root(tmp.path(), "abc").unwrap(); + let result = unblock_cmd_with_root(tmp.path(), "to fix the blocking issue"); assert!( - output.contains("Usage"), - "non-numeric arg should show usage hint: {output}" + result.is_none(), + "natural-language args should route to LLM (None): {result:?}" + ); + } + + #[test] + fn unblock_command_well_formed_runs_handler() { + let tmp = tempfile::TempDir::new().unwrap(); + let result = unblock_cmd_with_root(tmp.path(), "666"); + assert!( + result.is_some(), + "well-formed numeric arg should run the command handler: {result:?}" ); } diff --git a/server/src/chat/transport/matrix/bot/messages/on_room_message.rs b/server/src/chat/transport/matrix/bot/messages/on_room_message.rs index 9e8c1197..e9574d7e 100644 --- a/server/src/chat/transport/matrix/bot/messages/on_room_message.rs +++ b/server/src/chat/transport/matrix/bot/messages/on_room_message.rs @@ -272,35 +272,26 @@ pub(in crate::chat::transport::matrix::bot) async fn on_room_message( // Check for the assign command, which requires async agent ops (stop + // start) and cannot be handled by the sync command registry. - if let Some(assign_cmd) = super::super::super::assign::extract_assign_command( + // Only handle the well-formed variant; BadArgs falls through to the LLM. + if let Some(super::super::super::assign::AssignCommand::Assign { + story_number, + model, + }) = super::super::super::assign::extract_assign_command( &user_message, &ctx.services.bot_name, ctx.matrix_user_id.as_str(), ) { - let response = match assign_cmd { - super::super::super::assign::AssignCommand::Assign { - story_number, - model, - } => { - slog!( - "[matrix-bot] Handling assign command from {sender}: story {story_number} model={model}" - ); - super::super::super::assign::handle_assign( - &ctx.services.bot_name, - &story_number, - &model, - &effective_root, - &ctx.services.agents, - ) - .await - } - super::super::super::assign::AssignCommand::BadArgs => { - format!( - "Usage: `{} assign ` (e.g. `assign 42 opus`)", - ctx.services.bot_name - ) - } - }; + slog!( + "[matrix-bot] Handling assign command from {sender}: story {story_number} model={model}" + ); + let response = super::super::super::assign::handle_assign( + &ctx.services.bot_name, + &story_number, + &model, + &effective_root, + &ctx.services.agents, + ) + .await; let html = markdown_to_html(&response); if let Ok(msg_id) = ctx .transport @@ -346,26 +337,22 @@ pub(in crate::chat::transport::matrix::bot) async fn on_room_message( // Check for the delete command, which requires async agent/worktree ops // and cannot be handled by the sync command registry. - if let Some(del_cmd) = super::super::super::delete::extract_delete_command( - &user_message, - &ctx.services.bot_name, - ctx.matrix_user_id.as_str(), - ) { - let response = match del_cmd { - super::super::super::delete::DeleteCommand::Delete { story_number } => { - slog!("[matrix-bot] Handling delete command from {sender}: story {story_number}"); - super::super::super::delete::handle_delete( - &ctx.services.bot_name, - &story_number, - &effective_root, - &ctx.services.agents, - ) - .await - } - super::super::super::delete::DeleteCommand::BadArgs => { - format!("Usage: `{} delete `", ctx.services.bot_name) - } - }; + // Only handle the well-formed variant; BadArgs falls through to the LLM. + if let Some(super::super::super::delete::DeleteCommand::Delete { story_number }) = + super::super::super::delete::extract_delete_command( + &user_message, + &ctx.services.bot_name, + ctx.matrix_user_id.as_str(), + ) + { + slog!("[matrix-bot] Handling delete command from {sender}: story {story_number}"); + let response = super::super::super::delete::handle_delete( + &ctx.services.bot_name, + &story_number, + &effective_root, + &ctx.services.agents, + ) + .await; let html = markdown_to_html(&response); if let Ok(msg_id) = ctx .transport @@ -380,26 +367,22 @@ pub(in crate::chat::transport::matrix::bot) async fn on_room_message( // Check for the rmtree command, which requires async agent/worktree ops // and cannot be handled by the sync command registry. - if let Some(rmtree_cmd) = super::super::super::rmtree::extract_rmtree_command( - &user_message, - &ctx.services.bot_name, - ctx.matrix_user_id.as_str(), - ) { - let response = match rmtree_cmd { - super::super::super::rmtree::RmtreeCommand::Rmtree { story_number } => { - slog!("[matrix-bot] Handling rmtree command from {sender}: story {story_number}"); - super::super::super::rmtree::handle_rmtree( - &ctx.services.bot_name, - &story_number, - &effective_root, - &ctx.services.agents, - ) - .await - } - super::super::super::rmtree::RmtreeCommand::BadArgs => { - format!("Usage: `{} rmtree `", ctx.services.bot_name) - } - }; + // Only handle the well-formed variant; BadArgs falls through to the LLM. + if let Some(super::super::super::rmtree::RmtreeCommand::Rmtree { story_number }) = + super::super::super::rmtree::extract_rmtree_command( + &user_message, + &ctx.services.bot_name, + ctx.matrix_user_id.as_str(), + ) + { + slog!("[matrix-bot] Handling rmtree command from {sender}: story {story_number}"); + let response = super::super::super::rmtree::handle_rmtree( + &ctx.services.bot_name, + &story_number, + &effective_root, + &ctx.services.agents, + ) + .await; let html = markdown_to_html(&response); if let Ok(msg_id) = ctx .transport @@ -414,35 +397,26 @@ pub(in crate::chat::transport::matrix::bot) async fn on_room_message( // Check for the start command, which requires async agent ops and cannot // be handled by the sync command registry. - if let Some(start_cmd) = super::super::super::start::extract_start_command( + // Only handle the well-formed variant; BadArgs falls through to the LLM. + if let Some(super::super::super::start::StartCommand::Start { + story_number, + agent_hint, + }) = super::super::super::start::extract_start_command( &user_message, &ctx.services.bot_name, ctx.matrix_user_id.as_str(), ) { - let response = match start_cmd { - super::super::super::start::StartCommand::Start { - story_number, - agent_hint, - } => { - slog!( - "[matrix-bot] Handling start command from {sender}: story {story_number} agent={agent_hint:?}" - ); - super::super::super::start::handle_start( - &ctx.services.bot_name, - &story_number, - agent_hint.as_deref(), - &effective_root, - &ctx.services.agents, - ) - .await - } - super::super::super::start::StartCommand::BadArgs => { - format!( - "Usage: `{} start ` or `{} start opus`", - ctx.services.bot_name, ctx.services.bot_name - ) - } - }; + slog!( + "[matrix-bot] Handling start command from {sender}: story {story_number} agent={agent_hint:?}" + ); + let response = super::super::super::start::handle_start( + &ctx.services.bot_name, + &story_number, + agent_hint.as_deref(), + &effective_root, + &ctx.services.agents, + ) + .await; let html = markdown_to_html(&response); if let Ok(msg_id) = ctx .transport