diff --git a/assets/functions/tools/fs_patch.sh b/assets/functions/tools/fs_patch.sh index a47e29a..13d8655 100755 --- a/assets/functions/tools/fs_patch.sh +++ b/assets/functions/tools/fs_patch.sh @@ -5,6 +5,23 @@ set -e # PREFERRED way to modify a file. Prefer this over fs_write whenever the file already exists: it sends less data, # preserves unchanged content automatically, and is less prone to accidental data loss from full rewrites. # Use fs_write only when you are creating a new file or doing a complete rewrite where most of the content changes. +# +# CRITICAL — the patch is matched byte-for-byte. There is no fuzzy matching, no whitespace tolerance, and no context shift: +# - Context lines (prefixed with a single space) and removed lines (prefixed with '-') must equal the file content exactly. +# If unsure, fs_cat the file first and copy the bytes verbatim into your patch. +# - JSON-escape the contents string ONCE. Each literal backslash in the file becomes \\ in the JSON contents string. So a +# shell line containing s|\\"|"|g must appear in JSON as s|\\\\\"|\"|g — NOT s|\\\\\\\"|\\\"|g. Over-escaping backslashes +# is the most common cause of "unable to apply patch" failures, especially in files with sed/jq/regex pipelines or +# embedded Python with quoted strings. +# - Hunks are applied in order; the first hunk that fails aborts the whole patch — later hunks are NOT attempted. +# - If you've edited this file in earlier tool calls, fs_cat it again before composing the patch. A stale view of the file +# produces context lines that no longer match. +# - On failure the error message names the failing hunk and shows the expected-vs-actual line. Fix that specific line and +# retry — do not blindly resend a near-identical patch. +# +# For files with heavy escaping (sed/jq/regex pipelines, shell with embedded heredocs, deeply quoted strings), prefer +# fs_write over chained fs_patch hunks to replace the entire file with the full new contents (i.e. original content + +# your changes). # @option --path! The path of the file to apply the patch to # @option --contents! The patch to apply to the file diff --git a/assets/functions/utils/prompt-utils.sh b/assets/functions/utils/prompt-utils.sh index cfa695a..b00f738 100755 --- a/assets/functions/utils/prompt-utils.sh +++ b/assets/functions/utils/prompt-utils.sh @@ -600,6 +600,14 @@ patch_file() { for (i = 2; i <= hunkTotalOriginalLines[hunkIndex]; i++) { if (lines[nextLineIndex] != hunkOriginalLines[hunkIndex,i]) { + if (i - 1 > bestPartialLen[hunkIndex]) { + bestPartialLen[hunkIndex] = i - 1 + bestPartialAnchorLine[hunkIndex] = lineIndex + bestPartialHunkPos[hunkIndex] = i + bestPartialDivergeLine[hunkIndex] = nextLineIndex + bestPartialExpected[hunkIndex] = hunkOriginalLines[hunkIndex,i] + bestPartialActual[hunkIndex] = lines[nextLineIndex] + } nextLineIndex = 0 break } @@ -621,7 +629,32 @@ patch_file() { } if (hunkIndex != totalHunks + 1) { + failingHunk = hunkIndex print "error: unable to apply patch" > "/dev/stderr" + print "" > "/dev/stderr" + print "Hunk " failingHunk " of " totalHunks " did not match the file." > "/dev/stderr" + + if (bestPartialLen[failingHunk] == 0) { + print "" > "/dev/stderr" + print "The first context/removed line of hunk " failingHunk " was not found anywhere in the file:" > "/dev/stderr" + print " expected: " hunkOriginalLines[failingHunk, 1] > "/dev/stderr" + } else { + print "" > "/dev/stderr" + print "Closest match: anchored at file line " bestPartialAnchorLine[failingHunk] ", matched " bestPartialLen[failingHunk] " of " hunkTotalOriginalLines[failingHunk] " original lines before diverging." > "/dev/stderr" + print "" > "/dev/stderr" + print "At file line " bestPartialDivergeLine[failingHunk] " (hunk original line " bestPartialHunkPos[failingHunk] "):" > "/dev/stderr" + print " expected: " bestPartialExpected[failingHunk] > "/dev/stderr" + print " actual: " bestPartialActual[failingHunk] > "/dev/stderr" + } + + print "" > "/dev/stderr" + print "Lines must match byte-for-byte (no fuzzy matching). Check escaping, whitespace, and quoting." > "/dev/stderr" + + if (failingHunk < totalHunks) { + print "" > "/dev/stderr" + print (totalHunks - failingHunk) " subsequent hunk(s) were not attempted (patcher aborts on first failure)." > "/dev/stderr" + } + exit 1 } }