huskies: merge 1073
This commit is contained in:
@@ -38,6 +38,14 @@ pub(crate) fn tool_create_bug(args: &Value, ctx: &AppContext) -> Result<String,
|
||||
depends_on.as_deref(),
|
||||
)?;
|
||||
|
||||
let _ = ctx
|
||||
.watcher_tx
|
||||
.send(crate::io::watcher::WatcherEvent::NewItemCreated {
|
||||
item_id: bug_id.clone(),
|
||||
item_type: "bug".to_string(),
|
||||
name: req.name.as_ref().to_string(),
|
||||
});
|
||||
|
||||
Ok(format!("Created bug: {bug_id}"))
|
||||
}
|
||||
|
||||
|
||||
@@ -36,6 +36,14 @@ pub(crate) fn tool_create_refactor(args: &Value, ctx: &AppContext) -> Result<Str
|
||||
depends_on.as_deref(),
|
||||
)?;
|
||||
|
||||
let _ = ctx
|
||||
.watcher_tx
|
||||
.send(crate::io::watcher::WatcherEvent::NewItemCreated {
|
||||
item_id: refactor_id.clone(),
|
||||
item_type: "refactor".to_string(),
|
||||
name: req.name.as_ref().to_string(),
|
||||
});
|
||||
|
||||
Ok(format!("Created refactor: {refactor_id}"))
|
||||
}
|
||||
|
||||
|
||||
@@ -36,6 +36,14 @@ pub(crate) fn tool_create_spike(args: &Value, ctx: &AppContext) -> Result<String
|
||||
depends_on.as_deref(),
|
||||
)?;
|
||||
|
||||
let _ = ctx
|
||||
.watcher_tx
|
||||
.send(crate::io::watcher::WatcherEvent::NewItemCreated {
|
||||
item_id: spike_id.clone(),
|
||||
item_type: "spike".to_string(),
|
||||
name: req.name.as_ref().to_string(),
|
||||
});
|
||||
|
||||
Ok(format!("Created spike: {spike_id}"))
|
||||
}
|
||||
|
||||
|
||||
@@ -31,6 +31,14 @@ pub(crate) fn tool_create_story(args: &Value, ctx: &AppContext) -> Result<String
|
||||
false,
|
||||
)?;
|
||||
|
||||
let _ = ctx
|
||||
.watcher_tx
|
||||
.send(crate::io::watcher::WatcherEvent::NewItemCreated {
|
||||
item_id: story_id.clone(),
|
||||
item_type: "story".to_string(),
|
||||
name: req.name.as_ref().to_string(),
|
||||
});
|
||||
|
||||
// Bug 503: warn at creation time if any depends_on points at an already-archived story.
|
||||
let archived_deps: Vec<u32> = depends_on_ids
|
||||
.as_deref()
|
||||
|
||||
@@ -90,4 +90,14 @@ pub enum WatcherEvent {
|
||||
/// `true` if acceptance gates passed; `false` if they failed.
|
||||
success: bool,
|
||||
},
|
||||
/// A new work item was successfully created and added to the backlog.
|
||||
/// Triggers a creation notification to configured chat rooms.
|
||||
NewItemCreated {
|
||||
/// Work item ID (e.g. `"1075_refactor_split_stage_enum"`).
|
||||
item_id: String,
|
||||
/// Human-readable item type (`"story"`, `"bug"`, `"refactor"`, `"spike"`).
|
||||
item_type: String,
|
||||
/// Human-readable item name.
|
||||
name: String,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -26,6 +26,8 @@ pub enum EventAction {
|
||||
/// `true` if acceptance gates passed.
|
||||
success: bool,
|
||||
},
|
||||
/// Post a new-item-created notification.
|
||||
NewItemCreated,
|
||||
/// Log server-side only; do not post to chat (e.g. hard rate-limit blocks).
|
||||
LogOnly,
|
||||
/// Reload the project configuration.
|
||||
@@ -51,6 +53,7 @@ pub fn classify(event: &WatcherEvent) -> EventAction {
|
||||
WatcherEvent::AgentCompleted { success, .. } => {
|
||||
EventAction::AgentCompleted { success: *success }
|
||||
}
|
||||
WatcherEvent::NewItemCreated { .. } => EventAction::NewItemCreated,
|
||||
_ => EventAction::Skip,
|
||||
}
|
||||
}
|
||||
@@ -178,4 +181,14 @@ mod tests {
|
||||
EventAction::AgentCompleted { success: false }
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_item_created_is_classified_correctly() {
|
||||
let event = WatcherEvent::NewItemCreated {
|
||||
item_id: "1075_refactor_split_stage".to_string(),
|
||||
item_type: "refactor".to_string(),
|
||||
name: "Split Stage enum".to_string(),
|
||||
};
|
||||
assert_eq!(classify(&event), EventAction::NewItemCreated);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -220,6 +220,26 @@ pub fn format_agent_completed_notification(
|
||||
(plain, html)
|
||||
}
|
||||
|
||||
/// Format a new-work-item creation notification.
|
||||
///
|
||||
/// Returns `(plain_text, html)` suitable for `ChatTransport::send_message`.
|
||||
pub fn format_new_item_notification(
|
||||
item_id: &str,
|
||||
item_type: &str,
|
||||
name: &str,
|
||||
) -> (String, String) {
|
||||
let number = extract_item_number(item_id).unwrap_or(item_id);
|
||||
let emoji = match item_type {
|
||||
"bug" => "\u{1f41b}", // 🐛
|
||||
"refactor" => "\u{1f4dd}", // 📝
|
||||
"spike" => "\u{1f52c}", // 🔬
|
||||
_ => "\u{1f4d6}", // 📖 (story and unknown)
|
||||
};
|
||||
let plain = format!("{emoji} New {item_type} #{number} \u{2014} {name}");
|
||||
let html = format!("{emoji} New {item_type} <strong>#{number}</strong> \u{2014} {name}");
|
||||
(plain, html)
|
||||
}
|
||||
|
||||
/// Extract the first non-empty line from a merge failure reason, truncated to `max_len` chars.
|
||||
///
|
||||
/// Used to produce a compact snippet for chat notifications.
|
||||
@@ -599,6 +619,67 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
// ── format_new_item_notification ──────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn format_new_item_notification_story() {
|
||||
let (plain, html) =
|
||||
format_new_item_notification("42_story_my_feature", "story", "My Feature");
|
||||
assert_eq!(plain, "\u{1f4d6} New story #42 \u{2014} My Feature");
|
||||
assert_eq!(
|
||||
html,
|
||||
"\u{1f4d6} New story <strong>#42</strong> \u{2014} My Feature"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_new_item_notification_bug() {
|
||||
let (plain, html) =
|
||||
format_new_item_notification("99_bug_login_crash", "bug", "Login Crash");
|
||||
assert_eq!(plain, "\u{1f41b} New bug #99 \u{2014} Login Crash");
|
||||
assert_eq!(
|
||||
html,
|
||||
"\u{1f41b} New bug <strong>#99</strong> \u{2014} Login Crash"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_new_item_notification_refactor() {
|
||||
let (plain, html) = format_new_item_notification(
|
||||
"1075_refactor_split_stage",
|
||||
"refactor",
|
||||
"Split Stage enum into Pipeline + Status",
|
||||
);
|
||||
assert_eq!(
|
||||
plain,
|
||||
"\u{1f4dd} New refactor #1075 \u{2014} Split Stage enum into Pipeline + Status"
|
||||
);
|
||||
assert_eq!(
|
||||
html,
|
||||
"\u{1f4dd} New refactor <strong>#1075</strong> \u{2014} Split Stage enum into Pipeline + Status"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_new_item_notification_spike() {
|
||||
let (plain, html) =
|
||||
format_new_item_notification("7_spike_encoder_comparison", "spike", "Compare Encoders");
|
||||
assert_eq!(plain, "\u{1f52c} New spike #7 \u{2014} Compare Encoders");
|
||||
assert_eq!(
|
||||
html,
|
||||
"\u{1f52c} New spike <strong>#7</strong> \u{2014} Compare Encoders"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_new_item_notification_non_numeric_id_uses_full_id() {
|
||||
let (plain, _html) = format_new_item_notification("abc_story_thing", "story", "Some Story");
|
||||
assert_eq!(
|
||||
plain,
|
||||
"\u{1f4d6} New story #abc_story_thing \u{2014} Some Story"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_agent_completed_notification_empty_name_falls_back_to_number() {
|
||||
let (plain, _html) =
|
||||
|
||||
@@ -15,8 +15,9 @@ use super::super::events::classify;
|
||||
use super::super::filter::{AGENT_EVENT_DEBOUNCE, should_send_rate_limit};
|
||||
use super::super::format::{
|
||||
format_agent_completed_notification, format_agent_started_notification,
|
||||
format_blocked_notification, format_error_notification, format_oauth_account_swapped,
|
||||
format_oauth_accounts_exhausted, format_rate_limit_notification, merge_failure_snippet,
|
||||
format_blocked_notification, format_error_notification, format_new_item_notification,
|
||||
format_oauth_account_swapped, format_oauth_accounts_exhausted, format_rate_limit_notification,
|
||||
merge_failure_snippet,
|
||||
};
|
||||
use super::super::route::rooms_for_notification;
|
||||
use super::{find_story_name_any_stage, read_story_name};
|
||||
@@ -276,6 +277,26 @@ pub fn spawn_notification_listener(
|
||||
pending_agent_events.insert(key, (plain, html));
|
||||
agent_flush_deadline = Some(tokio::time::Instant::now() + AGENT_EVENT_DEBOUNCE);
|
||||
}
|
||||
EventAction::NewItemCreated => {
|
||||
if !config.status_push_enabled {
|
||||
continue;
|
||||
}
|
||||
let WatcherEvent::NewItemCreated {
|
||||
ref item_id,
|
||||
ref item_type,
|
||||
ref name,
|
||||
} = event
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let (plain, html) = format_new_item_notification(item_id, item_type, name);
|
||||
slog!("[bot] Sending new-item notification: {plain}");
|
||||
for room_id in &rooms_for_notification(&get_room_ids) {
|
||||
if let Err(e) = transport.send_message(room_id, &plain, &html).await {
|
||||
slog!("[bot] Failed to send new-item notification to {room_id}: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
EventAction::LogOnly => {
|
||||
// Hard-block: log server-side for debugging; do NOT post to chat.
|
||||
// Hard-block auto-resume is normal operation — the status command
|
||||
|
||||
@@ -37,6 +37,8 @@ pub fn watcher_event_to_response(e: WatcherEvent) -> Option<WsResponse> {
|
||||
// Agent lifecycle events are forwarded to chat transports only; no WebSocket message.
|
||||
WatcherEvent::AgentStarted { .. } => None,
|
||||
WatcherEvent::AgentCompleted { .. } => None,
|
||||
// Creation notifications are forwarded to chat transports only; no WebSocket message.
|
||||
WatcherEvent::NewItemCreated { .. } => None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user