Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 2 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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: <email>
Expand Down Expand Up @@ -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.
55 changes: 13 additions & 42 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(&current_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));
Expand All @@ -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);
}
}

Expand Down Expand Up @@ -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!(
Expand Down
Loading