use crate::collect::ai_markers::{detect, CommitSignals};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum AgenticMode {
FullAgentic,
IdeAssisted,
None,
}
impl AgenticMode {
pub fn as_str(self) -> &'static str {
match self {
AgenticMode::FullAgentic => "full_agentic",
AgenticMode::IdeAssisted => "ide_assisted",
AgenticMode::None => "none",
}
}
}
impl std::str::FromStr for AgenticMode {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"full_agentic" => Ok(AgenticMode::FullAgentic),
"ide_assisted" => Ok(AgenticMode::IdeAssisted),
"none" => Ok(AgenticMode::None),
_ => Err(()),
}
}
}
pub fn detect_ai_tool(message: &str) -> Option<&'static str> {
detect(&CommitSignals::from_message(message)).tool
}
pub fn detect_agentic_mode(message: &str) -> AgenticMode {
detect(&CommitSignals::from_message(message)).mode
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn detect_trusty_mpm_footer_is_full_agentic() {
let msg = "docs: add website link to README (#5330)\n\n\
🤖🤖🤖 Generated with trusty-mpm — https://github.com/bobmatnyc/trusty-tools";
assert_eq!(detect_agentic_mode(msg), AgenticMode::FullAgentic);
assert_eq!(detect_ai_tool(msg), Some("trusty-mpm"));
}
#[test]
fn detect_ai_tool_detects_claude() {
let msg =
"feat: add auth\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>";
assert_eq!(detect_ai_tool(msg), Some("claude"));
}
#[test]
fn detect_ai_tool_case_insensitive_key() {
let msg = "fix: bug\n\nco-authored-by: Claude Sonnet 4 <noreply@anthropic.com>";
assert_eq!(detect_ai_tool(msg), Some("claude"));
}
#[test]
fn detect_ai_tool_detects_copilot() {
let msg = "feat: autocomplete\n\nCo-Authored-By: GitHub Copilot <copilot@github.com>";
assert_eq!(detect_ai_tool(msg), Some("copilot"));
}
#[test]
fn detect_ai_tool_detects_copilot_bare() {
let msg = "fix: npe\n\nCo-Authored-By: copilot <noreply@github.com>";
assert_eq!(detect_ai_tool(msg), Some("copilot"));
}
#[test]
fn detect_ai_tool_detects_cursor() {
let msg = "chore: refactor\n\nCo-Authored-By: Cursor <noreply@cursor.sh>";
assert_eq!(detect_ai_tool(msg), Some("cursor"));
}
#[test]
fn detect_ai_tool_returns_none_for_human() {
let msg = "feat: auth\n\nCo-Authored-By: Alice Smith <alice@example.com>";
assert_eq!(detect_ai_tool(msg), None);
}
#[test]
fn detect_ai_tool_returns_none_for_no_trailer() {
assert_eq!(detect_ai_tool("feat: add feature"), None);
assert_eq!(detect_ai_tool(""), None);
}
#[test]
fn detect_ai_tool_priority_claude_before_copilot() {
let msg = "pair session\n\n\
Co-Authored-By: Claude Opus <noreply@anthropic.com>\n\
Co-Authored-By: GitHub Copilot <copilot@github.com>";
assert_eq!(detect_ai_tool(msg), Some("claude"));
}
#[test]
fn detect_ai_tool_priority_copilot_before_cursor() {
let msg = "pair session\n\n\
Co-Authored-By: GitHub Copilot <copilot@github.com>\n\
Co-Authored-By: Cursor <noreply@cursor.sh>";
assert_eq!(detect_ai_tool(msg), Some("copilot"));
}
#[test]
fn agentic_mode_as_str() {
assert_eq!(AgenticMode::FullAgentic.as_str(), "full_agentic");
assert_eq!(AgenticMode::IdeAssisted.as_str(), "ide_assisted");
assert_eq!(AgenticMode::None.as_str(), "none");
}
#[test]
fn agentic_mode_from_str_round_trips() {
use std::str::FromStr;
assert_eq!(
AgenticMode::from_str("full_agentic"),
Ok(AgenticMode::FullAgentic)
);
assert_eq!(
AgenticMode::from_str("ide_assisted"),
Ok(AgenticMode::IdeAssisted)
);
assert_eq!(AgenticMode::from_str("none"), Ok(AgenticMode::None));
assert!(AgenticMode::from_str("unknown_value").is_err());
assert!(AgenticMode::from_str("").is_err());
}
#[test]
fn detect_agentic_mode_claude_coauthor_is_full_agentic() {
let msg = "feat: add feature\n\n\
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>";
assert_eq!(detect_agentic_mode(msg), AgenticMode::FullAgentic);
}
#[test]
fn detect_agentic_mode_generated_with_claude_code_is_full_agentic() {
let msg = "fix: resolve timeout\n\n\
🤖 Generated with [Claude Code](https://claude.ai/claude-code)\n\
Co-Authored-By: Claude Sonnet 4 <noreply@anthropic.com>";
assert_eq!(detect_agentic_mode(msg), AgenticMode::FullAgentic);
}
#[test]
fn detect_agentic_mode_generated_body_only_is_full_agentic() {
let msg = "chore: update deps\n\nGenerated with Claude Code";
assert_eq!(detect_agentic_mode(msg), AgenticMode::FullAgentic);
}
#[test]
fn detect_agentic_mode_x_ai_tokens_is_full_agentic() {
let msg = "feat: implement search\n\n\
X-AI-Tokens-In: 1234\n\
X-AI-Tokens-Out: 5678";
assert_eq!(detect_agentic_mode(msg), AgenticMode::FullAgentic);
}
#[test]
fn detect_agentic_mode_x_ai_model_is_full_agentic() {
let msg = "refactor: extract helper\n\nX-AI-Model: claude-sonnet-4-6";
assert_eq!(detect_agentic_mode(msg), AgenticMode::FullAgentic);
}
#[test]
fn detect_agentic_mode_cursor_is_ide_assisted() {
let msg = "fix: null check\n\nCo-Authored-By: Cursor <noreply@cursor.sh>";
assert_eq!(detect_agentic_mode(msg), AgenticMode::IdeAssisted);
}
#[test]
fn detect_agentic_mode_copilot_is_ide_assisted() {
let msg = "feat: autocomplete\n\nCo-Authored-By: GitHub Copilot <copilot@github.com>";
assert_eq!(detect_agentic_mode(msg), AgenticMode::IdeAssisted);
}
#[test]
fn detect_agentic_mode_copilot_bare_is_ide_assisted() {
let msg = "fix: npe\n\nCo-Authored-By: copilot <noreply@github.com>";
assert_eq!(detect_agentic_mode(msg), AgenticMode::IdeAssisted);
}
#[test]
fn detect_agentic_mode_plain_commit_is_none() {
assert_eq!(detect_agentic_mode("feat: add button"), AgenticMode::None);
assert_eq!(detect_agentic_mode(""), AgenticMode::None);
}
#[test]
fn detect_agentic_mode_human_coauthor_is_none() {
let msg = "feat: pair program\n\nCo-Authored-By: Alice Smith <alice@example.com>";
assert_eq!(detect_agentic_mode(msg), AgenticMode::None);
}
#[test]
fn detect_agentic_mode_cursor_in_human_name_is_not_ide_assisted() {
let msg = "feat: auth\n\nCo-Authored-By: Alice Cursor-Williams <alice@example.com>";
assert_eq!(detect_agentic_mode(msg), AgenticMode::None);
assert_eq!(detect_ai_tool(msg), None);
}
#[test]
fn is_cursor_match_email_domain_form() {
let msg = "fix: npe\n\nCo-Authored-By: AI Bot <ai@cursor.sh>";
assert_eq!(detect_agentic_mode(msg), AgenticMode::IdeAssisted);
assert_eq!(detect_ai_tool(msg), Some("cursor"));
}
#[test]
fn detect_agentic_mode_claude_wins_over_cursor() {
let msg = "pair: fix auth\n\n\
Co-Authored-By: Cursor <noreply@cursor.sh>\n\
Co-Authored-By: Claude Opus <noreply@anthropic.com>";
assert_eq!(detect_agentic_mode(msg), AgenticMode::FullAgentic);
}
}