Skip to content

Fix agentdiff sync and PR attribution reporting#7

Merged
codeprakhar25 merged 2 commits into
mainfrom
fix/sync-ci-attribution
Apr 26, 2026
Merged

Fix agentdiff sync and PR attribution reporting#7
codeprakhar25 merged 2 commits into
mainfrom
fix/sync-ci-attribution

Conversation

@codeprakhar25
Copy link
Copy Markdown
Owner

Summary

  • Fix agentdiff diff so commit/range mode reports only changed new-side lines.
  • Make Cursor capture handle more hook line-range payload shapes.
  • Prevent repeated slow trace pushes after successful sync and make PR CI post/update attribution comments.

Test plan

  • cargo test
  • python3 -m unittest scripts.tests.test_capture_cursor
  • cargo build
  • target/debug/agentdiff -C /home/prakh/agentdiff diff HEAD --ai-only

Closes #6

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented Apr 26, 2026

Greptile Summary

This PR fixes three distinct issues: agentdiff diff now uses git diff --unified=0 {commit}^! to report only new-side changed lines (not context lines), Cursor hook capture gains a richer set of line-range payload parsers, and push avoids redundant slow syncs by returning early when all local traces are already present on the remote ref. The CI workflows gain pull-requests: write permission and a post-attribution-comment step using gh pr comment --edit-last --create-if-none.

Confidence Score: 5/5

Safe to merge; all remaining findings are P2 style/robustness suggestions that do not affect the primary fix paths.

The core fixes (new-side-only diff parsing, early-return push, idempotent PR comment) are correct and well-tested. Three P2 findings remain: silent git exit-code swallowing, potential over-attribution in extract_lines_from_changes when both top-level and nested range keys co-exist, and a slightly misleading success message when remote push is skipped. None of these block merge.

src/commands/diff.rs (exit-code check) and scripts/capture-cursor.py (over-attribution guard) are worth a second look before the next release.

Important Files Changed

Filename Overview
src/commands/diff.rs Rewrites parse_diff_hunks to track only new-side (+) lines and uses {commit}^! spec; exit code of git diff is not checked, so initial-commit or invalid-ref errors are silently swallowed.
scripts/capture-cursor.py Adds flexible line-range extraction helpers; extract_lines_from_changes unconditionally processes both top-level and nested range keys, potentially over-attributing lines when both are present.
src/commands/push.rs Adds early-return when no new traces exist, proper error handling for remote read/write, and prune_local_traces helper; success message displayed even when remote push was skipped.
src/commands/report.rs Adds --edit-last --create-if-none to gh pr comment for idempotent attribution comment updates; minor reformatting only.
src/commands/install_ci.rs Adds pull-requests: write permission, a PR head checkout step, and a post-attribution-comment step to the policy workflow; formatting cleanup only for the Rust side.
scripts/tests/test_capture_cursor.py Adds two unit tests covering extract_lines_from_changes and extract_lines with mixed payload shapes; tests look correct.

Sequence Diagram

sequenceDiagram
    participant Dev as Developer
    participant Push as agentdiff push
    participant LocalRef as Local Git Ref
    participant GitHub as GitHub API

    Dev->>Push: agentdiff push
    Push->>GitHub: fetch remote traces (API)
    alt fetch succeeds
        GitHub-->>Push: remote traces JSONL
    else fetch fails
        GitHub-->>Push: error (remote_read_failed=true)
    end
    Push->>Push: dedup local vs remote (new_count)
    Push->>LocalRef: write merged traces.jsonl
    alt new_count == 0
        Push->>Push: prune_local_traces()
        Push-->>Dev: already up to date
    else new_count > 0
        alt remote_read_failed
            Push-->>Dev: wrote N trace(s) to local ref
        else
            Push->>GitHub: push_content_to_ref (best-effort)
            alt push succeeds
                GitHub-->>Push: ok
                Push->>Push: prune_local_traces()
                Push-->>Dev: pushed N trace(s)
            else push fails
                Push-->>Dev: warn + pushed N trace(s) [misleading]
            end
        end
    end
Loading

Comments Outside Diff (1)

  1. src/commands/push.rs, line 120-128 (link)

    P2 Success message misleading when remote push was skipped

    remote_pushed is false both when the push legitimately failed and when the read failed (remote_read_failed == true) causing the push to be skipped entirely. In the latter case, the printed message still says "pushed N trace(s)" with the ✓ prefix, which could confuse users into thinking the data reached GitHub when it did not.

Reviews (1): Last reviewed commit: "fix: improve agentdiff sync and PR attri..." | Re-trigger Greptile

