From 1203fd343c0e1335b989b65898f7d3618bad4060 Mon Sep 17 00:00:00 2001 From: Alec Thomas Date: Mon, 11 May 2026 10:14:01 +1000 Subject: [PATCH] feat: attribute only the first detected agent --- README.md | 6 ++---- src/main.rs | 55 +++++++++++++---------------------------------------- 2 files changed, 15 insertions(+), 46 deletions(-) diff --git a/README.md b/README.md index 575b903..a7c401f 100644 --- a/README.md +++ b/README.md @@ -9,9 +9,7 @@ It finds agents in four ways: 3. It walks up the process tree and checks all descendants of siblings at each level, looking for agents working in the same repository. 4. It checks agent-specific state files ("breadcrumbs") to determine if an agent was recently active in this repo (e.g. `~/.claude/projects/`, `~/.codex/sessions/`, `~/.pi/agent/sessions/`). -Multiple agents can be attributed in a single commit. Results are deduplicated by email address. - -If any agents are found, it will append the following git trailer to the git commit: +At most one agent is attributed per commit: the first one found, in the order above. If an agent is found, it will append the following git trailer to the git commit: ``` Co-authored-by: @@ -65,4 +63,4 @@ ln -s /usr/local/bin/aittributor .git/hooks/prepare-commit-msg **Agent-initiated commits are the most reliable.** Attribution is most accurate when the agent itself runs `git commit`. Manual commits while an agent session is open (or recently closed) are the main source of attribution that may not reflect actual code contribution. -**Duplicate trailers when multiple writers are active.** Aittributor deduplicates by email address against both its own detected agents and any `Co-authored-by` trailers already in the commit message. However, if another process writes a trailer *after* aittributor runs, duplicates with different display names may appear. +**Only one agent is attributed per commit.** When several agents are detected (e.g. a different agent is running elsewhere in the process tree, or a breadcrumb is found for another agent), only the first match is recorded. Aittributor still skips adding its trailer if a `Co-authored-by` for that email is already present in the commit message. diff --git a/src/main.rs b/src/main.rs index 101d857..a51c443 100644 --- a/src/main.rs +++ b/src/main.rs @@ -172,24 +172,13 @@ fn detect_agents(debug: bool) -> Vec<&'static Agent> { agents } -fn dedup_agents(agents: Vec<&'static Agent>) -> Vec<&'static Agent> { - let mut seen = std::collections::HashSet::new(); - agents - .into_iter() - .filter(|a| { - let addr = Agent::extract_email_addr(a.email); - seen.insert(addr) - }) - .collect() -} - fn breadcrumb_fallback(debug: bool) -> Vec<&'static Agent> { let current_dir = std::env::current_dir().unwrap_or_default(); let repo_path = find_git_root(¤t_dir).unwrap_or(current_dir); breadcrumbs::detect_agents_from_breadcrumbs(&repo_path, debug) } -fn detect_and_merge(debug: bool) -> Vec<&'static Agent> { +fn first_detected_agent(debug: bool) -> Option<&'static Agent> { let (bc_tx, bc_rx) = mpsc::channel(); std::thread::spawn(move || { let _ = bc_tx.send(breadcrumb_fallback(debug)); @@ -201,27 +190,27 @@ fn detect_and_merge(debug: bool) -> Vec<&'static Agent> { agents.extend(bc_agents); } - dedup_agents(agents) + agents.into_iter().next() } fn run(cli: Cli) { - let agents = detect_and_merge(cli.debug); + let agent = first_detected_agent(cli.debug); let Some(commit_msg_file) = cli.commit_msg_file else { - if agents.is_empty() { - eprintln!("No agent found"); - std::process::exit(1); - } - for agent in &agents { - println!("{}", agent.email); + match agent { + Some(a) => println!("{}", a.email), + None => { + eprintln!("No agent found"); + std::process::exit(1); + } } return; }; - for agent in &agents { - if let Err(e) = append_trailers(&commit_msg_file, agent, cli.debug) { - eprintln!("aittributor: failed to append trailers: {}", e); - } + if let Some(agent) = agent + && let Err(e) = append_trailers(&commit_msg_file, agent, cli.debug) + { + eprintln!("aittributor: failed to append trailers: {}", e); } } @@ -292,24 +281,6 @@ mod tests { ); } - #[test] - fn test_dedup_agents_removes_duplicates() { - let claude = Agent::find_by_name("claude").unwrap(); - let amp = Agent::find_by_name("amp").unwrap(); - let agents = vec![claude, amp, claude]; - let deduped = dedup_agents(agents); - assert_eq!(deduped.len(), 2); - assert_eq!(deduped[0].email, claude.email); - assert_eq!(deduped[1].email, amp.email); - } - - #[test] - fn test_dedup_agents_empty() { - let agents: Vec<&'static Agent> = vec![]; - let deduped = dedup_agents(agents); - assert!(deduped.is_empty()); - } - #[test] fn test_extract_email_addr() { assert_eq!(