-
Notifications
You must be signed in to change notification settings - Fork 3
feat: add AGENTS.md context injection to agentdiff configure #17
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,254 @@ | ||
| use crate::util::{dim, ok}; | ||
| use anyhow::Result; | ||
| use std::path::Path; | ||
|
|
||
| const AGENTS_MD_START: &str = "<!-- agentdiff: managed block — do not edit -->"; | ||
| const AGENTS_MD_END: &str = "<!-- end agentdiff -->"; | ||
|
|
||
| fn managed_block() -> String { | ||
| format!( | ||
| "{start}\n\ | ||
| ## AgentDiff\n\ | ||
| \n\ | ||
| [AgentDiff](https://github.com/codeprakhar25/agentdiff) tracks which AI agent \ | ||
| wrote which lines of code in this repository. Every file edit made through a \ | ||
| configured agent is captured and stored as a signed `AgentTrace` record in \ | ||
| `.git/agentdiff/traces/`. Attribution is computed per-commit and covers all \ | ||
| configured agents: Claude Code, Cursor, Codex, Copilot, Windsurf, OpenCode, \ | ||
| and Gemini.\n\ | ||
| \n\ | ||
| ### Before editing traced files\n\ | ||
| \n\ | ||
| Check attribution context to understand prior AI contributions:\n\ | ||
| \n\ | ||
| ```bash\n\ | ||
| agentdiff context path/to/file\n\ | ||
| agentdiff context path/to/file --json\n\ | ||
| ```\n\ | ||
| \n\ | ||
| ### Before committing\n\ | ||
| \n\ | ||
| Let the git hooks run — do **not** bypass them with `--no-verify`. The \ | ||
| `pre-commit` hook computes per-file attribution, and the `post-commit` hook \ | ||
| signs and stores the trace. Skipping either breaks the attribution ledger.\n\ | ||
| \n\ | ||
| ### When reviewing PRs\n\ | ||
| \n\ | ||
| ```bash\n\ | ||
| agentdiff report --format markdown # Aggregate attribution summary\n\ | ||
| agentdiff blame src/main.rs # Line-level attribution\n\ | ||
| agentdiff diff HEAD # Attribution changes in a commit\n\ | ||
| ```\n\ | ||
| \n\ | ||
| ### Attribution conventions\n\ | ||
| \n\ | ||
| - Files you edit without an AI agent are attributed to `human`\n\ | ||
| - Files changed by an agent use its name: `claude-code`, `cursor`, `codex`, etc.\n\ | ||
| - Copilot inline completions are tracked for stats but excluded from file attribution\n\ | ||
| - When multiple agents touch a file in one session, the majority-lines agent wins\n\ | ||
| {end}", | ||
| start = AGENTS_MD_START, | ||
| end = AGENTS_MD_END, | ||
| ) | ||
| } | ||
|
|
||
| pub fn step_configure_agents_md(repo_root: &Path) -> Result<()> { | ||
| // AGENTS.md is a per-project file. Skip when not inside a git repository | ||
| // (e.g. `agentdiff configure` run from a home directory for machine-global setup). | ||
| if !repo_root.join(".git").exists() { | ||
| println!( | ||
| "{} AGENTS.md skipped (not a git repository: {})", | ||
| dim(), | ||
| repo_root.display() | ||
| ); | ||
| return Ok(()); | ||
| } | ||
|
|
||
| let agents_md_path = repo_root.join("AGENTS.md"); | ||
| let block = managed_block(); | ||
|
|
||
| let existing = std::fs::read_to_string(&agents_md_path).unwrap_or_default(); | ||
|
|
||
| if let Some(start_pos) = existing.find(AGENTS_MD_START) { | ||
| if let Some(rel_end) = existing[start_pos..].find(AGENTS_MD_END) { | ||
| let end_pos = start_pos + rel_end + AGENTS_MD_END.len(); | ||
| let current_block = &existing[start_pos..end_pos]; | ||
| if current_block == block { | ||
| println!("{} AGENTS.md AgentDiff section already up-to-date", dim()); | ||
| return Ok(()); | ||
| } | ||
| // Update existing block in place. | ||
| let updated = format!("{}{}{}", &existing[..start_pos], block, &existing[end_pos..]); | ||
| std::fs::write(&agents_md_path, updated)?; | ||
| println!( | ||
| "{} AGENTS.md AgentDiff section updated in {}", | ||
| ok(), | ||
| agents_md_path.display() | ||
| ); | ||
| return Ok(()); | ||
| } | ||
| // Start sentinel present but no end sentinel — file is malformed. Warn and skip | ||
| // rather than appending a second block and corrupting the file further. | ||
| eprintln!( | ||
| "warn: AGENTS.md has an opening agentdiff sentinel but no closing sentinel; \ | ||
| skipping to avoid creating a duplicate block. Fix manually: add \ | ||
| `{}` after the managed section.", | ||
| AGENTS_MD_END | ||
| ); | ||
| return Ok(()); | ||
| } | ||
|
|
||
| // Append block to file (create if absent). | ||
| let separator = if existing.is_empty() || existing.ends_with('\n') { | ||
| "\n" | ||
| } else { | ||
| "\n\n" | ||
| }; | ||
| let updated = format!("{}{}{}\n", existing, separator, block); | ||
| std::fs::write(&agents_md_path, updated)?; | ||
|
|
||
| if existing.is_empty() { | ||
| println!( | ||
| "{} AGENTS.md created with AgentDiff section at {}", | ||
| ok(), | ||
| agents_md_path.display() | ||
| ); | ||
| } else { | ||
| println!( | ||
| "{} AGENTS.md AgentDiff section added to {}", | ||
| ok(), | ||
| agents_md_path.display() | ||
| ); | ||
| } | ||
| Ok(()) | ||
| } | ||
|
|
||
| #[cfg(test)] | ||
| mod tests { | ||
| use super::*; | ||
| use std::fs; | ||
|
|
||
| fn tmp_dir() -> std::path::PathBuf { | ||
| let dir = std::env::temp_dir() | ||
| .join(format!("agentdiff-agents-md-{}", std::process::id())); | ||
| fs::create_dir_all(&dir).unwrap(); | ||
| dir | ||
| } | ||
|
|
||
| /// Create a fake git repo root (just a `.git` dir) so the git-repo guard passes. | ||
| fn fake_git_repo(dir: &std::path::Path) { | ||
| fs::create_dir_all(dir.join(".git")).unwrap(); | ||
| } | ||
|
|
||
| #[test] | ||
| fn creates_agents_md_from_scratch() { | ||
| let dir = tmp_dir().join("scratch"); | ||
| fs::create_dir_all(&dir).unwrap(); | ||
| fake_git_repo(&dir); | ||
|
|
||
| step_configure_agents_md(&dir).unwrap(); | ||
|
|
||
| let content = fs::read_to_string(dir.join("AGENTS.md")).unwrap(); | ||
| assert!(content.contains(AGENTS_MD_START)); | ||
| assert!(content.contains(AGENTS_MD_END)); | ||
| assert!(content.contains("## AgentDiff")); | ||
| assert!(content.contains("agentdiff context path/to/file")); | ||
|
|
||
| let _ = fs::remove_dir_all(&dir); | ||
| } | ||
|
|
||
| #[test] | ||
| fn idempotent_when_block_unchanged() { | ||
| let dir = tmp_dir().join("idempotent"); | ||
| fs::create_dir_all(&dir).unwrap(); | ||
| fake_git_repo(&dir); | ||
|
|
||
| step_configure_agents_md(&dir).unwrap(); | ||
| let first = fs::read_to_string(dir.join("AGENTS.md")).unwrap(); | ||
|
|
||
| step_configure_agents_md(&dir).unwrap(); | ||
| let second = fs::read_to_string(dir.join("AGENTS.md")).unwrap(); | ||
|
|
||
| assert_eq!(first, second); | ||
| let _ = fs::remove_dir_all(&dir); | ||
| } | ||
|
|
||
| #[test] | ||
| fn updates_existing_block_without_duplicating() { | ||
| let dir = tmp_dir().join("update"); | ||
| fs::create_dir_all(&dir).unwrap(); | ||
| fake_git_repo(&dir); | ||
|
|
||
| // Write a stale block. | ||
| let stale = format!( | ||
| "# My Project\n\n{start}\n## AgentDiff\n\nOld content here.\n{end}\n\n## Other section\n", | ||
| start = AGENTS_MD_START, | ||
| end = AGENTS_MD_END, | ||
| ); | ||
| fs::write(dir.join("AGENTS.md"), &stale).unwrap(); | ||
|
|
||
| step_configure_agents_md(&dir).unwrap(); | ||
|
|
||
| let content = fs::read_to_string(dir.join("AGENTS.md")).unwrap(); | ||
| // Should have exactly one managed block. | ||
| assert_eq!(content.matches(AGENTS_MD_START).count(), 1); | ||
| assert_eq!(content.matches(AGENTS_MD_END).count(), 1); | ||
| // Old content replaced. | ||
| assert!(!content.contains("Old content here.")); | ||
| // Surrounding content preserved. | ||
| assert!(content.contains("# My Project")); | ||
| assert!(content.contains("## Other section")); | ||
|
|
||
| let _ = fs::remove_dir_all(&dir); | ||
| } | ||
|
|
||
| #[test] | ||
| fn appends_to_existing_agents_md_without_managed_block() { | ||
| let dir = tmp_dir().join("append"); | ||
| fs::create_dir_all(&dir).unwrap(); | ||
| fake_git_repo(&dir); | ||
|
|
||
| let pre_existing = "# My Project\n\nSome existing content.\n"; | ||
| fs::write(dir.join("AGENTS.md"), pre_existing).unwrap(); | ||
|
|
||
| step_configure_agents_md(&dir).unwrap(); | ||
|
|
||
| let content = fs::read_to_string(dir.join("AGENTS.md")).unwrap(); | ||
| assert!(content.starts_with("# My Project")); | ||
| assert!(content.contains("Some existing content.")); | ||
| assert!(content.contains(AGENTS_MD_START)); | ||
| assert!(content.contains("## AgentDiff")); | ||
|
|
||
| let _ = fs::remove_dir_all(&dir); | ||
| } | ||
|
|
||
| #[test] | ||
| fn skips_when_not_a_git_repo() { | ||
| let dir = tmp_dir().join("not-git"); | ||
| fs::create_dir_all(&dir).unwrap(); | ||
| // No .git directory — should skip without error. | ||
| step_configure_agents_md(&dir).unwrap(); | ||
| assert!(!dir.join("AGENTS.md").exists()); | ||
| let _ = fs::remove_dir_all(&dir); | ||
| } | ||
|
|
||
| #[test] | ||
| fn warns_and_skips_on_unclosed_sentinel() { | ||
| let dir = tmp_dir().join("malformed"); | ||
| fs::create_dir_all(&dir).unwrap(); | ||
| fake_git_repo(&dir); | ||
|
|
||
| // Write a file with an opening sentinel but no closing sentinel. | ||
| let malformed = format!("# Project\n\n{start}\n## AgentDiff\n\nTruncated block with no end.\n", start = AGENTS_MD_START); | ||
| fs::write(dir.join("AGENTS.md"), &malformed).unwrap(); | ||
|
|
||
| step_configure_agents_md(&dir).unwrap(); | ||
|
|
||
| // File must be unchanged — no duplicate block appended. | ||
| let content = fs::read_to_string(dir.join("AGENTS.md")).unwrap(); | ||
| assert_eq!(content.matches(AGENTS_MD_START).count(), 1, "must not duplicate start sentinel"); | ||
| assert!(!content.contains(AGENTS_MD_END), "end sentinel must not be added silently"); | ||
|
|
||
| let _ = fs::remove_dir_all(&dir); | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.