fix(edit): taught markdown bullet escaping

Clarified hashline minus-row errors and model-facing prompt examples so Markdown list rows use the + body-row prefix instead of triggering write fallbacks.

Fixes #4179
This commit is contained in:
roboomp
2026-07-01 22:15:53 +00:00
parent 0823892295
commit cf5150568f
6 changed files with 23 additions and 9 deletions
+2 -2
View File
@@ -43,7 +43,7 @@ Patch language inside `input`:
- Every body row is `+TEXT`; `+` alone adds a blank line.
- `DEL` never has body rows.
- There is no repeat row kind. To keep a line, leave it out of every range; split edits into multiple hunks when needed.
- `-` rows are invalid. Literal text beginning with `-` or `+` must be written as `+-text` / `++text`.
- `-` rows are invalid. Literal Markdown bullets or text beginning with `-` / `+` must be written as `+- item` / `++ item`.
Anchors come from `read`/`grep` output. `read` emits a `[PATH#TAG]` header from the session snapshot store and lines as `LINE:TEXT`; copy the header into the edit section and copy only the line number into hunk headers.
@@ -165,7 +165,7 @@ DEL 20
- Stray payload line:
- `line N: payload line has no preceding hunk header. Use \`SWAP N.=M:\`, \`DEL N.=M\`, or \`INS.PRE|POST|HEAD|TAIL:\` above the body. Got "...".`
- Minus row:
- ``line N: `-` rows are not valid; the range already names the lines being changed. For a literal `-` line, write `+-…`.``
- ``line N: `-` rows are not valid; the range already names the lines being changed. For Markdown bullets or other literal `-` lines, prefix the literal row with `+`: `+- item`.``
- Empty body-bearing hunk:
- `line N: \`INS\` needs at least one \`+TEXT\` body row.`
- `line N: \`SWAP.BLK N:\` needs at least one \`+TEXT\` body row. To delete a block, use \`DEL.BLK N\`.`
+1
View File
@@ -21,6 +21,7 @@
- Fixed RPC mode abort_bash being blocked by running bash commands by dispatching bash in the background.
- Fixed task.maxConcurrency and task.maxRecursionDepth limits being bypassed by sub-spawn paths, ensuring limits are dynamically resized and respected.
- Fixed the edit tool inflating session files by pruning extremely large file snapshots from tool-result details.
- Fixed edit-tool Markdown list guidance so hashline parser errors and the model-facing prompt teach `+- item` escaping instead of steering agents toward full-file `write` fallbacks. ([#4179](https://github.com/can1357/oh-my-pi/issues/4179))
- Fixed workstation OS detection rendering "Kernel: unknown" on macOS 15+.
- Fixed /copy code and /copy cmd commands being treated as normal prompts instead of copying the requested blocks.
- Fixed interactive bash status line not updating after directory changes (cd).
+1
View File
@@ -6,6 +6,7 @@
- Fixed an issue where snapshot tag collisions could cause line-anchored edits to be incorrectly applied to unrelated content.
- Fixed tracking of edit anchors when earlier in-session insertions or deletions shift unchanged target lines.
- Fixed hashline edit guidance for Markdown list rows by teaching `+- item` escaping in the model prompt and minus-row parser error. ([#4179](https://github.com/can1357/oh-my-pi/issues/4179))
## [16.2.8] - 2026-06-30
+1 -1
View File
@@ -54,7 +54,7 @@ export const BARE_BODY_AUTO_PIPED_WARNING =
/** Unified-diff-style `-` row in a hunk body. */
export const MINUS_ROW_REJECTED =
"`-` rows are not valid; the range already names the lines being changed. For a literal `-` line, write `+-…`.";
"`-` rows are not valid; the range already names the lines being changed. For Markdown bullets or other literal `-` lines, prefix the literal row with `+`: `+- item`.";
/** Replace hunk with no body. */
export const EMPTY_REPLACE = `\`SWAP N${HL_RANGE_SEP}M:\` needs at least one \`+TEXT\` body row. To delete lines, use \`DEL N${HL_RANGE_SEP}M\`.`;
+10 -2
View File
@@ -19,7 +19,7 @@ Single line: `SWAP N.=N:` / `DEL N`. The range is the ORIGINAL lines you touch;
</ops>
<body-rows>
Body rows appear only under a `:` header. Every body row is `+TEXT` — add a literal line `TEXT`, verbatim (leading whitespace kept); `+` alone adds a blank line. No other row kind. NEVER write `-old` or a bare/context line. To keep a line, leave it out of every range. To insert a literal line starting with `-` or `+`, prefix it: `+-x`, `++x`.
Body rows appear only under a `:` header. Every body row is `+TEXT` — add a literal line `TEXT`, verbatim (leading whitespace kept); `+` alone adds a blank line. No other row kind. NEVER write `-old` or a bare/context line. To keep a line, leave it out of every range. Literal lines starting with `-`/`+` still need the body prefix: Markdown `- item` → `+- item`, `+ item` → `++ item`.
</body-rows>
<rules>
@@ -103,6 +103,14 @@ INS.TAIL:
+greet("everyone")
```
Insert Markdown bullets — the leading `+` is the body-row marker; the file receives `- task`:
```
[PLAN.md#A1B2]
INS.POST 2:
+- task
+ - nested task
```
Replace the whole `greet` function block — `SWAP.BLK 1:` resolves lines 1–3 (the `def` header through `print(msg)`); line 4 is a separate statement and stays:
```
[greet.py#A1B2]
@@ -160,5 +168,5 @@ INS.POST 3:
If you remember nothing else:
1. RE-GROUND AFTER EVERY EDIT. Every apply mints a fresh `#TAG` and renumbers — take the next edit's numbers from the edit response or a fresh `read`. Stale tag or surprise? STOP, re-`read`.
2. RANGES ARE TIGHT. Cover only lines that change; a stale wide range shreds everything it spans. Whole construct → `SWAP.BLK N`.
3. THE BODY IS THE FINAL CONTENT. Only `+TEXT` rows; never `-old`/context lines. The range does the deleting.
3. THE BODY IS THE FINAL CONTENT. Every body row starts with `+`; Markdown bullets use `+- item`, not `- item`.
</critical>
+8 -4
View File
@@ -166,12 +166,16 @@ describe("hashline body contracts", () => {
expect(applyEdits(FILE, result.edits).text).toBe('a\n1: "one",\n2: "two",\nd\ne');
});
it("rejects `-` body rows with a teaching error", () => {
expect(() => parsePatch("SWAP 2.=2:\n-old\n+new")).toThrow(/`-` rows are not valid/);
it("rejects `-` body rows with Markdown bullet escape guidance", () => {
expect(() => parsePatch("SWAP 2.=2:\n-old\n+new")).toThrow(
/Markdown bullets or other literal `-` lines.*`\+- item`/,
);
});
it("allows literal text that begins with `-` or `+` when prefixed with `+`", () => {
expect(applyPatch(FILE, "SWAP 2.=2:\n+-literal\n++plus")).toBe("a\n-literal\n+plus\nc\nd\ne");
it("allows literal Markdown bullets and plus-prefixed text when prefixed with `+`", () => {
expect(applyPatch(FILE, "SWAP 2.=2:\n+- item\n+ - nested\n++plus")).toBe(
"a\n- item\n - nested\n+plus\nc\nd\ne",
);
});
it("treats empty replace as delete and still rejects empty insert", () => {