@@ -295,22 +295,45 @@ export class MultiSearchReplaceDiffStrategy implements DiffStrategy {
295295 // Has ======= but missing >>>>>>> REPLACE — append closing marker
296296 const body = block . replace ( / \s + $ / , "" )
297297 repaired += body + "\n>>>>>>> REPLACE" + separator
298+ } else if ( hasCloser && ! hasSeparator ) {
299+ // Has >>>>>>> REPLACE but missing the ======= separator. Don't synthesize a
300+ // second closer; splice the separator in right before the existing closer so
301+ // everything above it becomes the SEARCH section.
302+ const body = block . replace ( / \s + $ / , "" )
303+ repaired += body . replace ( / ( \n ) ( > > > > > > > R E P L A C E ) (? = \n | $ ) / , "$1=======\n$2" ) + separator
298304 } else {
299- // Missing both ======= and >>>>>>> REPLACE
305+ // Missing both ======= and >>>>>>> REPLACE.
300306 const searchMatch = block . match ( / ^ < < < < < < < S E A R C H \n ? ( [ \s \S ] * ) $ / )
301- const content = ( searchMatch ?. [ 1 ] ?? "" ) . replace ( / \s + $ / , "" )
307+ let content = ( searchMatch ?. [ 1 ] ?? "" ) . replace ( / \s + $ / , "" )
308+
309+ // Peel off any leading Grok header directives (:start_line:, :end_line:, -------)
310+ // so the "first line is SEARCH" heuristic sees real content, not metadata. The
311+ // directives are preserved as a header on the SEARCH section.
312+ let header = ""
313+ const directiveLine = / ^ (?: : s t a r t _ l i n e : \s * \d + | : e n d _ l i n e : \s * \d + | - - - - - - - ) \s * $ /
314+ let nlIdx : number
315+ while ( ( nlIdx = content . indexOf ( "\n" ) ) !== - 1 && directiveLine . test ( content . slice ( 0 , nlIdx ) ) ) {
316+ header += content . slice ( 0 , nlIdx + 1 )
317+ content = content . slice ( nlIdx + 1 )
318+ }
319+
302320 const firstNewlineIdx = content . indexOf ( "\n" )
303321 if ( firstNewlineIdx !== - 1 ) {
304322 // First line is SEARCH content, rest is REPLACE content
305323 const searchContent = content . substring ( 0 , firstNewlineIdx )
306324 const replaceContent = content . substring ( firstNewlineIdx + 1 )
307325 repaired +=
308326 "<<<<<<< SEARCH\n" +
327+ header +
309328 searchContent +
310329 "\n=======\n" +
311330 replaceContent +
312331 "\n>>>>>>> REPLACE" +
313332 separator
333+ } else if ( header ) {
334+ // Only a directive header plus a single content line: that line is the SEARCH
335+ // target (the user pinned it with start_line); the REPLACE section is empty.
336+ repaired += "<<<<<<< SEARCH\n" + header + content + "\n=======\n\n>>>>>>> REPLACE" + separator
314337 } else {
315338 // Single line — treat as empty SEARCH with content as REPLACE
316339 repaired += "<<<<<<< SEARCH\n=======\n" + content + "\n>>>>>>> REPLACE" + separator
@@ -329,9 +352,9 @@ export class MultiSearchReplaceDiffStrategy implements DiffStrategy {
329352 ) : Promise < DiffResult > {
330353 // Repair truncated diffs before validation (common with Grok and other models
331354 // whose output gets cut off mid-stream, leaving missing ======= and >>>>>>> REPLACE markers)
332- diffContent = this . repairTruncatedDiff ( diffContent )
355+ const repairedDiff = this . repairTruncatedDiff ( diffContent )
333356
334- const validseq = this . validateMarkerSequencing ( diffContent )
357+ const validseq = this . validateMarkerSequencing ( repairedDiff )
335358 if ( ! validseq . success ) {
336359 return {
337360 success : false ,
@@ -371,7 +394,7 @@ export class MultiSearchReplaceDiffStrategy implements DiffStrategy {
371394 */
372395
373396 let matches = [
374- ...diffContent . matchAll (
397+ ...repairedDiff . matchAll (
375398 / (?: ^ | \n ) (?< ! \\ ) < < < < < < < S E A R C H > ? \s * \n ( (?: \: s t a r t _ l i n e : \s * ( \d + ) \s * \n ) ) ? ( (?: \: e n d _ l i n e : \s * ( \d + ) \s * \n ) ) ? ( (?< ! \\ ) - - - - - - - \s * \n ) ? ( [ \s \S ] * ?) (?: \n ) ? (?: (?< = \n ) (?< ! \\ ) = = = = = = = \s * \n ) ( [ \s \S ] * ?) (?: \n ) ? (?: (?< = \n ) (?< ! \\ ) > > > > > > > R E P L A C E ) (? = \n | $ ) / g,
376399 ) ,
377400 ]
0 commit comments