Skip to content

RemoteWorkspaceBackend::build_full_file_diff() emits whole-file replace as diff, not unified diff #429

@chubes4

Description

@chubes4

Summary

`RemoteWorkspaceBackend::build_full_file_diff()` in `inc/Workspace/RemoteWorkspaceBackend.php` (lines 663-679) is not a unified diff. It dumps the entire old content as `-` lines and the entire new content as `+` lines, regardless of how small the actual change is.

```php
private function build_full_file_diff( string $path, string $old_content, string $new_content ): string {
$old_lines = $this->diff_lines( $old_content );
$new_lines = $this->diff_lines( $new_content );

\$diff  = 'diff --git a/' . \$path . ' b/' . \$path . \"\n\";
\$diff .= '--- a/' . \$path . \"\n\";
\$diff .= '+++ b/' . \$path . \"\n\";
\$diff .= sprintf( '@@ -1,%d +1,%d @@', count( \$old_lines ), count( \$new_lines ) ) . \"\n\";
foreach ( \$old_lines as \$line ) {
    \$diff .= '-' . \$line . \"\n\";
}
foreach ( \$new_lines as \$line ) {
    \$diff .= '+' . \$line . \"\n\";
}
return \$diff;

}
```

Impact: agents misread their own edits

`workspace_edit` correctly applies a surgical `substr_replace` against the file content. But when the agent then calls `workspace_git_diff` to verify, it sees the entire 250-line file marked for removal and re-addition. This looks like "my edit nuked the file" or "the file has line-ending weather," even though the actual write to GitHub is correct.

Real example from world-of-wordpress agent run 26050485830:

  • Agent edits a single `

    ` heading on `content/page/world-observatory.md` (one-line change).

  • `workspace_edit` returns `replacements: 1` — fine.
  • `workspace_git_diff` returns `@@ -1,250 +1,278 @@` followed by every line of the file removed then re-added.
  • Agent concludes "the edit produced whole-file line-ending weather" and reverts the change to avoid committing a noisy rewrite.

The agent has now hit this pattern three cycles in a row on the same Observatory page (PR #343, PR #345, and an earlier cycle), each time exercising the restraint verb the prompt menu provides. The restraint is correct given what the agent sees, but it's based on a phantom diff.

Why "line-ending weather" is a phantom

`diff_lines()` does `explode("\n", rtrim($content, "\n"))`. The actual content stored in `pending_files` after `edit_file` is byte-identical to what came out of `read_file` except for the `substr_replace` window. The fake diff format hides that — the agent has no way to see what actually changed.

Fix sketch

Replace the whole-file dump with a real unified diff. Options:

  1. PHP-side unified diff — use `xdiff_string_diff()` if available, else `league/uri`-style minimal-myers-diff in PHP. ~50-100 line implementation, no new dependencies if we accept basic Myers diff.
  2. Shell out to git — DMC already coordinates GitHub-backed worktrees; we could write the old + new content to temp files and call `git diff --no-index --unified=3 old new`. Cleaner output, but requires git on the WP host.
  3. Use `sebastian/diff` library — small composer dep, produces real unified diffs, well-tested.

Option 1 or 3 is preferable on `intelligence-chubes4`-style hosts where shelling to git is sketchy.

Workaround for agents in the meantime

Until this is fixed, prompts that rely on `workspace_git_diff` verification should either (a) skip the diff step on edits and trust `workspace_edit`'s `replacements: 1` return, or (b) treat the diff output as advisory only and not as evidence the edit failed. The bundle's prompt for `world-creator` will be widened to mention this.

Related

AI assistance

Diagnosed by Claude Code (Sonnet 4.5) from the world-creator agent's transcript and DMC source, after the agent's daily memory described "line-ending weather" three cycles in a row. The diff function source was the smoking gun.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions