From f09c5bd67c1ef3d29047cffa78cae403db36d7c0 Mon Sep 17 00:00:00 2001 From: can1357 Date: Sun, 22 Mar 2026 22:42:42 +0100 Subject: [PATCH] feat(patch): added boundary duplication detection to prevent off-by-one range errors - Added boundary duplication warning to detect off-by-one range errors in replace_range and replace_line operations. - Updated hashline tool documentation with boundary duplication trap guidance to prevent closing delimiter duplication. --- packages/coding-agent/CHANGELOG.md | 2 +- packages/coding-agent/src/patch/hashline.ts | 32 +++++++++++++++++++ .../src/prompts/tools/hashline.md | 1 + 3 files changed, 34 insertions(+), 1 deletion(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 9c9b98b73..1920da9ec 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -1,7 +1,6 @@ # Changelog ## [Unreleased] - ### Breaking Changes - Renamed hashline edit operation types: `append` → `append_at`, `prepend` → `prepend_at`, `append_eof` → `append_file`, `prepend_bof` → `prepend_file` @@ -11,6 +10,7 @@ ### Added +- Added boundary duplication warning when replace_range or replace_line operations include a last inserted line that matches the next surviving line, helping detect off-by-one range errors - Added git branch isolation for autoresearch sessions via `ensureAutoresearchBranch()` to safely revert failed experiments - Added branch status line to autoresearch initialization and resume prompts showing created or reused branch name - Added `Files in Scope`, `Off Limits`, and `Constraints` sections to autoresearch.md template for explicit scope definition diff --git a/packages/coding-agent/src/patch/hashline.ts b/packages/coding-agent/src/patch/hashline.ts index e4c2b6df5..58330de73 100644 --- a/packages/coding-agent/src/patch/hashline.ts +++ b/packages/coding-agent/src/patch/hashline.ts @@ -539,6 +539,38 @@ export function applyHashlineEdits( } maybeAutocorrectEscapedTabIndentation(edits, warnings); maybeWarnSuspiciousUnicodeEscapePlaceholder(edits, warnings); + + // Warn when a replace_range/replace_line's last inserted line duplicates the next surviving line. + // This catches the common boundary-overreach pattern where the agent includes a closing delimiter + // in the replacement but sets `end` to the line before the delimiter, causing duplication. + for (const edit of edits) { + let endLine: number; + switch (edit.op) { + case "replace_line": + endLine = edit.pos.line; + break; + case "replace_range": + endLine = edit.end.line; + break; + default: + continue; + } + if (edit.lines.length === 0) continue; + const nextSurvivingIdx = endLine; // 0-indexed: endLine (1-indexed) is the next line after `end` + if (nextSurvivingIdx >= originalFileLines.length) continue; + const nextSurvivingLine = originalFileLines[nextSurvivingIdx]; + const lastInsertedLine = edit.lines[edit.lines.length - 1]; + const trimmedNext = nextSurvivingLine.trim(); + const trimmedLast = lastInsertedLine.trim(); + // Only warn for non-trivial lines to avoid false positives on blank lines or bare punctuation + if (trimmedLast.length > 0 && trimmedLast === trimmedNext) { + const tag = formatLineTag(endLine + 1, nextSurvivingLine); + warnings.push( + `Possible boundary duplication: your last replacement line \`${trimmedLast}\` is identical to the next surviving line ${tag}. ` + + `If you meant to replace the entire block, set \`end\` to ${tag} instead.`, + ); + } + } // Deduplicate identical edits targeting the same line(s) const seenEditKeys = new Map(); const dedupIndices = new Set(); diff --git a/packages/coding-agent/src/prompts/tools/hashline.md b/packages/coding-agent/src/prompts/tools/hashline.md index fcbc3f233..8a5c34d81 100644 --- a/packages/coding-agent/src/prompts/tools/hashline.md +++ b/packages/coding-agent/src/prompts/tools/hashline.md @@ -127,6 +127,7 @@ When adding a sibling declaration, prefer `prepend_at` on the next declaration. - `replace_range` requires both `pos` and `end`. All other anchored ops require `pos` only. - `append_file` and `prepend_file` do not take anchors. - Replace exactly the owned span. If `lines` re-emits content beyond `end`, it will duplicate. +- **Boundary duplication trap**: when replacing a block, `end` must be the **last line of the block** (e.g. the closing `}`), not the last *content* line before it. Otherwise the closing delimiter survives and your replacement adds a second copy. - Do not target shared boundary lines such as `} else {`, `} catch (…) {`, `}),`, or `},{`. - For a block, either replace only the body or replace the whole block. Do not split block boundaries. - `lines` must be literal file content with matching indentation. If the file uses tabs, use real tabs.