From f2d5f4ca9223550b08508e7f0fc55e68a755bd8b Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Tue, 17 Mar 2026 16:40:40 +0900 Subject: [PATCH] improve(hashline-edit): rewrite tool description with examples and fix lines schema - Add XML-structured description (, , , ) - Add 5 concrete examples including BAD pattern showing duplication - Add explicit anti-duplication warning for range replace - Move snapshot rule to top-level section - Clarify batch semantics (multiple ops, not one big replace) - Fix lines schema: add string[] to union (was string|null, now string[]|string|null) - Matches runtime RawHashlineEdit type and description text --- src/tools/hashline-edit/tool-description.ts | 120 +++++++++++------- src/tools/hashline-edit/tools.ts | 2 +- {benchmarks => tests/hashline}/bun.lock | 0 {benchmarks => tests/hashline}/headless.ts | 0 {benchmarks => tests/hashline}/package.json | 0 .../hashline}/test-edge-cases.ts | 0 .../hashline}/test-edit-ops.ts | 0 .../hashline}/test-multi-model.ts | 0 8 files changed, 74 insertions(+), 48 deletions(-) rename {benchmarks => tests/hashline}/bun.lock (100%) rename {benchmarks => tests/hashline}/headless.ts (100%) rename {benchmarks => tests/hashline}/package.json (100%) rename {benchmarks => tests/hashline}/test-edge-cases.ts (100%) rename {benchmarks => tests/hashline}/test-edit-ops.ts (100%) rename {benchmarks => tests/hashline}/test-multi-model.ts (100%) diff --git a/src/tools/hashline-edit/tool-description.ts b/src/tools/hashline-edit/tool-description.ts index c8a566860..211a487ad 100644 --- a/src/tools/hashline-edit/tool-description.ts +++ b/src/tools/hashline-edit/tool-description.ts @@ -7,63 +7,89 @@ WORKFLOW: 4. If same file needs another call, re-read first. 5. Use anchors as "LINE#ID" only (never include trailing "|content"). -VALIDATION: - Payload shape: { "filePath": string, "edits": [...], "delete"?: boolean, "rename"?: string } - Each edit must be one of: replace, append, prepend - Edit shape: { "op": "replace"|"append"|"prepend", "pos"?: "LINE#ID", "end"?: "LINE#ID", "lines": string|string[]|null } - lines must contain plain replacement text only (no LINE#ID prefixes, no diff + markers) - CRITICAL: all operations validate against the same pre-edit file snapshot and apply bottom-up. Refs/tags are interpreted against the last-read version of the file. + +- SNAPSHOT: All edits in one call reference the ORIGINAL file state. Do NOT adjust line numbers for prior edits in the same call — the system applies them bottom-up automatically. +- replace removes lines pos..end (inclusive) and inserts lines in their place. Lines BEFORE pos and AFTER end are UNTOUCHED — do NOT include them in lines. If you do, they will appear twice. +- lines must contain ONLY the content that belongs inside the consumed range. Content after end survives unchanged. +- Tags MUST be copied exactly from read output or >>> mismatch output. NEVER guess tags. +- Batch = multiple operations in edits[], NOT one big replace covering everything. Each operation targets the smallest possible change. +- lines must contain plain replacement text only (no LINE#ID prefixes, no diff + markers). + -LINE#ID FORMAT (CRITICAL): - Each line reference must be in "{line_number}#{hash_id}" format where: - {line_number}: 1-based line number - {hash_id}: Two CID letters from the set ZPMQVRWSNKTXJBYH + +LINE#ID FORMAT: + Each line reference must be in "{line_number}#{hash_id}" format where: + {line_number}: 1-based line number + {hash_id}: Two CID letters from the set ZPMQVRWSNKTXJBYH -FILE MODES: - delete=true deletes file and requires edits=[] with no rename - rename moves final content to a new path and removes old path +OPERATION CHOICE: + replace with pos only -> replace one line at pos + replace with pos+end -> replace range pos..end inclusive as a block (ranges MUST NOT overlap across edits) + append with pos/end anchor -> insert after that anchor + prepend with pos/end anchor -> insert before that anchor + append/prepend without anchors -> EOF/BOF insertion (also creates missing files) CONTENT FORMAT: lines can be a string (single line) or string[] (multi-line, preferred). If you pass a multi-line string, it is split by real newline characters. - Literal "\\n" is preserved as text. + lines: null or lines: [] with replace -> delete those lines. -FILE CREATION: - append without anchors adds content at EOF. If file does not exist, creates it. - prepend without anchors adds content at BOF. If file does not exist, creates it. - CRITICAL: only unanchored append/prepend can create a missing file. +FILE MODES: + delete=true deletes file and requires edits=[] with no rename + rename moves final content to a new path and removes old path -OPERATION CHOICE: - replace with pos only -> replace one line at pos - replace with pos+end -> replace ENTIRE range pos..end as a block (ranges MUST NOT overlap across edits) - append with pos/end anchor -> insert after that anchor - prepend with pos/end anchor -> insert before that anchor - append/prepend without anchors -> EOF/BOF insertion +RULES: + 1. Minimize scope: one logical mutation site per operation. + 2. Preserve formatting: keep indentation, punctuation, line breaks, trailing commas, brace style. + 3. Prefer insertion over neighbor rewrites: anchor to structural boundaries (}, ], },), not interior property lines. + 4. No no-ops: replacement content must differ from current content. + 5. Touch only requested code: avoid incidental edits. + 6. Use exact current tokens: NEVER rewrite approximately. + 7. For swaps/moves: prefer one range operation over multiple single-line operations. + 8. Anchor to structural lines (function/class/brace), NEVER blank lines. + 9. Re-read after each successful edit call before issuing another on the same file. + -RULES (CRITICAL): - 1. Minimize scope: one logical mutation site per operation. - 2. Preserve formatting: keep indentation, punctuation, line breaks, trailing commas, brace style. - 3. Prefer insertion over neighbor rewrites: anchor to structural boundaries (}, ], },), not interior property lines. - 4. No no-ops: replacement content must differ from current content. - 5. Touch only requested code: avoid incidental edits. - 6. Use exact current tokens: NEVER rewrite approximately. - 7. For swaps/moves: prefer one range operation over multiple single-line operations. - 8. Output tool calls only; no prose or commentary between them. + +Given this file content after read: + 10#VK|function hello() { + 11#XJ| console.log("hi"); + 12#MB| console.log("bye"); + 13#QR|} + 14#TN| + 15#WS|function world() { -TAG CHOICE (ALWAYS): - - Copy tags exactly from read output or >>> mismatch output. - - NEVER guess tags. - - Anchor to structural lines (function/class/brace), NEVER blank lines. - - Anti-pattern warning: blank/whitespace anchors are fragile. - - Re-read after each successful edit call before issuing another on the same file. +Single-line replace (change line 11): + { op: "replace", pos: "11#XJ", lines: [" console.log(\\"hello\\");"] } + Result: line 11 replaced. Lines 10, 12-15 unchanged. -AUTOCORRECT (built-in - you do NOT need to handle these): - Merged lines are auto-expanded back to original line count. - Indentation is auto-restored from original lines. - BOM and CRLF line endings are preserved automatically. - Hashline prefixes and diff markers in text are auto-stripped. +Range replace (rewrite function body, lines 11-12): + { op: "replace", pos: "11#XJ", end: "12#MB", lines: [" return \\"hello world\\";"] } + Result: lines 11-12 removed, replaced by 1 new line. Lines 10, 13-15 unchanged. + +Delete a line: + { op: "replace", pos: "12#MB", lines: null } + Result: line 12 removed. Lines 10-11, 13-15 unchanged. + +Insert after line 13 (between functions): + { op: "append", pos: "13#QR", lines: ["", "function added() {", " return true;", "}"] } + Result: 4 new lines inserted after line 13. All existing lines unchanged. + +BAD — lines extend past end (DUPLICATES line 13): + { op: "replace", pos: "11#XJ", end: "12#MB", lines: [" return \\"hi\\";", "}"] } + Line 13 is "}" which already exists after end. Including "}" in lines duplicates it. + CORRECT: { op: "replace", pos: "11#XJ", end: "12#MB", lines: [" return \\"hi\\";"] } + + + +Built-in autocorrect (you do NOT need to handle these): + Merged lines are auto-expanded back to original line count. + Indentation is auto-restored from original lines. + BOM and CRLF line endings are preserved automatically. + Hashline prefixes and diff markers in text are auto-stripped. + Boundary echo lines (duplicating adjacent surviving lines) are auto-stripped. + RECOVERY (when >>> mismatch error appears): - Copy the updated LINE#ID tags shown in the error output directly. - Re-read only if the needed tags are missing from the error snippet. - ALWAYS batch all edits for one file in a single call.` + Copy the updated LINE#ID tags shown in the error output directly. + Re-read only if the needed tags are missing from the error snippet.` diff --git a/src/tools/hashline-edit/tools.ts b/src/tools/hashline-edit/tools.ts index 798b86273..6d9cb9424 100644 --- a/src/tools/hashline-edit/tools.ts +++ b/src/tools/hashline-edit/tools.ts @@ -30,7 +30,7 @@ export function createHashlineEditTool(): ToolDefinition { pos: tool.schema.string().optional().describe("Primary anchor in LINE#ID format"), end: tool.schema.string().optional().describe("Range end anchor in LINE#ID format"), lines: tool.schema - .union([tool.schema.string(), tool.schema.null()]) + .union([tool.schema.array(tool.schema.string()), tool.schema.string(), tool.schema.null()]) .describe("Replacement or inserted lines as newline-delimited string. null deletes with replace"), }) ) diff --git a/benchmarks/bun.lock b/tests/hashline/bun.lock similarity index 100% rename from benchmarks/bun.lock rename to tests/hashline/bun.lock diff --git a/benchmarks/headless.ts b/tests/hashline/headless.ts similarity index 100% rename from benchmarks/headless.ts rename to tests/hashline/headless.ts diff --git a/benchmarks/package.json b/tests/hashline/package.json similarity index 100% rename from benchmarks/package.json rename to tests/hashline/package.json diff --git a/benchmarks/test-edge-cases.ts b/tests/hashline/test-edge-cases.ts similarity index 100% rename from benchmarks/test-edge-cases.ts rename to tests/hashline/test-edge-cases.ts diff --git a/benchmarks/test-edit-ops.ts b/tests/hashline/test-edit-ops.ts similarity index 100% rename from benchmarks/test-edit-ops.ts rename to tests/hashline/test-edit-ops.ts diff --git a/benchmarks/test-multi-model.ts b/tests/hashline/test-multi-model.ts similarity index 100% rename from benchmarks/test-multi-model.ts rename to tests/hashline/test-multi-model.ts