Skip to content
80 linesCodeBlameRaw
1//! Coding agents and bots that commit under their own names (Claude Code,
2//! Codex, Copilot, Dependabot…), matched by the address on their commits.
3//! The same ids, addresses and logins as `packages/contracts/src/coding-agents.ts`,
4//! which also holds their names and marks; a test keeps the two equal.
5
6/// One agent: its id, the addresses it commits with, and the GitHub logins
7/// whose noreply addresses are its. All lowercase.
8pub struct CodingAgent {
9 pub id: &'static str,
10 pub emails: &'static [&'static str],
11 pub logins: &'static [&'static str],
12}
13
14pub const CODING_AGENTS: &[CodingAgent] = &[
15 CodingAgent { id: "claude", emails: &["noreply@anthropic.com"], logins: &["claude", "claude[bot]"] },
16 CodingAgent { id: "codex", emails: &[], logins: &["chatgpt-codex-connector[bot]", "codex[bot]"] },
17 CodingAgent { id: "copilot", emails: &[], logins: &["copilot", "copilot-swe-agent[bot]", "copilot-pull-request-reviewer[bot]"] },
18 CodingAgent { id: "gemini", emails: &[], logins: &["gemini-code-assist[bot]", "gemini-cli[bot]"] },
19 CodingAgent { id: "jules", emails: &[], logins: &["google-labs-jules[bot]"] },
20 CodingAgent { id: "cursor", emails: &["cursoragent@cursor.com"], logins: &["cursor[bot]", "cursor-agent[bot]"] },
21 CodingAgent { id: "devin", emails: &[], logins: &["devin-ai-integration[bot]"] },
22 CodingAgent { id: "aider", emails: &["noreply@aider.chat", "aider@aider.chat"], logins: &[] },
23 CodingAgent { id: "dependabot", emails: &["support@dependabot.com"], logins: &["dependabot[bot]", "dependabot-preview[bot]"] },
24 CodingAgent { id: "renovate", emails: &["bot@renovateapp.com"], logins: &["renovate[bot]"] },
25 CodingAgent { id: "github-actions", emails: &["action@github.com"], logins: &["github-actions[bot]"] },
26];
27
28/// The login a GitHub noreply address names (`123+login@users.noreply.github.com`).
29fn github_login(address: &str) -> Option<&str> {
30 let local = address.strip_suffix("@users.noreply.github.com")?;
31 Some(local.split_once('+').map_or(local, |(_, login)| login))
32}
33
34/// The id of the coding agent a commit's address belongs to.
35pub fn coding_agent_for(email: &str) -> Option<&'static str> {
36 let address = email.trim().to_ascii_lowercase();
37 if address.is_empty() {
38 return None;
39 }
40 let login = github_login(&address);
41 CODING_AGENTS
42 .iter()
43 .find(|agent| agent.emails.contains(&address.as_str()) || login.is_some_and(|login| agent.logins.contains(&login)))
44 .map(|agent| agent.id)
45}
46
47#[cfg(test)]
48mod tests {
49 use super::*;
50
51 #[test]
52 fn agents_are_found_by_their_commit_addresses() {
53 assert_eq!(coding_agent_for("noreply@anthropic.com"), Some("claude"));
54 assert_eq!(coding_agent_for(" NoReply@Anthropic.com "), Some("claude"));
55 assert_eq!(coding_agent_for("49699333+dependabot[bot]@users.noreply.github.com"), Some("dependabot"));
56 assert_eq!(coding_agent_for("198982749+Copilot@users.noreply.github.com"), Some("copilot"));
57 assert_eq!(coding_agent_for("ada@example.com"), None);
58 assert_eq!(coding_agent_for("1+ada@users.noreply.github.com"), None);
59 assert_eq!(coding_agent_for(""), None);
60 }
61
62 /// The site's list (packages/contracts/src/coding-agents.ts) has every
63 /// agent here, with the same addresses and logins, and no others.
64 #[test]
65 fn the_typescript_list_is_the_same() {
66 let ts = include_str!("../../../packages/contracts/src/coding-agents.ts");
67 let ids: Vec<&str> = ts.match_indices(" id: \"").map(|(at, m)| &ts[at + m.len()..]).map(|rest| &rest[..rest.find('"').unwrap()]).collect();
68 assert_eq!(ids, CODING_AGENTS.iter().map(|a| a.id).collect::<Vec<_>>());
69 for agent in CODING_AGENTS {
70 let start = ts.find(&format!(" id: \"{}\"", agent.id)).unwrap();
71 let block = &ts[start..ts[start..].find("\n },").map(|end| start + end).unwrap()];
72 let list = |key: &str| -> Vec<String> {
73 let line = block.lines().find(|l| l.trim_start().starts_with(&format!("{key}:"))).unwrap();
74 line.split('"').skip(1).step_by(2).map(str::to_owned).collect()
75 };
76 assert_eq!(list("emails"), agent.emails.iter().map(|s| s.to_string()).collect::<Vec<_>>(), "{} emails", agent.id);
77 assert_eq!(list("logins"), agent.logins.iter().map(|s| s.to_string()).collect::<Vec<_>>(), "{} logins", agent.id);
78 }
79 }
80}