Skip to content
80 linesCodeBlameRaw

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

Coding agents show as themselves on commits and contributors: Claude, Codex, Copilot, Gemini, Jules, Cursor, Devin, Aider, Dependabot, Renovate and GitHub Actions are known by the address they write on their commits (never by name), shown with their maker's mark, their own name and an agent or bot label, and a card saying what each is. Anyone else not on g1t gets a card too, saying so and how to claim the commits by adding the address. Contributor lists are worked out again (stats version 2) so an agent counts once.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}