diff --git a/docs/tools/edit.md b/docs/tools/edit.md index dd4b6231a..2b35bb44b 100644 --- a/docs/tools/edit.md +++ b/docs/tools/edit.md @@ -34,6 +34,7 @@ Patch language inside `input`: - `delete block N` — delete the whole tree-sitter block beginning on line N (resolved like `replace block N`, with the same decorator/comment caveat). No body. On success the result echoes the matched span (`delete block N → resolved lines A-B`). Same resolution failure modes and `delete N..M` fallback. - `insert before N:` — insert body rows immediately before line N. - `insert after N:` — insert body rows immediately after line N. + - `insert after block N:` — insert body rows after the last line of the tree-sitter block beginning on line N. Point N at the line that opens the construct, never its closing delimiter / last visible line; if you can see the last line already, use plain `insert after M:`. Same resolution failure modes and `insert after M:` fallback. - `insert head:` — insert body rows at the start of the file. - `insert tail:` — insert body rows at the end of the file. - **Body rows**: @@ -62,7 +63,7 @@ The canonical grammar is strict, but the hand parser accepts a few non-dangerous - `delete N..M:` and any body rows under `delete` / `delete block` are rejected. - Empty `replace` / `insert` / `replace block` hunks are rejected. - `-` body rows are rejected with `MINUS_ROW_REJECTED`. -- `replace block N:` / `delete block N` require a wired tree-sitter resolver; `replace block` additionally needs at least one `+TEXT` body row, while `delete block` takes none. An unresolvable block (unsupported language, blank/closing-delimiter line, no node beginning on N, or a syntax error in the resolved block) is rejected on the apply/final-preview path; the streaming preview silently drops it instead. +- `replace block N:` / `delete block N` / `insert after block N:` require a wired tree-sitter resolver; `replace block` and `insert after block` additionally need at least one `+TEXT` body row, while `delete block` takes none. An unresolvable block (unsupported language, blank/closing-delimiter line, no node beginning on N, or a syntax error in the resolved block) is rejected on the apply/final-preview path; the streaming preview silently drops it instead. ## Outputs - Single-shot tool result; hashline mode does not use a `resolve` preview/apply handshake. @@ -171,8 +172,9 @@ delete 20 - `line N: \`replace N..M:\` needs at least one \`+TEXT\` body row. To delete lines, use \`delete N..M\`.` - `line N: \`insert\` needs at least one \`+TEXT\` body row.` - `line N: \`replace block N:\` needs at least one \`+TEXT\` body row. To delete a block, use \`delete N..M\` with the block's line range.` -- Unresolvable `replace block N:` (apply / final-preview path only): +- Unresolvable block anchor (apply / final-preview path only): - `line N: \`replace block X:\` could not resolve a syntactic block beginning on line X. The language may be unsupported, the line may be blank or a closing delimiter, or the block may not parse. Use \`replace X..M:\` with the block's explicit end line instead.` — followed by a blank line and numbered `*`-marked context rows around line X (same shape as the mismatch preview). + - `line N: \`insert after block X:\` could not resolve a syntactic block beginning on line X. The language may be unsupported, the line may be blank or a closing delimiter, or the block may not parse. Use \`insert after M:\` with the block's explicit last line instead.` — same context preview. - Delete with body: - `line N: \`delete N..M\` does not take body rows. Remove the body, or use \`replace N..M:\`.` - `line N: \`delete block N\` does not take body rows. Remove the body, or use \`replace block N:\` to replace the block.` diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index c1b019efa..cf0b3da2a 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -45,6 +45,7 @@ ### Fixed +- Fixed the edit-tool hashline prompt to stop steering agents toward `insert after block N:` on closing delimiter lines; opener-only block anchors now point visible closing-line insertions to plain `insert after M:`. ([#2292](https://github.com/can1357/oh-my-pi/issues/2292)) - Fixed `irc` live message delivery so successfully handed-off messages are no longer enqueued as mailbox mail, so they do not inflate unread `irc` counts - Fixed `irc send` with `await: true` to wait for a fresh reply to the current call instead of consuming previously buffered messages - Fixed main-session chat output to stop duplicating outbound `irc` sends from the main agent as relay cards diff --git a/packages/hashline/CHANGELOG.md b/packages/hashline/CHANGELOG.md index ad05e655d..0e03d3e9f 100644 --- a/packages/hashline/CHANGELOG.md +++ b/packages/hashline/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Fixed the `insert after block N:` prompt guidance so it explicitly says N must be the block opener, not the closing delimiter or last visible line, and points visible closing-line edits to plain `insert after M:`. ([#2292](https://github.com/can1357/oh-my-pi/issues/2292)) + ## [15.11.0] - 2026-06-10 ### Changed diff --git a/packages/hashline/src/prompt.md b/packages/hashline/src/prompt.md index 94caae1e7..fc278e726 100644 --- a/packages/hashline/src/prompt.md +++ b/packages/hashline/src/prompt.md @@ -11,7 +11,7 @@ Every file section starts with `[PATH#TAG]`. `TAG` is the 4-hex snapshot tag fro `delete block N` — delete the whole syntactic block that BEGINS on line N. `insert before N:` — insert the body rows immediately before line N. `insert after N:` — insert the body rows immediately after line N. -`insert after block N:` — insert the body rows after the END of the syntactic block that BEGINS on line N (resolved like `replace block`). +`insert after block N:` — insert the body rows after the END of the syntactic block that BEGINS on line N. Point N at the line that OPENS the construct (the `if`/`function`/`def`/`{`-bearing line), not the closing delimiter / last visible line; if you have the last line, use plain `insert after M:` instead. `insert head:` — insert the body rows at the very start of the file. `insert tail:` — insert the body rows at the very end of the file. Single line: `replace N..N:` / `delete N`. The range is the ORIGINAL lines you touch; body length is irrelevant (replacing 1 line with 10 is still `replace N..N:`). @@ -36,6 +36,7 @@ There is NO other body row kind. NEVER write `-old` or a bare/context line. To k - Keep every range as tight as the change: a range covers ONLY lines whose content actually changes. Never widen it to swallow an unchanged signature, brace, or neighboring statement just to rewrite a few lines inside — change one line with `replace N..N`, not the whole block around it. Tightness means excluding unchanged lines, not being short: a range where every line genuinely changes is correctly long. Tight ranges bound the blast radius of a stale number: a stale one-line range corrupts one line; a stale wide range shreds every line it spans. This applies to hand-counted `replace N..M` ranges; `replace block N` is exempt — tree-sitter fixes the end. - `replace block N` vs `replace N..M`: use `replace block N` to rewrite a WHOLE construct (function / `if` / loop / class body) — tree-sitter resolves its closing line, so a long body can't be mis-counted and a stale end can't clip it mid-block. The edit result echoes the span it matched (`replace block N → resolved lines A-B`); glance at it to confirm you got what you meant. Use `replace N..M` to change specific lines inside a construct. - The resolved span of `replace block N` is EXACTLY the node beginning on line N. A leading decorator, attribute, or doc-comment is a separate node and is NOT included; to take a decorated definition together with its decorator, point N at the FIRST decorator line (Python parses `@dec` + `def` as one block). A leading line-comment that parses as its own node (e.g. Rust `///`) is not captured by any single opener — use `replace N..M` spanning the comment and the construct. +- `insert after block N` follows the same opener-only anchor rule as `replace block N`: N is the first line of the syntactic block, never a line inside it, its closing delimiter, or its last visible line. To append after a closing delimiter you can see, use plain `insert after M:`. - To change lines 2 and 5 while keeping 3–4, issue two hunks (`replace 2..2:` and `replace 5..5:`). Untouched lines are simply absent from every range. - Pure additions use `insert`, never a widened `replace`. If the change only adds lines, `insert before/after` the spot and keep every existing line out of all ranges. Do NOT `replace` a span of keepers and retype them around the new line "to preserve" them — those retyped keepers are exactly what gets silently dropped when one is forgotten. A keeper that never enters your body cannot be lost. `replace` is only for lines whose own text changes. - NEVER use this tool to format code — reordering imports, re-indenting, aligning columns, or any mechanical restyling. That is the project formatter's job; run it instead of hand-editing layout here. @@ -125,6 +126,13 @@ replace 2..4: # RIGHT — touch nothing you keep; the new line is the whole body. insert after 2: + extra = compute(name) + +# WRONG — `insert after block N:` anchored on a closing delimiter / last visible line. RIGHT: plain `insert after M:` +insert after block 3: ++after() +# RIGHT +insert after 3: ++after() diff --git a/packages/hashline/test/block.test.ts b/packages/hashline/test/block.test.ts index 2e404f7ed..d3feb97b7 100644 --- a/packages/hashline/test/block.test.ts +++ b/packages/hashline/test/block.test.ts @@ -13,6 +13,7 @@ import { parsePatch, resolveBlockEdits, } from "@oh-my-pi/hashline"; +import promptText from "../src/prompt.md" with { type: "text" }; const PATH = "x.ts"; @@ -340,6 +341,33 @@ describe("insert after block", () => { expect(() => resolveBlockEdits(edits, "ignored", PATH, () => null)).toThrow("`insert after block 7:`"); }); + it("rejects a closing-delimiter line as an insert-after-block anchor", () => { + const section = Patch.parseSingle(`[${PATH}#1A2B]\ninsert after block 3:\n+ done();`); + const resolver: BlockResolver = ({ line }) => (line === 2 ? { start: 2, end: 3 } : null); + let error: Error | undefined; + try { + section.applyTo(text, resolver); + } catch (err) { + error = err instanceof Error ? err : new Error(String(err)); + } + + expect(error?.message).toContain( + "`insert after block 3:` could not resolve a syntactic block beginning on line 3", + ); + expect(error?.message).toContain("Use `insert after M:` with the block's explicit last line instead"); + expect(error?.message).toContain("*3: }"); + }); + + it("documents the opener-only rule for insert-after-block anchors", () => { + const entry = promptText.split("\n").find(line => line.startsWith("`insert after block N:`")); + + expect(entry).toContain("OPENS"); + expect(entry).toContain("not the closing delimiter / last visible line"); + expect(promptText).toContain( + "# WRONG — `insert after block N:` anchored on a closing delimiter / last visible line. RIGHT: plain `insert after M:`", + ); + }); + it("applyTo inserts the body after the resolved block's last line", () => { const section = Patch.parseSingle(`[${PATH}#1A2B]\ninsert after block 2:\n+ done();`); // stub span [2,3] → body lands after " }" (line 3), before the final "}".