Skip to content

Commit 6e5d421

Browse files
baldawarishiclaude
andauthored
refactor: fold BreadcrumbSource into KNOWN_AGENTS (#4)
## Summary - Moves `breadcrumb_dir` and `breadcrumb_ext` fields directly into the `Agent` struct, eliminating the separate `BreadcrumbSource` struct and `SOURCES` constant - Removes the `find_agent()` helper that matched agents by email prefix — no longer needed since we iterate `KNOWN_AGENTS` directly - Makes it easier to add breadcrumb support for new agents by keeping all agent metadata in one place - Simplifies the breadcrumb fallback section in README Addresses #3 (comment) ## Test plan - [x] `cargo test` — all 16 tests pass - [x] `./scripts/aittributor --debug` — breadcrumb fallback still works correctly 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent e8d5ca4 commit 6e5d421

File tree

4 files changed

+47
-53
lines changed

4 files changed

+47
-53
lines changed

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

README.md

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -60,9 +60,4 @@ ln -s /usr/local/bin/aittributor .git/hooks/prepare-commit-msg
6060

6161
## Breadcrumb fallback
6262

63-
The commit hook relies on detecting a running AI agent process. If the agent exits before you commit, the hook won't find it. As a fallback, aittributor checks agent-specific state files ("breadcrumbs") left behind by agents:
64-
65-
- **Claude Code**: checks `~/.claude/projects/` for recent session files matching the current repo
66-
- **Codex**: checks `~/.codex/sessions/` for recent session files whose `cwd` is within the current git root (monorepo subdirectories match; sibling repo names like `repo` and `repo2` do not)
67-
68-
Files modified within the last 2 hours are considered recent. No additional setup is required — these directories are created automatically by the agents themselves.
63+
If the AI agent exits before you commit, aittributor falls back to checking agent-specific state files to detect recently active agents. This only works when state files are available. No additional setup is required.

src/agent.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,63 +4,87 @@ pub struct Agent {
44
pub process_names: &'static [&'static str],
55
pub env_vars: &'static [(&'static str, &'static str)],
66
pub email: &'static str,
7+
pub breadcrumb_dir: Option<&'static str>,
8+
pub breadcrumb_ext: Option<&'static str>,
79
}
810

911
pub const KNOWN_AGENTS: &[Agent] = &[
1012
Agent {
1113
process_names: &["claude"],
1214
env_vars: &[],
1315
email: "Claude Code <noreply@anthropic.com>",
16+
breadcrumb_dir: Some(".claude/projects"),
17+
breadcrumb_ext: Some("jsonl"),
1418
},
1519
Agent {
1620
process_names: &["goose"],
1721
env_vars: &[],
1822
email: "Goose <opensource@block.xyz>",
23+
breadcrumb_dir: None,
24+
breadcrumb_ext: None,
1925
},
2026
Agent {
2127
process_names: &["cursor", "cursor-agent"],
2228
env_vars: &[],
2329
email: "Cursor <noreply@cursor.com>",
30+
breadcrumb_dir: None,
31+
breadcrumb_ext: None,
2432
},
2533
Agent {
2634
process_names: &["aider"],
2735
env_vars: &[],
2836
email: "Aider <noreply@aider.chat>",
37+
breadcrumb_dir: None,
38+
breadcrumb_ext: None,
2939
},
3040
Agent {
3141
process_names: &["windsurf"],
3242
env_vars: &[],
3343
email: "Windsurf <noreply@codeium.com>",
44+
breadcrumb_dir: None,
45+
breadcrumb_ext: None,
3446
},
3547
Agent {
3648
process_names: &["codex"],
3749
env_vars: &[],
3850
email: "Codex <noreply@openai.com>",
51+
breadcrumb_dir: Some(".codex/sessions"),
52+
breadcrumb_ext: Some("jsonl"),
3953
},
4054
Agent {
4155
process_names: &["copilot-agent"],
4256
env_vars: &[],
4357
email: "GitHub Copilot <noreply@github.com>",
58+
breadcrumb_dir: None,
59+
breadcrumb_ext: None,
4460
},
4561
Agent {
4662
process_names: &["amazon-q", "q"],
4763
env_vars: &[],
4864
email: "Amazon Q Developer <noreply@amazon.com>",
65+
breadcrumb_dir: None,
66+
breadcrumb_ext: None,
4967
},
5068
Agent {
5169
process_names: &["amp"],
5270
env_vars: &[],
5371
email: "Amp <amp@ampcode.com>",
72+
breadcrumb_dir: None,
73+
breadcrumb_ext: None,
5474
},
5575
Agent {
5676
process_names: &[],
5777
env_vars: &[("CLINE_ACTIVE", "true")],
5878
email: "Cline <noreply@cline.bot>",
79+
breadcrumb_dir: None,
80+
breadcrumb_ext: None,
5981
},
6082
Agent {
6183
process_names: &["gemini"],
6284
env_vars: &[],
6385
email: "Gemini CLI Agent <gemini-cli-agent@google.com>",
86+
breadcrumb_dir: None,
87+
breadcrumb_ext: None,
6488
},
6589
];
6690

