improve(hashline-edit): rewrite tool description with examples and fix lines schema

- Add XML-structured description (<must>, <operations>, <examples>, <auto>)
- Add 5 concrete examples including BAD pattern showing duplication
- Add explicit anti-duplication warning for range replace
- Move snapshot rule to top-level <must> 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
This commit is contained in:
YeonGyu-Kim
2026-03-17 16:40:40 +09:00
parent b788586caf
commit f2d5f4ca92
8 changed files with 74 additions and 48 deletions

View File

@@ -7,40 +7,38 @@ WORKFLOW:
4. If same file needs another call, re-read first. 4. If same file needs another call, re-read first.
5. Use anchors as "LINE#ID" only (never include trailing "|content"). 5. Use anchors as "LINE#ID" only (never include trailing "|content").
VALIDATION: <must>
Payload shape: { "filePath": string, "edits": [...], "delete"?: boolean, "rename"?: string } - 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.
Each edit must be one of: replace, append, prepend - 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.
Edit shape: { "op": "replace"|"append"|"prepend", "pos"?: "LINE#ID", "end"?: "LINE#ID", "lines": string|string[]|null } - lines must contain ONLY the content that belongs inside the consumed range. Content after end survives unchanged.
lines must contain plain replacement text only (no LINE#ID prefixes, no diff + markers) - Tags MUST be copied exactly from read output or >>> mismatch output. NEVER guess tags.
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. - 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).
</must>
LINE#ID FORMAT (CRITICAL): <operations>
LINE#ID FORMAT:
Each line reference must be in "{line_number}#{hash_id}" format where: Each line reference must be in "{line_number}#{hash_id}" format where:
{line_number}: 1-based line number {line_number}: 1-based line number
{hash_id}: Two CID letters from the set ZPMQVRWSNKTXJBYH {hash_id}: Two CID letters from the set ZPMQVRWSNKTXJBYH
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.
lines: null or lines: [] with replace -> delete those lines.
FILE MODES: FILE MODES:
delete=true deletes file and requires edits=[] with no rename delete=true deletes file and requires edits=[] with no rename
rename moves final content to a new path and removes old path rename moves final content to a new path and removes old path
CONTENT FORMAT: RULES:
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.
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.
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 (CRITICAL):
1. Minimize scope: one logical mutation site per operation. 1. Minimize scope: one logical mutation site per operation.
2. Preserve formatting: keep indentation, punctuation, line breaks, trailing commas, brace style. 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. 3. Prefer insertion over neighbor rewrites: anchor to structural boundaries (}, ], },), not interior property lines.
@@ -48,22 +46,50 @@ RULES (CRITICAL):
5. Touch only requested code: avoid incidental edits. 5. Touch only requested code: avoid incidental edits.
6. Use exact current tokens: NEVER rewrite approximately. 6. Use exact current tokens: NEVER rewrite approximately.
7. For swaps/moves: prefer one range operation over multiple single-line operations. 7. For swaps/moves: prefer one range operation over multiple single-line operations.
8. Output tool calls only; no prose or commentary between them. 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.
</operations>
TAG CHOICE (ALWAYS): <examples>
- Copy tags exactly from read output or >>> mismatch output. Given this file content after read:
- NEVER guess tags. 10#VK|function hello() {
- Anchor to structural lines (function/class/brace), NEVER blank lines. 11#XJ| console.log("hi");
- Anti-pattern warning: blank/whitespace anchors are fragile. 12#MB| console.log("bye");
- Re-read after each successful edit call before issuing another on the same file. 13#QR|}
14#TN|
15#WS|function world() {
AUTOCORRECT (built-in - you do NOT need to handle these): Single-line replace (change line 11):
{ op: "replace", pos: "11#XJ", lines: [" console.log(\\"hello\\");"] }
Result: line 11 replaced. Lines 10, 12-15 unchanged.
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\\";"] }
</examples>
<auto>
Built-in autocorrect (you do NOT need to handle these):
Merged lines are auto-expanded back to original line count. Merged lines are auto-expanded back to original line count.
Indentation is auto-restored from original lines. Indentation is auto-restored from original lines.
BOM and CRLF line endings are preserved automatically. BOM and CRLF line endings are preserved automatically.
Hashline prefixes and diff markers in text are auto-stripped. Hashline prefixes and diff markers in text are auto-stripped.
Boundary echo lines (duplicating adjacent surviving lines) are auto-stripped.
</auto>
RECOVERY (when >>> mismatch error appears): RECOVERY (when >>> mismatch error appears):
Copy the updated LINE#ID tags shown in the error output directly. 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. Re-read only if the needed tags are missing from the error snippet.`
ALWAYS batch all edits for one file in a single call.`

View File

@@ -30,7 +30,7 @@ export function createHashlineEditTool(): ToolDefinition {
pos: tool.schema.string().optional().describe("Primary anchor in LINE#ID format"), 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"), end: tool.schema.string().optional().describe("Range end anchor in LINE#ID format"),
lines: tool.schema 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"), .describe("Replacement or inserted lines as newline-delimited string. null deletes with replace"),
}) })
) )