edit_apply validated the target with IsPathAllowed in the handler, then later
wrote it via os.WriteFile on the raw path after reading+parsing+diffing. A
symlink swapped in during that window could redirect the write outside the
workspace root. Inject the admission check into the edit Engine
(NewEngineWithValidator) and re-run it inside a new writeValidatedFile helper
immediately before the write — IsPathAllowed resolves symlinks at write time, so
a swapped-in link now resolving outside the root is rejected. This narrows the
TOCTOU window to near-zero while still permitting legitimate in-workspace symlink
edits (unlike a blanket O_NOFOLLOW). writeValidatedFile also dedups the
perm-preserving write shared by AST and text modes.
Tests: rejected-path edit leaves the file untouched; file mode is preserved.
firstLineOf (query) and truncateString (edit) sliced strings at a raw byte
offset, splitting a multibyte UTF-8 character into invalid bytes in
LLM-facing output (ast_query compact format and edit not-found errors are
hot paths). Extract the rune-boundary backoff already used by search's
truncateLine into util.RuneBoundary and route all three sites through it.
Adds table-driven tests for the primitive and a multibyte case for firstLineOf.
hunkBounds approximated one-sided start lines by reusing the opposite side's number, and re-assigned it every iteration inside the count==0 guard. For pure insertion/deletion hunks (e.g. writing into an empty file, or deleting a whole file) this produced wrong headers like '@@ -2,0 +1,2 @@' instead of '@@ -0,0 +1,2 @@'. flattenDiffLines now records both running line numbers on every line, so hunkBounds takes the start from the first line directly and applies GNU's zero-count convention (line-before-change, clamped to 0). Adds TestGenerateDiffHunkHeaderBounds.
A replace with empty new_content failed with a bare 'new_content is required' message, leaving the model to guess. Append a hint to use operation=delete when the intent is removal. Adds TestValidateReplaceEmptySuggestsDelete.
- [x] Remove auto-indentation from text mode edits (caller controls whitespace)
- [x] Add line-ending detection and normalization for both AST and text modes
- [x] Share edit logic via new `spliceContent` function for both modes
- [x] Fix diff to emit "No newline at end of file" markers
- [x] Fix diff to strip raw CR from CRLF file output
- [x] Remove double-unescape of backslash sequences in new_content
- [x] Fix countDiffLines to be hunk-aware (correctly count lines starting with +/-)
- [x] Fix block-comment stripping to remove standalone lines cleanly
- [x] Fix Python license header stripping to preserve separator blank lines