src/breadcrumbs.rs

Lines changed: 21 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -10,28 +10,6 @@ const CUTOFF_SECS: u64 = 2 * 60 * 60; // 2 hours as a rough approximation
1010
/// Maximum number of lines to read from a session file when looking for "cwd".
1111
const MAX_LINES_TO_SCAN: usize = 5;
1212

13-
struct BreadcrumbSource {
14-
/// Prefix to match against Agent.email in KNOWN_AGENTS
15-
email_prefix: &'static str,
16-
/// Base directory relative to $HOME (e.g. ".claude/projects")
17-
base_dir: &'static str,
18-
/// File extension to look for (without dot)
19-
file_ext: &'static str,
20-
}
21-
22-
const SOURCES: &[BreadcrumbSource] = &[
23-
BreadcrumbSource {
24-
email_prefix: "Claude Code",
25-
base_dir: ".claude/projects",
26-
file_ext: "jsonl",
27-
},
28-
BreadcrumbSource {
29-
email_prefix: "Codex",
30-
base_dir: ".codex/sessions",
31-
file_ext: "jsonl",
32-
},
33-
];
34-
3513
fn home_dir() -> Option<String> {
3614
std::env::var("HOME").ok()
3715
}
@@ -46,10 +24,6 @@ fn has_extension(path: &Path, ext: &str) -> bool {
4624
path.extension().and_then(|e| e.to_str()) == Some(ext)
4725
}
4826

49-
fn find_agent(email_prefix: &str) -> Option<&'static Agent> {
50-
KNOWN_AGENTS.iter().find(|a| a.email.starts_with(email_prefix))
51-
}
52-
5327
fn extract_cwd_from_json(line: &str) -> Option<&str> {
5428
// Simple string extraction: find "cwd":"<value>"
5529
let marker = "\"cwd\":\"";
@@ -116,36 +90,37 @@ fn find_session_file_with_cwd(dir: &Path, ext: &str, repo_path: &Path, cutoff: S
11690
false
11791
}
11892

119-
fn check_source(
120-
source: &BreadcrumbSource,
121-
repo_path: &Path,
122-
cutoff: SystemTime,
123-
debug: bool,
124-
) -> Option<&'static Agent> {
125-
let home = home_dir()?;
126-
let base = Path::new(&home).join(source.base_dir);
93+
fn check_source(agent: &'static Agent, repo_path: &Path, cutoff: SystemTime, debug: bool) -> bool {
94+
let breadcrumb_dir = match agent.breadcrumb_dir {
95+
Some(d) => d,
96+
None => return false,
97+
};
98+
let breadcrumb_ext = agent.breadcrumb_ext.unwrap_or("jsonl");
99+
100+
let home = match home_dir() {
101+
Some(h) => h,
102+
None => return false,
103+
};
104+
let base = Path::new(&home).join(breadcrumb_dir);
127105

128106
if debug {
129-
eprintln!(" {} breadcrumb dir: {}", source.email_prefix, base.display());
107+
eprintln!(" {} breadcrumb dir: {}", agent.email, base.display());
130108
}
131109

132110
if !base.is_dir() {
133111
if debug {
134112
eprintln!(" Not found");
135113
}
136-
return None;
114+
return false;
137115
}
138116

139-
let matched = find_session_file_with_cwd(&base, source.file_ext, repo_path, cutoff, debug);
117+
let matched = find_session_file_with_cwd(&base, breadcrumb_ext, repo_path, cutoff, debug);
140118

141-
if matched {
142-
find_agent(source.email_prefix)
143-
} else {
144-
if debug {
145-
eprintln!(" No match for {}", source.email_prefix);
146-
}
147-
None
119+
if !matched && debug {
120+
eprintln!(" No match for {}", agent.email);
148121
}
122+
123+
matched
149124
}
150125

151126
pub fn detect_agents_from_breadcrumbs(repo_path: &Path, debug: bool) -> Vec<&'static Agent> {
@@ -156,8 +131,8 @@ pub fn detect_agents_from_breadcrumbs(repo_path: &Path, debug: bool) -> Vec<&'st
156131
eprintln!("\n=== Breadcrumb Fallback ===");
157132
}
158133

159-
for source in SOURCES {
160-
if let Some(agent) = check_source(source, repo_path, cutoff, debug) {
134+
for agent in KNOWN_AGENTS {
135+
if check_source(agent, repo_path, cutoff, debug) {
161136
agents.push(agent);
162137
}
163138
}

0 commit comments

Comments
 (0)