diff --git a/cmd/entire/cli/strategy/content_overlap.go b/cmd/entire/cli/strategy/content_overlap.go index b33e423f1..c10a61b39 100644 --- a/cmd/entire/cli/strategy/content_overlap.go +++ b/cmd/entire/cli/strategy/content_overlap.go @@ -186,10 +186,14 @@ func filesOverlapWithContent(ctx context.Context, repo *git.Repository, shadowBr func stagedFilesOverlapWithContent(ctx context.Context, repo *git.Repository, shadowTree *object.Tree, stagedFiles, filesTouched []string) bool { logCtx := logging.WithComponent(ctx, "checkpoint") - // Build set of filesTouched for quick lookup + // Build set of filesTouched for quick lookup. + // Normalize to base filenames as a fallback to handle path format mismatches + // (absolute vs repo-relative) from stale sessions or older CLI versions (#768). touchedSet := make(map[string]bool) + touchedByBase := make(map[string]bool) // fallback: basename-only lookup for _, f := range filesTouched { touchedSet[f] = true + touchedByBase[filepath.Base(f)] = true } // Get HEAD tree to determine if files are being modified or newly created @@ -232,8 +236,17 @@ func stagedFilesOverlapWithContent(ctx context.Context, repo *git.Repository, sh // Check each staged file for _, stagedPath := range stagedFiles { + // Try exact path match first, then fall back to basename match. + // This handles stale sessions or older CLI versions that stored + // absolute paths in FilesTouched (#768). if !touchedSet[stagedPath] { - continue // Not in filesTouched, skip + if !touchedByBase[filepath.Base(stagedPath)] { + continue // Not in filesTouched by exact or basename, skip + } + logging.Debug(logCtx, "stagedFilesOverlapWithContent: path format mismatch, matched by basename", + slog.String("staged_path", stagedPath), + slog.Any("files_touched", filesTouched), + ) } // Check if this is a modified file (exists in HEAD) or new file diff --git a/cmd/entire/cli/strategy/manual_commit_git.go b/cmd/entire/cli/strategy/manual_commit_git.go index 01e7012a9..9623be267 100644 --- a/cmd/entire/cli/strategy/manual_commit_git.go +++ b/cmd/entire/cli/strategy/manual_commit_git.go @@ -141,7 +141,8 @@ func (s *ManualCommitStrategy) SaveStep(ctx context.Context, step StepContext) e state.PromptAttributions = append(state.PromptAttributions, promptAttr) // Track touched files (modified, new, and deleted) - state.FilesTouched = mergeFilesTouched(state.FilesTouched, step.ModifiedFiles, step.NewFiles, step.DeletedFiles) + // Normalize paths to repo-relative form to prevent path format mismatches (#768) + state.FilesTouched = mergeFilesTouched(state.WorktreePath, state.FilesTouched, step.ModifiedFiles, step.NewFiles, step.DeletedFiles) // On first checkpoint, record the transcript identifier for this session if state.StepCount == 1 { @@ -273,7 +274,8 @@ func (s *ManualCommitStrategy) SaveTaskStep(ctx context.Context, step TaskStepCo } // Track touched files (modified, new, and deleted) - state.FilesTouched = mergeFilesTouched(state.FilesTouched, step.ModifiedFiles, step.NewFiles, step.DeletedFiles) + // Normalize paths to repo-relative form to prevent path format mismatches (#768) + state.FilesTouched = mergeFilesTouched(state.WorktreePath, state.FilesTouched, step.ModifiedFiles, step.NewFiles, step.DeletedFiles) // Save updated state if err := s.saveSessionState(ctx, state); err != nil { @@ -315,15 +317,17 @@ func (s *ManualCommitStrategy) SaveTaskStep(ctx context.Context, step TaskStepCo } // mergeFilesTouched merges multiple file lists into existing touched files, deduplicating. -func mergeFilesTouched(existing []string, fileLists ...[]string) []string { +// All paths are normalized to repo-relative form using worktreePath to prevent +// path format mismatches between absolute agent paths and repo-relative git paths (#768). +func mergeFilesTouched(worktreePath string, existing []string, fileLists ...[]string) []string { seen := make(map[string]bool) for _, f := range existing { - seen[f] = true + seen[normalizeToRepoRelative(f, worktreePath)] = true } for _, list := range fileLists { for _, f := range list { - seen[f] = true + seen[normalizeToRepoRelative(f, worktreePath)] = true } } @@ -337,6 +341,18 @@ func mergeFilesTouched(existing []string, fileLists ...[]string) []string { return result } +// normalizeToRepoRelative converts a file path to repo-relative form. +// If the path is already relative or worktreePath is empty, the path is returned as-is. +func normalizeToRepoRelative(filePath, worktreePath string) string { + if worktreePath == "" || !filepath.IsAbs(filePath) { + return filePath + } + if rel := paths.ToRelativePath(filePath, worktreePath); rel != "" { + return rel + } + return filePath +} + // accumulateTokenUsage adds new token usage to existing accumulated usage. // If existing is nil, returns a copy of incoming. If incoming is nil, returns existing unchanged. func accumulateTokenUsage(existing, incoming *agent.TokenUsage) *agent.TokenUsage {