use std::path::{Path, PathBuf};
use std::sync::OnceLock;
use regex::Regex;
use crate::collect::ai_attribution::{provenance_possibly_stripped, AgenticMode};
use crate::collect::ai_marker_config::{
marker_file_path, MarkerConfig, MarkerConfigError, MarkerScope,
};
#[derive(Debug, Clone, Copy)]
pub struct CommitSignals<'a> {
pub message: &'a str,
pub author_email: &'a str,
pub committer_email: &'a str,
}
impl<'a> CommitSignals<'a> {
pub fn from_message(message: &'a str) -> Self {
Self {
message,
author_email: "",
committer_email: "",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Detection {
pub tool: Option<&'static str>,
pub mode: AgenticMode,
}
pub const DETECTOR_VERSION: i64 = 1;
pub fn detect(signals: &CommitSignals<'_>) -> Detection {
detect_in(marker_set(), signals)
}
fn detect_in(set: &MarkerSet, signals: &CommitSignals<'_>) -> Detection {
let trailers: Vec<&str> = trailer_line()
.captures_iter(signals.message)
.filter_map(|c| c.get(1).map(|m| m.as_str()))
.collect();
let builtin = scan(&set.markers[..set.builtin_len], signals, &trailers);
if builtin.mode != AgenticMode::None {
return builtin;
}
let operator = scan(&set.markers[set.builtin_len..], signals, &trailers);
if operator.mode != AgenticMode::None {
return operator;
}
if provenance_possibly_stripped(signals.message) {
return Detection {
tool: None,
mode: AgenticMode::Unknown,
};
}
Detection {
tool: None,
mode: AgenticMode::None,
}
}
fn scan(markers: &[AiMarker], signals: &CommitSignals<'_>, trailers: &[&str]) -> Detection {
let mut ide: Option<&'static str> = None;
for marker in markers {
if !marker.matches(signals, trailers) {
continue;
}
match marker.mode {
AgenticMode::FullAgentic => {
return Detection {
tool: Some(marker.tool),
mode: AgenticMode::FullAgentic,
};
}
AgenticMode::IdeAssisted => {
if ide.is_none() {
ide = Some(marker.tool);
}
}
AgenticMode::None | AgenticMode::Unknown => {}
}
}
match ide {
Some(tool) => Detection {
tool: Some(tool),
mode: AgenticMode::IdeAssisted,
},
None => Detection {
tool: None,
mode: AgenticMode::None,
},
}
}
pub fn detection_disclosure() -> String {
disclosure_for(marker_set())
}
fn disclosure_for(set: &MarkerSet) -> String {
let mut tools: Vec<&str> = Vec::new();
for m in &set.markers {
if !tools.contains(&m.tool) {
tools.push(m.tool);
}
}
let operator = match &set.source {
MarkerSource::Absent(path) => format!(
"no operator marker file at {} (set {} to add markers without a code change)",
path.display(),
crate::collect::ai_marker_config::ENV_AI_MARKERS
),
MarkerSource::Loaded { path, count } => {
format!("{count} operator marker(s) loaded from {}", path.display())
}
MarkerSource::Failed { path, error } => format!(
"operator marker file {} was REJECTED and none of it applied ({error}) — \
builtin markers only",
path.display()
),
};
format!(
"agentic detection: {} builtin marker(s) + {}; active for [{}]; detection is \
marker-based only — commits whose trailers or footers were stripped, squashed, or \
rewritten are indistinguishable from human commits, so a low agentic share means \
\"no markers emitted\", not \"no AI assistance\"",
BUILTIN.len(),
operator,
tools.join(", ")
)
}
#[derive(Debug)]
struct AiMarker {
tool: &'static str,
mode: AgenticMode,
scope: MarkerScope,
pattern: Regex,
}
impl AiMarker {
fn matches(&self, signals: &CommitSignals<'_>, trailers: &[&str]) -> bool {
match self.scope {
MarkerScope::Trailer => trailers.iter().any(|v| self.pattern.is_match(v)),
MarkerScope::Message => self.pattern.is_match(signals.message),
MarkerScope::Email => {
self.pattern.is_match(signals.author_email)
|| self.pattern.is_match(signals.committer_email)
}
}
}
}
struct BuiltinSpec {
tool: &'static str,
mode: AgenticMode,
scope: MarkerScope,
pattern: &'static str,
}
const BUILTIN: &[BuiltinSpec] = &[
BuiltinSpec {
tool: "claude",
mode: AgenticMode::FullAgentic,
scope: MarkerScope::Trailer,
pattern: r"(?i)\bclaude\b",
},
BuiltinSpec {
tool: "claude",
mode: AgenticMode::FullAgentic,
scope: MarkerScope::Message,
pattern: r"(?i)Generated\s+with\s+\[?Claude\s+Code",
},
BuiltinSpec {
tool: "claude",
mode: AgenticMode::FullAgentic,
scope: MarkerScope::Message,
pattern: r"(?im)^X-AI-Tokens-(?:In|Out):\s*\d",
},
BuiltinSpec {
tool: "claude",
mode: AgenticMode::FullAgentic,
scope: MarkerScope::Message,
pattern: r"(?im)^X-AI-Model:\s*\S",
},
BuiltinSpec {
tool: "trusty-mpm",
mode: AgenticMode::FullAgentic,
scope: MarkerScope::Message,
pattern: r"(?i)Generated\s+with\s+\[?trusty-mpm",
},
BuiltinSpec {
tool: "devin",
mode: AgenticMode::FullAgentic,
scope: MarkerScope::Trailer,
pattern: r"(?i)\bdevin-ai-integration\b|@devin\.ai\b",
},
BuiltinSpec {
tool: "devin",
mode: AgenticMode::FullAgentic,
scope: MarkerScope::Email,
pattern: r"(?i)^devin-ai-integration(\[bot\])?@",
},
BuiltinSpec {
tool: "openhands",
mode: AgenticMode::FullAgentic,
scope: MarkerScope::Trailer,
pattern: r"(?i)\bopenhands@all-hands\.dev\b|\bopenhands[-_]?release-bot\b|\bopenhands\s+bot\b",
},
BuiltinSpec {
tool: "openhands",
mode: AgenticMode::FullAgentic,
scope: MarkerScope::Email,
pattern: r"(?i)^openhands@all-hands\.dev$",
},
BuiltinSpec {
tool: "aider",
mode: AgenticMode::FullAgentic,
scope: MarkerScope::Trailer,
pattern: r"(?i)\baider\b",
},
BuiltinSpec {
tool: "copilot",
mode: AgenticMode::IdeAssisted,
scope: MarkerScope::Trailer,
pattern: r"(?i)\bcopilot\b|GitHub\s+Copilot",
},
BuiltinSpec {
tool: "cursor",
mode: AgenticMode::IdeAssisted,
scope: MarkerScope::Trailer,
pattern: r"(?i)@cursor\.sh|\bcursor\b($|[^-\w])",
},
];
fn trailer_line() -> &'static Regex {
static RE: OnceLock<Regex> = OnceLock::new();
RE.get_or_init(|| {
Regex::new(r"(?im)^[Cc]o-[Aa]uthored-[Bb]y:\s*(.+)$")
.expect("trailer_line pattern compiles")
})
}
struct MarkerSet {
markers: Vec<AiMarker>,
builtin_len: usize,
source: MarkerSource,
}
enum MarkerSource {
Absent(PathBuf),
Loaded { path: PathBuf, count: usize },
Failed { path: PathBuf, error: String },
}
fn marker_set() -> &'static MarkerSet {
static SET: OnceLock<MarkerSet> = OnceLock::new();
SET.get_or_init(|| build_marker_set(&marker_file_path()))
}
fn build_marker_set(path: &Path) -> MarkerSet {
let markers: Vec<AiMarker> = BUILTIN
.iter()
.map(|s| AiMarker {
tool: s.tool,
mode: s.mode,
scope: s.scope,
pattern: Regex::new(s.pattern).expect("builtin marker pattern compiles"),
})
.collect();
let builtin_len = markers.len();
let mut markers = markers;
if !path.exists() {
return MarkerSet {
markers,
builtin_len,
source: MarkerSource::Absent(path.to_path_buf()),
};
}
let source = match MarkerConfig::load_from(path).and_then(|cfg| compile_operator(&cfg)) {
Ok(operator) => {
let count = operator.len();
markers.extend(operator);
MarkerSource::Loaded {
path: path.to_path_buf(),
count,
}
}
Err(error) => {
tracing::warn!(
path = %path.display(),
%error,
"operator agentic-marker file rejected; continuing with the builtin markers"
);
MarkerSource::Failed {
path: path.to_path_buf(),
error: error.to_string(),
}
}
};
MarkerSet {
markers,
builtin_len,
source,
}
}
fn compile_operator(cfg: &MarkerConfig) -> Result<Vec<AiMarker>, MarkerConfigError> {
cfg.markers
.iter()
.enumerate()
.map(|(index, spec)| {
if spec.tool.trim().is_empty() {
return Err(MarkerConfigError::EmptyTool { index });
}
let pattern =
Regex::new(&spec.pattern).map_err(|source| MarkerConfigError::Pattern {
index,
tool: spec.tool.clone(),
pattern: spec.pattern.clone(),
source,
})?;
Ok(AiMarker {
tool: Box::leak(spec.tool.clone().into_boxed_str()),
mode: spec.mode.as_agentic_mode(),
scope: spec.scope,
pattern,
})
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
fn mode_of(msg: &str) -> AgenticMode {
detect(&CommitSignals::from_message(msg)).mode
}
#[test]
fn every_builtin_pattern_compiles() {
let set = build_marker_set(Path::new("/definitely/not/a/marker/file.yaml"));
assert_eq!(set.markers.len(), BUILTIN.len());
}
#[test]
fn detects_trusty_mpm_footer() {
let msg = "feat: add thing\n\n🤖🤖🤖 Generated with trusty-mpm — \
https://github.com/bobmatnyc/trusty-tools";
let d = detect(&CommitSignals::from_message(msg));
assert_eq!(d.mode, AgenticMode::FullAgentic);
assert_eq!(d.tool, Some("trusty-mpm"));
let linked = "fix: y\n\n🤖🤖🤖 Generated with \
[trusty-mpm](https://github.com/bobmatnyc/trusty-tools)";
assert_eq!(mode_of(linked), AgenticMode::FullAgentic);
let linked_claude = "fix: z\n\n🤖 Generated with \
[Claude Code](https://claude.com/claude-code)";
assert_eq!(mode_of(linked_claude), AgenticMode::FullAgentic);
}
#[test]
fn detects_openhands_coauthor() {
let msg = "Fix runtime startup\n\n\
Co-authored-by: openhands <openhands@all-hands.dev>";
let d = detect(&CommitSignals::from_message(msg));
assert_eq!(d.mode, AgenticMode::FullAgentic);
assert_eq!(d.tool, Some("openhands"));
}
#[test]
fn detects_devin_and_aider() {
assert_eq!(
mode_of(
"chore: bump\n\nCo-authored-by: Devin AI \
<devin-ai-integration[bot]@users.noreply.github.com>"
),
AgenticMode::FullAgentic
);
assert_eq!(
mode_of("refactor: extract\n\nCo-authored-by: aider (gpt-4o) <noreply@aider.chat>"),
AgenticMode::FullAgentic
);
}
#[test]
fn human_named_devin_is_not_agentic() {
let msg = "feat: scoring\n\nCo-authored-by: Devin Booker <devin@personal.example>";
let d = detect(&CommitSignals::from_message(msg));
assert_eq!(d.mode, AgenticMode::None, "a first name is not a marker");
assert_eq!(d.tool, None);
assert_eq!(
mode_of("fix: x\n\nCo-authored-by: Devin <devin@devin.ai>"),
AgenticMode::FullAgentic
);
}
#[test]
fn email_scope_matches_author_or_committer() {
let by_author = CommitSignals {
message: "fix: something",
author_email: "openhands@all-hands.dev",
committer_email: "human@example.com",
};
assert_eq!(detect(&by_author).mode, AgenticMode::FullAgentic);
let by_committer = CommitSignals {
message: "fix: something",
author_email: "human@example.com",
committer_email: "devin-ai-integration[bot]@users.noreply.github.com",
};
assert_eq!(detect(&by_committer).mode, AgenticMode::FullAgentic);
}
#[test]
fn vendor_domain_alone_is_not_agentic() {
let human = CommitSignals {
message: "fix: something",
author_email: "engineer@all-hands.dev",
committer_email: "engineer@all-hands.dev",
};
assert_eq!(detect(&human).mode, AgenticMode::None);
}
#[test]
fn full_agentic_wins_over_ide_assisted() {
let msg = "pair: fix auth\n\n\
Co-Authored-By: Cursor <noreply@cursor.sh>\n\
Co-Authored-By: Claude Opus <noreply@anthropic.com>";
let d = detect(&CommitSignals::from_message(msg));
assert_eq!(d.mode, AgenticMode::FullAgentic);
assert_eq!(d.tool, Some("claude"));
}
#[test]
fn trailer_scope_does_not_match_body_prose() {
assert_eq!(
mode_of("fix: rewrite the openhands adapter, no trailer here"),
AgenticMode::None
);
assert_eq!(
mode_of("fix: x\n\nCo-authored-by: openhands <openhands@all-hands.dev>"),
AgenticMode::FullAgentic
);
}
#[test]
fn merge_summary_is_unknown_not_none() {
let msg = "Merge branch 'feat/x' into main";
let by_message = detect(&CommitSignals::from_message(msg));
assert_eq!(by_message.mode, AgenticMode::Unknown);
assert_eq!(by_message.tool, None);
let with_identities = detect(&CommitSignals {
message: msg,
author_email: "engineer@example.com",
committer_email: "noreply@github.com",
});
assert_eq!(with_identities, by_message);
assert_eq!(mode_of("feat: add button"), AgenticMode::None);
}
#[test]
fn disclosure_names_active_tools() {
let d = detection_disclosure();
assert!(d.contains("claude"), "{d}");
assert!(d.contains("trusty-mpm"), "{d}");
assert!(d.contains("no markers emitted"), "{d}");
}
fn write_marker_file(dir: &tempfile::TempDir, body: &str) -> PathBuf {
let path = dir.path().join("ai-markers.yaml");
std::fs::write(&path, body).expect("writes fixture");
path
}
#[test]
fn operator_markers_extend_the_builtins() {
let dir = tempfile::tempdir().expect("tempdir");
let path = write_marker_file(
&dir,
"markers:\n\
\x20 - tool: acme-copilot\n\
\x20 mode: full_agentic\n\
\x20 scope: message\n\
\x20 pattern: '(?i)Produced\\s+by\\s+ACME\\s+Autopilot'\n",
);
let set = build_marker_set(&path);
assert_eq!(set.markers.len(), BUILTIN.len() + 1);
let signals = CommitSignals::from_message("feat: x\n\nProduced by ACME Autopilot v3");
let d = detect_in(&set, &signals);
assert_eq!(d.mode, AgenticMode::FullAgentic);
assert_eq!(d.tool, Some("acme-copilot"));
let disclosure = disclosure_for(&set);
assert!(
disclosure.contains("1 operator marker(s) loaded"),
"{disclosure}"
);
assert!(disclosure.contains("acme-copilot"), "{disclosure}");
}
#[test]
fn operator_markers_never_relabel_a_builtin_match() {
let dir = tempfile::tempdir().expect("tempdir");
let path = write_marker_file(
&dir,
"markers:\n - { tool: house, mode: full_agentic, scope: message, pattern: 'feat' }\n",
);
let set = build_marker_set(&path);
let msg = "feat: add thing\n\n🤖🤖🤖 Generated with trusty-mpm";
let d = detect_in(&set, &CommitSignals::from_message(msg));
assert_eq!(d.tool, Some("trusty-mpm"), "builtin keeps the label");
}
#[test]
fn operator_full_agentic_cannot_upgrade_a_builtin_ide_match() {
let dir = tempfile::tempdir().expect("tempdir");
let path = write_marker_file(
&dir,
"markers:\n - { tool: house-full, mode: full_agentic, scope: message, pattern: 'copilot' }\n",
);
let set = build_marker_set(&path);
let msg = "feat: autocomplete\n\nCo-Authored-By: GitHub Copilot <copilot@github.com>";
let d = detect_in(&set, &CommitSignals::from_message(msg));
assert_eq!(d.tool, Some("copilot"), "the builtin verdict must survive");
assert_eq!(d.mode, AgenticMode::IdeAssisted, "and must not be upgraded");
}
#[test]
fn operator_ide_marker_loses_to_builtin_full_agentic() {
let dir = tempfile::tempdir().expect("tempdir");
let path = write_marker_file(
&dir,
"markers:\n - { tool: house-ide, mode: ide_assisted, scope: message, pattern: 'refactor' }\n",
);
let set = build_marker_set(&path);
let msg = "refactor: split\n\nCo-Authored-By: Claude <noreply@anthropic.com>";
let d = detect_in(&set, &CommitSignals::from_message(msg));
assert_eq!(d.mode, AgenticMode::FullAgentic);
assert_eq!(d.tool, Some("claude"));
}
#[test]
fn operator_marker_still_applies_to_a_provenance_stripped_subject() {
let msg = "Merge branch 'feat/x' into main";
assert!(
provenance_possibly_stripped(msg),
"precondition: the subject must look stripped, else this proves nothing"
);
let dir = tempfile::tempdir().expect("tempdir");
let path = write_marker_file(
&dir,
"markers:\n - { tool: house-bot, mode: full_agentic, scope: message, pattern: \"Merge branch\" }\n",
);
let set = build_marker_set(&path);
let d = detect_in(&set, &CommitSignals::from_message(msg));
assert_eq!(
d.mode,
AgenticMode::FullAgentic,
"the operator marker must be consulted, not pre-empted by Unknown"
);
assert_eq!(d.tool, Some("house-bot"));
}
#[test]
fn provenance_stripped_subject_is_unknown_when_no_marker_matches() {
let dir = tempfile::tempdir().expect("tempdir");
let path = write_marker_file(
&dir,
"markers:\n - { tool: house-bot, mode: full_agentic, scope: message, pattern: 'nothing-matches-this' }\n",
);
let set = build_marker_set(&path);
let d = detect_in(
&set,
&CommitSignals::from_message("Merge branch 'feat/x' into main"),
);
assert_eq!(d.mode, AgenticMode::Unknown);
assert_eq!(d.tool, None);
}
#[test]
fn a_bad_pattern_rejects_the_whole_file() {
let dir = tempfile::tempdir().expect("tempdir");
let path = write_marker_file(
&dir,
"markers:\n\
\x20 - { tool: good, mode: full_agentic, scope: message, pattern: 'Acme Bot' }\n\
\x20 - { tool: broken, mode: full_agentic, scope: message, pattern: '([unclosed' }\n",
);
let set = build_marker_set(&path);
assert_eq!(set.markers.len(), BUILTIN.len(), "no partial application");
assert_eq!(
detect_in(
&set,
&CommitSignals::from_message("x\n\nGenerated with trusty-mpm")
)
.mode,
AgenticMode::FullAgentic
);
assert_eq!(
detect_in(&set, &CommitSignals::from_message("chore: Acme Bot ran")).mode,
AgenticMode::None
);
let disclosure = disclosure_for(&set);
assert!(disclosure.contains("REJECTED"), "{disclosure}");
assert!(
disclosure.contains("marker 1 (tool `broken`)"),
"{disclosure}"
);
}
#[test]
fn disclosure_reports_a_rejected_marker_file() {
let dir = tempfile::tempdir().expect("tempdir");
let malformed = write_marker_file(&dir, "markers: [ this is not: valid yaml\n");
let set = build_marker_set(&malformed);
assert_eq!(set.markers.len(), BUILTIN.len());
assert!(disclosure_for(&set).contains("REJECTED"));
let blank_tool = dir.path().join("blank.yaml");
std::fs::write(
&blank_tool,
"markers:\n - { tool: ' ', mode: full_agentic, scope: message, pattern: 'x' }\n",
)
.expect("writes fixture");
let set = build_marker_set(&blank_tool);
assert_eq!(set.markers.len(), BUILTIN.len());
let disclosure = disclosure_for(&set);
assert!(disclosure.contains("empty `tool` label"), "{disclosure}");
}
#[test]
fn an_absent_marker_file_is_not_a_failure() {
let set = build_marker_set(Path::new("/definitely/not/here/ai-markers.yaml"));
let disclosure = disclosure_for(&set);
assert!(
disclosure.contains("no operator marker file at"),
"{disclosure}"
);
assert!(disclosure.contains("TGA_AI_MARKERS"), "{disclosure}");
assert!(!disclosure.contains("REJECTED"), "{disclosure}");
}
#[test]
fn openhands_trailers_from_a_real_clone() {
let set = build_marker_set(Path::new("/definitely/not/here/ai-markers.yaml"));
for trailer in [
"Co-authored-by: openhands <openhands@all-hands.dev>",
"Co-authored-by: OpenHands CVE Fix Bot <openhands@all-hands.dev>",
"Co-authored-by: openhands-release-bot[bot] \
<290150379+openhands-release-bot[bot]@users.noreply.github.com>",
"Co-authored-by: OpenHands Bot <contact@all-hands.dev>",
] {
let msg = format!("fix: something\n\n{trailer}");
let d = detect_in(&set, &CommitSignals::from_message(&msg));
assert_eq!(d.mode, AgenticMode::FullAgentic, "{trailer}");
assert_eq!(d.tool, Some("openhands"), "{trailer}");
}
for trailer in [
"Co-authored-by: Simon Rosenberg <simon@openhands.dev>",
"Co-authored-by: aivong-openhands <ai.vong@openhands.dev>",
] {
let msg = format!("fix: something\n\n{trailer}");
let d = detect_in(&set, &CommitSignals::from_message(&msg));
assert_eq!(d.mode, AgenticMode::None, "{trailer}");
}
}
}