@@ -62,8 +62,8 @@ func init() {
6262// FileSpec represents a file with optional line range guard
6363type FileSpec struct {
6464 Path string
65- StartLine int // 1-indexed, 0 means from beginning
66- EndLine int // 1-indexed, 0 means to end
65+ StartLine int // 1-indexed, 0 means from beginning
66+ EndLine int // 1-indexed, 0 means to end
6767 HasGuard bool
6868}
6969
@@ -185,7 +185,7 @@ func runEdit(cmd *cobra.Command, args []string) error {
185185
186186 // Build prompts
187187 systemPrompt := buildEditSystemPrompt (cfg .Edit .Instructions , specs )
188- userPrompt := buildEditUserPrompt (request , files )
188+ userPrompt := buildEditUserPrompt (request , files , specs )
189189
190190 // Track file contents for applying edits
191191 fileContents := make (map [string ]string )
@@ -259,27 +259,30 @@ func processEditsConsolidated(edits []llm.EditToolCall, fileContents map[string]
259259 continue
260260 }
261261
262- if ! strings .Contains (currentContent , edit .OldString ) {
263- cf .skipReasons = append (cf .skipReasons , fmt .Sprintf ("old_string not found: %.40s..." , edit .OldString ))
264- continue
262+ // Find the matching spec to check for guards
263+ var matchSpec * FileSpec
264+ for i := range specs {
265+ if specs [i ].Path == path {
266+ matchSpec = & specs [i ]
267+ break
268+ }
265269 }
266270
267- // Check guard
268- guardErr := ""
269- for _ , spec := range specs {
270- if spec .Path == path && spec .HasGuard {
271- if err := validateGuardForReplace (currentContent , edit , spec ); err != nil {
272- guardErr = err .Error ()
273- break
274- }
275- }
271+ var match editMatch
272+ var err error
273+ if matchSpec != nil && matchSpec .HasGuard {
274+ // Use guard-scoped matching
275+ match , err = findEditMatchWithGuard (currentContent , edit .OldString , matchSpec .StartLine , matchSpec .EndLine )
276+ } else {
277+ // No guard, search full content
278+ match , err = findEditMatch (currentContent , edit .OldString )
276279 }
277- if guardErr != "" {
278- cf .skipReasons = append (cf .skipReasons , guardErr )
280+ if err != nil {
281+ cf .skipReasons = append (cf .skipReasons , err . Error () )
279282 continue
280283 }
281284
282- currentContent = strings . Replace (currentContent , edit . OldString , edit .NewString , 1 )
285+ currentContent = applyEditMatch (currentContent , match , edit .NewString )
283286 cf .editCount ++
284287 }
285288
@@ -376,27 +379,33 @@ func processEditsIndividually(edits []llm.EditToolCall, fileContents map[string]
376379 continue
377380 }
378381
379- if ! strings .Contains (content , editCall .OldString ) {
382+ // Find the matching spec to check for guards
383+ var matchSpec * FileSpec
384+ for i := range specs {
385+ if specs [i ].Path == editCall .FilePath {
386+ matchSpec = & specs [i ]
387+ break
388+ }
389+ }
390+
391+ var match editMatch
392+ var err error
393+ if matchSpec != nil && matchSpec .HasGuard {
394+ // Use guard-scoped matching
395+ match , err = findEditMatchWithGuard (content , editCall .OldString , matchSpec .StartLine , matchSpec .EndLine )
396+ } else {
397+ // No guard, search full content
398+ match , err = findEditMatch (content , editCall .OldString )
399+ }
400+ if err != nil {
380401 pe .skip = true
381- pe .skipReason = "old_string not found"
402+ pe .skipReason = err . Error ()
382403 processed = append (processed , pe )
383404 continue
384405 }
385406
386- for _ , spec := range specs {
387- if spec .Path == editCall .FilePath && spec .HasGuard {
388- if err := validateGuardForReplace (content , editCall , spec ); err != nil {
389- pe .skip = true
390- pe .skipReason = err .Error ()
391- break
392- }
393- }
394- }
395-
396- if ! pe .skip {
397- pe .oldContent = content
398- pe .newContent = strings .Replace (content , editCall .OldString , editCall .NewString , 1 )
399- }
407+ pe .oldContent = content
408+ pe .newContent = applyEditMatch (content , match , editCall .NewString )
400409 processed = append (processed , pe )
401410 }
402411
@@ -464,27 +473,33 @@ Context:
464473 base += fmt .Sprintf ("\n - User Context: %s" , instructions )
465474 }
466475
467- base += `
476+ base += fmt . Sprintf ( `
468477
469478Rules:
4704791. Make minimal, focused changes
4714802. Preserve existing code style
4724813. Use the edit tool for each change - you can call it multiple times
4734824. The edit tool does find/replace: old_string must match exactly
474- 5. Include enough context in old_string to be unique`
483+ 5. You may include the literal token %s in old_string to match any sequence of characters (including newlines)
484+ 6. Include enough context in old_string (especially around %s) to be unique` , editWildcardToken , editWildcardToken )
475485
476486 // Add guard info
487+ hasGuards := false
477488 for _ , spec := range specs {
478489 if spec .HasGuard {
479- base += fmt .Sprintf ("\n \n IMPORTANT: For %s, only modify lines %d-%d." ,
490+ hasGuards = true
491+ base += fmt .Sprintf ("\n \n IMPORTANT: For %s, only modify lines %d-%d. The <editable-region> block shows the exact content you may edit with line numbers." ,
480492 spec .Path , spec .StartLine , spec .EndLine )
481493 }
482494 }
495+ if hasGuards {
496+ base += "\n \n Your old_string MUST match text within the editable region. Use the line numbers in <editable-region> to ensure your edit is within bounds."
497+ }
483498
484499 return base
485500}
486501
487- func buildEditUserPrompt (request string , files []input.FileContent ) string {
502+ func buildEditUserPrompt (request string , files []input.FileContent , specs [] FileSpec ) string {
488503 var sb strings.Builder
489504
490505 sb .WriteString ("Files:\n \n " )
@@ -497,20 +512,59 @@ func buildEditUserPrompt(request string, files []input.FileContent) string {
497512 sb .WriteString ("</file>\n \n " )
498513 }
499514
515+ // Add editable region blocks for guarded files
516+ for _ , spec := range specs {
517+ if spec .HasGuard {
518+ // Find the matching file content
519+ for _ , f := range files {
520+ if f .Path == spec .Path {
521+ excerpt := extractLineRange (f .Content , spec .StartLine , spec .EndLine )
522+ sb .WriteString (fmt .Sprintf ("<editable-region path=\" %s\" lines=\" %d-%d\" >\n " ,
523+ spec .Path , spec .StartLine , spec .EndLine ))
524+ sb .WriteString (excerpt )
525+ if ! strings .HasSuffix (excerpt , "\n " ) {
526+ sb .WriteString ("\n " )
527+ }
528+ sb .WriteString ("</editable-region>\n \n " )
529+ break
530+ }
531+ }
532+ }
533+ }
534+
500535 sb .WriteString (fmt .Sprintf ("Request: %s" , request ))
501536 return sb .String ()
502537}
503538
504- func validateGuardForReplace (content string , editCall llm.EditToolCall , spec FileSpec ) error {
505- // Find where old_string appears
506- idx := strings .Index (content , editCall .OldString )
507- if idx < 0 {
508- return fmt .Errorf ("old_string not found" )
539+ // extractLineRange extracts lines startLine to endLine (1-indexed, inclusive) from content
540+ func extractLineRange (content string , startLine , endLine int ) string {
541+ lines := strings .Split (content , "\n " )
542+
543+ // Adjust for 0-based indexing
544+ start := startLine - 1
545+ if start < 0 {
546+ start = 0
547+ }
548+ end := endLine
549+ if end <= 0 || end > len (lines ) {
550+ end = len (lines )
551+ }
552+ if start >= len (lines ) {
553+ return ""
509554 }
510555
556+ // Build output with line numbers
557+ var sb strings.Builder
558+ for i := start ; i < end ; i ++ {
559+ sb .WriteString (fmt .Sprintf ("%d: %s\n " , i + 1 , lines [i ]))
560+ }
561+ return strings .TrimSuffix (sb .String (), "\n " )
562+ }
563+
564+ func validateGuardForReplace (content string , match editMatch , spec FileSpec ) error {
511565 // Count lines before
512- lineNum := strings .Count (content [:idx ], "\n " ) + 1
513- endLineNum := lineNum + strings .Count (editCall . OldString , "\n " )
566+ lineNum := strings .Count (content [:match . start ], "\n " ) + 1
567+ endLineNum := lineNum + strings .Count (match . text , "\n " )
514568
515569 // Check if within guard
516570 if spec .StartLine > 0 && lineNum < spec .StartLine {
0 commit comments