diff --git a/server/src/http/mcp.rs b/server/src/http/mcp.rs index adb6b6e..731c7fc 100644 --- a/server/src/http/mcp.rs +++ b/server/src/http/mcp.rs @@ -1788,6 +1788,13 @@ fn tool_report_merge_failure(args: &Value, ctx: &AppContext) -> Result for Option { }), WatcherEvent::ConfigChanged => Some(WsResponse::AgentConfigChanged), WatcherEvent::AgentStateChanged => Some(WsResponse::AgentStateChanged), + // MergeFailure is handled by the Matrix notification listener only; + // no WebSocket message is needed for the frontend. + WatcherEvent::MergeFailure { .. } => None, } } } diff --git a/server/src/io/watcher.rs b/server/src/io/watcher.rs index 649463e..231a0b3 100644 --- a/server/src/io/watcher.rs +++ b/server/src/io/watcher.rs @@ -50,6 +50,14 @@ pub enum WatcherEvent { /// Triggers a pipeline state refresh so the frontend can update agent /// assignments without waiting for a filesystem event. AgentStateChanged, + /// A story encountered a failure (e.g. merge failure). + /// Triggers an error notification to configured Matrix rooms. + MergeFailure { + /// Work item ID (e.g. `"42_story_my_feature"`). + story_id: String, + /// Human-readable description of the failure. + reason: String, + }, } /// Return `true` if `path` is the root-level `.story_kit/project.toml`, i.e. diff --git a/server/src/matrix/notifications.rs b/server/src/matrix/notifications.rs index 90820f9..4ec2170 100644 --- a/server/src/matrix/notifications.rs +++ b/server/src/matrix/notifications.rs @@ -81,6 +81,24 @@ pub fn format_stage_notification( (plain, html) } +/// Format an error notification message for a story failure. +/// +/// Returns `(plain_text, html)` suitable for `RoomMessageEventContent::text_html`. +pub fn format_error_notification( + item_id: &str, + story_name: Option<&str>, + reason: &str, +) -> (String, String) { + let number = extract_story_number(item_id).unwrap_or(item_id); + let name = story_name.unwrap_or(item_id); + + let plain = format!("\u{274c} #{number} {name} \u{2014} {reason}"); + let html = format!( + "\u{274c} #{number} {name} \u{2014} {reason}" + ); + (plain, html) +} + /// Spawn a background task that listens for watcher events and posts /// stage-transition notifications to all configured Matrix rooms. pub fn spawn_notification_listener( @@ -126,6 +144,32 @@ pub fn spawn_notification_listener( } } } + Ok(WatcherEvent::MergeFailure { + ref story_id, + ref reason, + }) => { + let story_name = + read_story_name(&project_root, "4_merge", story_id); + let (plain, html) = format_error_notification( + story_id, + story_name.as_deref(), + reason, + ); + + slog!("[matrix-bot] Sending error notification: {plain}"); + + for room_id in &room_ids { + if let Some(room) = client.get_room(room_id) { + let content = + RoomMessageEventContent::text_html(plain.clone(), html.clone()); + if let Err(e) = room.send(content).await { + slog!( + "[matrix-bot] Failed to send error notification to {room_id}: {e}" + ); + } + } + } + } Ok(_) => {} // Ignore non-work-item events Err(broadcast::error::RecvError::Lagged(n)) => { slog!( @@ -246,6 +290,42 @@ mod tests { assert_eq!(name, None); } + // ── format_error_notification ──────────────────────────────────────────── + + #[test] + fn format_error_notification_with_story_name() { + let (plain, html) = + format_error_notification("262_story_bot_errors", Some("Bot error notifications"), "merge conflict in src/main.rs"); + assert_eq!( + plain, + "\u{274c} #262 Bot error notifications \u{2014} merge conflict in src/main.rs" + ); + assert_eq!( + html, + "\u{274c} #262 Bot error notifications \u{2014} merge conflict in src/main.rs" + ); + } + + #[test] + fn format_error_notification_without_story_name_falls_back_to_item_id() { + let (plain, _html) = + format_error_notification("42_bug_fix_thing", None, "tests failed"); + assert_eq!( + plain, + "\u{274c} #42 42_bug_fix_thing \u{2014} tests failed" + ); + } + + #[test] + fn format_error_notification_non_numeric_id_uses_full_id() { + let (plain, _html) = + format_error_notification("abc_story_thing", Some("Some Story"), "clippy errors"); + assert_eq!( + plain, + "\u{274c} #abc_story_thing Some Story \u{2014} clippy errors" + ); + } + // ── format_stage_notification ─────────────────────────────────────────── #[test]