Comment thread src/commands/diff.rs
Comment on lines 82 to 89
fn run_git_diff(repo_root: &std::path::Path, commit: &str) -> Result<String> {
let spec = diff_spec(commit);
let output = std::process::Command::new("git")
.args(["diff", commit])
.args(["diff", "--unified=0", "--no-color", "--no-ext-diff", &spec])
.current_dir(repo_root)
.output()?;
Ok(String::from_utf8_lossy(&output.stdout).to_string())
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Git exit code not checked — silent empty diff on failure

Command::output()? captures stdout but does not inspect output.status. If git diff HEAD^! exits non-zero (e.g. the initial commit has no parent, a typo in the ref, or a shallow clone), stdout is empty and parse_diff_hunks returns an empty map. The diff command then silently prints "0 lines changed, 0 AI-attributed" with no indication of the underlying error. The switch to {commit}^! makes this edge case more likely to surface (initial commits, shallow clones).

Suggested change
fn run_git_diff(repo_root: &std::path::Path, commit: &str) -> Result<String> {
let spec = diff_spec(commit);
let output = std::process::Command::new("git")
.args(["diff", commit])
.args(["diff", "--unified=0", "--no-color", "--no-ext-diff", &spec])
.current_dir(repo_root)
.output()?;
Ok(String::from_utf8_lossy(&output.stdout).to_string())
}
fn run_git_diff(repo_root: &std::path::Path, commit: &str) -> Result<String> {
let spec = diff_spec(commit);
let output = std::process::Command::new("git")
.args(["diff", "--unified=0", "--no-color", "--no-ext-diff", &spec])
.current_dir(repo_root)
.output()?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
anyhow::bail!("git diff failed: {}", stderr.trim());
}
Ok(String::from_utf8_lossy(&output.stdout).to_string())
}

Comment thread scripts/capture-cursor.py
Comment on lines +100 to +110
def extract_lines_from_changes(changes):
out = set()
if not isinstance(changes, list):
return []
for change in changes:
if not isinstance(change, dict):
continue
_add_lines_from_range_object(out, change)
for key in ("range", "newRange", "new_range", "selection", "targetSelection"):
_add_lines_from_range_object(out, change.get(key))
return sorted(out)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Potential over-attribution when change has both top-level range and nested range/selection

When a change dict carries both a top-level startLine/endLine and a nested range or selection key (a plausible Cursor payload, e.g. edit at line 10 inside a selected block spanning lines 5–20), _add_lines_from_range_object(out, change) adds only lines 10–10, but the subsequent for key loop then also calls _add_lines_from_range_object(out, change.get("range")), adding lines 5–20. The union over-attributes the surrounding selection as AI-authored. If a specific top-level range is already resolved, there is no reason to also expand from the nested range keys.

Suggested change
def extract_lines_from_changes(changes):
out = set()
if not isinstance(changes, list):
return []
for change in changes:
if not isinstance(change, dict):
continue
_add_lines_from_range_object(out, change)
for key in ("range", "newRange", "new_range", "selection", "targetSelection"):
_add_lines_from_range_object(out, change.get(key))
return sorted(out)
def extract_lines_from_changes(changes):
out = set()
if not isinstance(changes, list):
return []
for change in changes:
if not isinstance(change, dict):
continue
before = len(out)
_add_lines_from_range_object(out, change)
if len(out) == before:
# No top-level line info; try nested range keys.
for key in ("range", "newRange", "new_range", "selection", "targetSelection"):
_add_lines_from_range_object(out, change.get(key))
return sorted(out)

@codeprakhar25 codeprakhar25 merged commit 80aef89 into main Apr 26, 2026
2 checks passed
@github-actions
Copy link
Copy Markdown

AgentDiff Report

Summary

Agent Lines %
claude-code 2049 56%
Prakhar Khatri 1641 44%

Files Modified

File Lines Dominant Agent
src/commands/status.rs 404 Prakhar Khatri
src/configure/mod.rs 402 claude-code
scripts/tests/test_extension.js 282 Prakhar Khatri
src/configure/antigravity.rs 272 claude-code
src/configure/codex.rs 257 claude-code
src/commands/remote_status.rs 185 claude-code
scripts/finalize-ledger.py 178 claude-code
src/commands/report.rs 175 Prakhar Khatri
scripts/tests/test_capture_prompts.py 166 Prakhar Khatri
src/configure/claude.rs 141 claude-code
src/commands/list.rs 135 Prakhar Khatri
src/configure/copilot.rs 114 claude-code
README.md 113 claude-code
scripts/capture-codex.py 102 claude-code
src/configure/windsurf.rs 100 claude-code
src/configure/cursor.rs 94 claude-code
scripts/tests/test_capture_cursor.py 60 Prakhar Khatri
src/configure/opencode.rs 59 claude-code
src/cli.rs 51 Prakhar Khatri
src/util.rs 43 claude-code

@codeprakhar25 codeprakhar25 deleted the fix/sync-ci-attribution branch April 26, 2026 16:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix: cursor capture emits empty lines[] — afterFileEdit payload lacks line range keys

1 participant