feat: attribute turn diffs so git-only changes stop polluting changed files - #4249
feat: attribute turn diffs so git-only changes stop polluting changed files#4249t3dotgg wants to merge 4 commits into
Conversation
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
ApprovabilityVerdict: Needs human review New feature adding git attribution logic with 3 unresolved medium-severity bugs identified (unborn HEAD handling, past-dated commits, mobile compatibility). The complexity of the attribution algorithm and its impact on what diffs users see warrants human review. You can customize Macroscope's approvability policy. Learn more. |
… files The changed-files card and per-turn inline diff are derived from full-tree checkpoint snapshots, so any tree-rewriting git command the agent runs (pull, checkout, cherry-pick, rebase) shows up as hundreds of "changed files" the agent never authored. This adds a read-side attribution layer on top of the existing checkpoint mechanism (capture, storage, and revert are untouched): - Checkpoint commits now record the HEAD oid and branch in their commit message. Legacy checkpoints without metadata degrade to today's behavior. - attributeCheckpointDiff classifies each path in a checkpoint tree delta: a path is "git" only when its exact blob transition also appears in the HEAD-movement delta for the turn and no commit authored during the turn touched it. Every ambiguity resolves to "agent" (shown), so misclassification can add noise but never hide agent work. Merge commits use the combined diff so only conflict resolutions count as agent work. - The checkpoint reactor tags each file with an origin; attribution failures degrade to unattributed files and never block the pipeline. - getTurnDiff/getFullThreadDiff drop git-attributed file sections from the patch unless the new includeGitChanges input is set. - The changed-files card lists agent files only, with a muted "Updated via git — N files" row for demoted changes. The diff panel shows a banner with a show/hide toggle so the full tree delta stays one click away. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- resolveDiffSectionPath scanned line-by-line and returned the `--- a/` source path before ever seeing `+++ b/`, so rename sections resolved to the pre-image path and could be mis-filtered (macroscope, bugbot). Now resolves the post-image path in a full pass with the source as fallback, plus a rename filtering test. - `commit --amend` of a pre-turn commit preserved the old author date, so amended agent content was misattributed to history and hidden — the one invariant violation both bots flagged. Commits authored before the turn but committed during it are now disambiguated by patch-id: an identical patch elsewhere in the repo (branches/remotes/reflog — reflog catches pre-rebase originals) means replayed pre-existing content (demote); otherwise it's an amend (show). Integration test included. - Per-commit diff-tree now passes --root so agent-authored root commits on orphan branches contribute their paths (macroscope). - includeGitChanges is scoped to the diff panel selection key so revealing git changes on one turn no longer leaks into other turns/threads (macroscope). - Git-only turns render a compact card with just the "Updated via git" row instead of a "0 changed files" header with dead controls (macroscope). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…iltering, and banner drift - patch-id --stable strips whitespace, so a whitespace-only amend collided with its pre-amend reflog original and was demoted as a replay — hiding agent work (macroscope, bugbot, codex). Switched to --verbatim and added an integration test amending a whitespace-only change. - Binary and mode-only sections have no ---/+++ lines, so they always survived filtering even when git-attributed (codex). resolveDiffSectionPath now falls back to rename/copy metadata and then the `diff --git` header (with C-style quoted-path decoding); genuinely ambiguous headers are still kept. Tests for binary, mode-only, quoted, spaced, rename-only, and ambiguous sections. - The diff panel banner was driven by the turn summary's origin tags while the patch was filtered by an independent attribution pass, so the two could disagree after partial failures (codex). getTurnDiff and getFullThreadDiff now return gitFileCount from the same pass that filtered the patch, and the banner uses that. Codex also flagged attribution cost (re-running per diff-query variant); deferred — all calls are plumbing on OIDs, the fast path covers turns without HEAD movement, and caching by checkpoint pair can come later if real-world latency shows up. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ch sections - Git C-style quoting octal-escapes raw UTF-8 bytes; decoding each escape with String.fromCharCode produced mojibake, so attribution lookups missed quoted non-ASCII paths and their sections were never filtered (macroscope, bugbot). stripGitPathQuoting now decodes into a byte buffer and UTF-8 decodes the result. - gitFileCount counted every git-attributed path in the attribution map, overstating hidden files when a path had no section in the patch (e.g. whitespace-only changes elided by ignoreWhitespace) and double-counting renames from the --no-renames map (macroscope, bugbot). It now counts git-attributed sections actually present in the patch, so the banner matches what the toggle can reveal. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
f2c7b15 to
bcebb79
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 3 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit bcebb79. Configure here.
| // no git-origin paths". | ||
| if (fromMetadata.headOid === toMetadata.headOid || fromMetadata.headOid === null) { | ||
| return new Map<string, "agent" | "git">(); | ||
| } |
There was a problem hiding this comment.
Unborn HEAD skips git attribution
Medium Severity
The fast path treats any turn that starts with unborn HEAD (head=none) as having no git-origin changes, including when HEAD later points at a real commit. A git pull / git checkout of existing history into a fresh repo therefore keeps every path as agent work, so the changed-files card and diff panel still show the foreign tree as agent edits.
Reviewed by Cursor Bugbot for commit bcebb79. Configure here.
| turnAuthoredCommits.push(commit); | ||
| } else if (committedAt >= fromCommitTime) { | ||
| rewrittenCommits.push(commit); | ||
| } |
There was a problem hiding this comment.
Past-dated agent commits demoted to git
Medium Severity
Turn commit classification only keeps commits with author or committer time at/after the baseline checkpoint. An agent commit whose author and committer dates both precede that baseline falls into neither bucket, so matching HEAD-movement blob transitions are labeled git and can be filtered out of the review diff, contrary to the fail-toward-agent rule.
Reviewed by Cursor Bugbot for commit bcebb79. Configure here.
| fromCheckpointRef, | ||
| toCheckpointRef, | ||
| diff: rawDiff, | ||
| includeGitChanges: input.includeGitChanges ?? false, |
There was a problem hiding this comment.
Mobile diffs hide git changes silently
Medium Severity
getTurnDiff / getFullThreadDiff now default includeGitChanges to false, so history-driven sections are stripped for every client. Mobile still builds review file lists from that patch and never sends includeGitChanges or offers a reveal control, so a git-only turn (pull, checkout, rebase) renders as an empty diff with no explanation.
Reviewed by Cursor Bugbot for commit bcebb79. Configure here.
|
Closing in favor of #2829 (orchestration V2). #2829 deletes the V1 orchestration layer this PR builds on — This is not a judgement on the change itself. Several of these are real gaps we still want fixed; the base just moved out from under them. Once #2829 merges, please rebase onto |


