Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion cmd/entire/cli/strategy/content_overlap.go
Original file line number Diff line number Diff line change
Expand Up @@ -434,8 +434,19 @@ func filesWithRemainingAgentChanges(
var remaining []string

for _, filePath := range filesTouched {
// If file wasn't committed at all, it definitely has remaining changes
// If file wasn't committed, check if it actually exists in the shadow tree.
// Transcript parsers capture file paths from all tool calls, including ones
// that were attempted but never created (e.g. agent writes src/types.go then
// creates src/types/types.go instead). buildTreeWithChanges skips non-existent
// files, so the shadow tree is the ground truth. Without this check, phantom
// paths cause infinite carry-forward loops.
if _, wasCommitted := committedFiles[filePath]; !wasCommitted {
if _, err := shadowTree.File(filePath); err != nil {
logging.Debug(logCtx, "filesWithRemainingAgentChanges: file not committed and not in shadow tree, skipping",
slog.String("file", filePath),
)
continue
}
Comment on lines +437 to +449
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

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

The new "phantom file" check treats any path missing from the shadow tree as skippable when it wasn’t committed. That also matches legitimate agent deletions (deleted files are included in FilesTouched), causing uncommitted deletions to be dropped from carry-forward. Consider only skipping when the path is missing from BOTH the shadow tree and the HEAD/commit tree (or otherwise explicitly handle deletions), and include the underlying shadowTree.File error in the debug log. Adding a unit test for an uncommitted deletion would prevent regressions.

Copilot uses AI. Check for mistakes.
remaining = append(remaining, filePath)
logging.Debug(logCtx, "filesWithRemainingAgentChanges: file not committed, keeping",
slog.String("file", filePath),
Expand Down
47 changes: 47 additions & 0 deletions cmd/entire/cli/strategy/content_overlap_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -663,6 +663,53 @@ func TestFilesWithRemainingAgentChanges_CacheEquivalence(t *testing.T) {
assert.NotContains(t, resultWith, "fileA.txt")
}

// TestFilesWithRemainingAgentChanges_PhantomFile tests that files tracked in
// filesTouched but not present in the shadow branch tree are skipped. This
// happens when an agent's transcript references a file path (e.g. via a
// write_file tool call) that was never actually created on disk — for example
// when Gemini tries to write src/types.go but creates src/types/types.go
// instead. Without this check, phantom files cause infinite carry-forward.
func TestFilesWithRemainingAgentChanges_PhantomFile(t *testing.T) {
t.Parallel()
dir := setupGitRepo(t)

repo, err := git.PlainOpen(dir)
require.NoError(t, err)

// Shadow branch only contains the REAL file (buildTreeWithChanges skips
// non-existent files, so the phantom path is never in the tree).
createShadowBranchWithContent(t, repo, "phn1234", "e3b0c4", map[string][]byte{
"src/types/types.go": []byte("package types\n\ntype User struct{}\n"),
})

// Create the real file on disk and commit it.
require.NoError(t, os.MkdirAll(filepath.Join(dir, "src", "types"), 0o755))
require.NoError(t, os.WriteFile(filepath.Join(dir, "src", "types", "types.go"),
[]byte("package types\n\ntype User struct{}\n"), 0o644))
wt, err := repo.Worktree()
require.NoError(t, err)
_, err = wt.Add("src/types/types.go")
require.NoError(t, err)
headCommit, err := wt.Commit("Add types.go", &git.CommitOptions{
Author: &object.Signature{Name: "Test", Email: "test@test.com", When: time.Now()},
})
require.NoError(t, err)

commit, err := repo.CommitObject(headCommit)
require.NoError(t, err)

shadowBranch := checkpoint.ShadowBranchNameForCommit("phn1234", "e3b0c4")
committedFiles := map[string]struct{}{"src/types/types.go": {}}

// filesTouched includes both the real path and a phantom path.
remaining := filesWithRemainingAgentChanges(context.Background(), repo, shadowBranch, commit,
[]string{"src/types.go", "src/types/types.go"}, committedFiles)

// src/types.go is not committed AND not in shadow tree → skip.
// src/types/types.go is committed with matching content → skip.
assert.Empty(t, remaining, "Phantom files not in shadow tree should not be carried forward")
}

// TestStagedFilesOverlapWithContent_ModifiedFile tests that a modified file
// (exists in HEAD) always counts as overlap.
func TestStagedFilesOverlapWithContent_ModifiedFile(t *testing.T) {
Expand Down
Loading