fix(lifecycle): compare committed files through Git's clean filters - #2482
Conversation
filterToUncommittedFiles decided "already committed" by comparing the working tree's raw bytes against the HEAD blob. Under core.autocrlf — the Git for Windows default, and what both testutil.InitRepo and e2e/testutil/repo.go set — the working tree holds CRLF while the blob holds LF, so every committed text file compared unequal and the filter never dropped anything. That matters because the filter is what stops an agent's mid-turn commit from being checkpointed twice. With it broken, turn-end saw a file that PostCommit had already condensed, missed the "no changes, skip" gate, and minted a fresh shadow branch on the *new* HEAD seconds after PostCommit deleted the old one. Nothing condenses that branch away — the session ends and no further commit arrives — so it outlives the session. On the nightly install smoke that surfaced as copilot-cli failing TestMultiSessionSequential on windows-latest with "shadow branches should be cleaned up within 10s after commit", every night since 09-11. The condition was latent from the start and only became reachable when copilot-cli's turn-end file list stopped being empty. The last green nightly ran the same broken filter, so its list was genuinely empty rather than correctly filtered; the only extraction change in that window is #2341's restrictedProperties.filePaths fallback. That fallback follows Copilot moving the field out of properties, so both halves are needed to explain the flip — and neither is at fault: reporting a path the agent later committed is correct at that layer, and narrowing it to what is still uncommitted is this function's job. The three other Windows agents produce no turn-end list at all and so never reached the comparison. Compare through gitrepo.HashWorktreeFiles (git hash-object) instead, which applies Git's path-specific clean filters. This also fixes .gitattributes eol/text rules and clean filters such as Git LFS, which the byte comparison got wrong for the same reason. Symlinks stay on the raw path: hash-object follows the link and hashes the target's content, while a Git symlink blob stores the target path. Anything Git cannot hash falls back to the previous comparison, so the function still fails open — toward keeping a file rather than dropping one. Both call sites benefit: turn-end and the subagent task path, whose own comment already described this exact failure. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Entire-Checkpoint: 01M2JJHKDTXB4SAFGYRC6FWRWJ
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Want fixes drafted automatically? Bugbot Autofix can create code changes for findings. A team admin can enable Autofix in the Cursor dashboard.
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 1a16ed8. Configure here.
There was a problem hiding this comment.
🟡 Changes recommended
Address the unresolved worktree entry validation issue before approval.
Get a fresh assessment by requesting another Copilot review.
Pull request overview
Fixes lifecycle file filtering by comparing worktree files through Git’s clean filters, preventing redundant checkpoints under CRLF and similar configurations.
Changes:
- Uses
HashWorktreeFilesfor filter-aware comparisons. - Preserves conservative fallback behavior.
- Adds CRLF regression tests for clean and modified files.
File summaries
| File | Summary |
|---|---|
cmd/entire/cli/state.go |
Implements Git-aware filtering. Moderate issue: validate worktree entries are regular files before hashing to avoid symlink/type-change misclassification or FIFO blocking. |
cmd/entire/cli/state_test.go |
Adds CRLF handling regression coverage. |
Review details
- Files reviewed: 2/2 changed files
- Comments generated: 1
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Checking only the HEAD entry's mode was not enough. git hash-object
follows a working-tree symlink and hashes the target's *content*, so a
tracked regular file replaced by a link to identical content hashes equal
to its HEAD blob and was dropped as "already committed" — while git status
calls it a typechange (" T"). The HEAD entry is still a regular file, so
the tree mode cannot catch it. Verified directly: after replacing a
committed foo.txt with a symlink to a file of equal content,
`git hash-object -- foo.txt` returns the HEAD blob hash exactly.
That drops a real change, which is the direction this function must never
fail in. A FIFO is worse than wrong: hash-object blocks reading it, and on
a hook path that costs the caller its whole budget.
strategy already had this rule and a test asserting it, so rather than add
a third copy the pair moves to worktreedir.HashableEntry, where both
callers reach it. It cannot live beside HashWorktreeFiles in gitrepo:
worktreedir's own test imports testutil, which imports gitrepo, so that
edge is an import cycle in the test binary. Its tests move with it, and
content_overlap_test keeps the half that is still its own — what the
confined fallback answers for a symlink.
Reported by Cursor Bugbot and Copilot on #2482, both pointing at
strategy.requiresConfinedWorktreeHash as the precedent. They were right.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 01M2JNBMD9081P0M1ZTX1KKXM2

https://entire.io/gh/entireio/cli/trails/1331
Summary
.gitattributes, and Git LFS. Preserve raw-byte fallbacks for symlinks and hashing failures.Testing
mise run checkandGOOS=windows go vet ./cmd/...passed.