Problem
The changed-files card and per-turn inline diff are derived from full-tree checkpoint snapshots (
git diff snap(N-1) snap(N)). That answers "what bytes changed between turn boundaries" — but the card is a review surface, and the moment the agent runs a tree-rewriting git command the two diverge:git pull origin main→ hundreds of already-reviewed upstream files listed as agent changesgit checkout other-branch→ the entire branch delta appearsgit cherry-pick/git rebase→ content authored elsewhere shows up as newApproach
Keep the snapshot mechanism exactly as-is (it powers revert and is ground truth) and add a read-side attribution layer. We don't need to observe what the agent did — git history records it after the fact:
head=<oid>andbranch=<name>in their commit message (no schema/ref changes; legacy checkpoints degrade to today's behavior).attributeCheckpointDiffclassifies each path in a checkpoint tree delta: a path isgitonly when its exact blob transition (mode + oid both sides,--no-renames) also appears in the HEAD-movement delta and no commit authored during the turn touched it. Commits are considered turn-authored when their author date ≥ the baseline checkpoint's commit time — cherry-pick/rebase preserve author dates, so replayed foreign commits are correctly demoted, while the agent committing its own edits stays "agent". Merge commits use the combined diff (-c) so only conflict resolutions count as agent work.rev-parses at capture, and behavior is identical to today.Surfaces
⎇ Updated via git — N filesrow for demoted changes (with aggregate +/− stats). A git-only turn shows just that row instead of disappearing.includeGitChangesinput ongetTurnDiff/getFullThreadDiff).OrchestrationCheckpointFilegains an optionalorigin: "agent" | "git"field.Testing
CheckpointAttribution.test.ts)CheckpointStore.test.ts)Diffs.test.ts), partition tests (turnDiffTree.test.ts)Plan write-up: https://syizveepeikc.postplan.dev
🤖 Generated with Claude Code
Note
Medium Risk
Non-trivial git history heuristics and new diff filtering affect what users see; ambiguous cases degrade to unfiltered diffs and legacy checkpoints skip attribution rather than blocking checkpoints.
Overview
Turn diffs and changed-files views no longer treat every checkpoint tree delta as agent work when git rewrote the tree (pull, checkout, cherry-pick, rebase). A read-side attribution layer classifies paths as
agentvsgit, while snapshot capture/revert stays unchanged.Server: Checkpoint commits embed HEAD oid + branch in commit messages. New
attributeCheckpointDiff(VCS →CheckpointStore) compares checkpoint tree deltas to HEAD-movement deltas and turn-authored commits (patch-id disambiguation for cherry-picks/amends).getTurnDiff/getFullThreadDiffrun attribution afterdiffCheckpoints, strip git-attributed file sections by default, acceptincludeGitChanges, and return optionalgitFileCountaligned with the rendered patch.filterUnifiedDiffFiles/ path parsing handles renames, binaries, and quoted paths.CheckpointReactortags turn summary files with optionalorigin(best-effort; failures leave files unattributed).Contracts & client: Optional
originon checkpoint files; diff APIs gainincludeGitChangesandgitFileCount.Web: Changed-files card lists agent files only plus a compact “Updated via git” row; diff panel shows a show/hide git changes banner per turn scope.
Reviewed by Cursor Bugbot for commit bcebb79. Bugbot is set up for automated code reviews on this repo. Configure here.
Note
Filter git-attributed file changes from turn diffs to reduce noise in changed files view
GitVcsDriver.attributeCheckpointDiffto classify each changed path as'agent'or'git'by comparing tree and history deltas, with conservative fallback tonull.CheckpointDiffQueryapplies this attribution to filter out git-attributed sections from unified diffs by default; callers passincludeGitChanges: trueto see them.CheckpointReactorattaches a per-fileoriginfield to turn summary files, andMessagesTimelinecollapses git-origin files into a separate 'Updated via git' row.nullfor legacy checkpoints and all files are treated as agent-authored.Macroscope summarized bcebb79.