diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md
index 68ca658b7..b4e0e051f 100644
--- a/packages/coding-agent/CHANGELOG.md
+++ b/packages/coding-agent/CHANGELOG.md
@@ -25,6 +25,7 @@
- Updated the `read` tool prompt to describe the new elision footer and instruct the model to follow `:raw` (or an explicit line range) when the elided body is actually needed, rather than guessing.
- Fixed plugin extensions failing to load when their `peerDependencies` reference internal `pi-*` packages under any scope other than `@mariozechner` (e.g. `Cannot find module '@earendil-works/pi-tui'` from `@juicesharp/rpiv-ask-user-question`, or `Cannot find module '@oh-my-pi/pi-utils'` from `@oh-my-pi/swarm-extension`). The legacy-pi specifier shim now treats `@mariozechner`, `@earendil-works`, **and** the canonical `@oh-my-pi` itself as aliases for the same set of bundled in-process packages (`pi-agent-core`, `pi-ai`, `pi-coding-agent`, `pi-natives`, `pi-tui`, `pi-utils`), and additionally rewrites the upstream-only `pi-ai/oauth` subpath onto our `pi-ai/utils/oauth` layout. Restored the `Key` runtime helper export on `@oh-my-pi/pi-tui` to match upstream — plugins using `Key.enter` / `Key.ctrl("c")` (e.g. `@plannotator/pi-extension`, `@juicesharp/rpiv-ask-user-question`) no longer fail with `Export named 'Key' not found`. End-to-end verified against `@juicesharp/rpiv-ask-user-question`, `@oh-my-pi/swarm-extension`, and `@plannotator/pi-extension` — each now loads cleanly with all of its tools/commands/handlers registered. Plugins importing any of those scopes are remapped to the omp binary's own copy at load time, so peer deps are no longer dragged in from npm and there is exactly one module instance per package regardless of which scope name the plugin's manifest happened to declare.
- Fixed hashline payload parsing to silently treat truly-blank lines as empty `~`-prefixed payload lines when more payload follows in the same run. The previous behavior broke at the blank ("payload line has no preceding +, <, or = operation.") even though the intent is obvious — the only ambiguity is between in-payload blanks and end-of-section blanks, and a one-line lookahead resolves it: blanks that precede a non-payload op still end the run cleanly as section separators. Recovers the common case of forgetting the leading separator on a blank inserted line without changing how trailing blanks between ops behave.
+- Rewrote the hashline edit prompt examples to use an ASCII-only `TITLE = "Mr"` → `"Mrs"` / `"Dr"` motif instead of the previous `" • "` and `"·"` separators. Some agents had been copying the middle-dot literal characters into real edits as if they were format scaffolding (e.g. emitting payload lines like `~ ·`), since the demo inserts were near-twins of the existing string. The new example keeps every original op shape (single-line replace, multiline replace, insert AFTER/BEFORE, append, delete, blank, plus both anti-patterns) but uses content that is obviously domain-specific and clearly distinct from any payload separator. Pure prompt change; no parser, schema, or runtime behavior is affected.
## [15.0.1] - 2026-05-14
### Breaking Changes
diff --git a/packages/coding-agent/src/prompts/tools/hashline.md b/packages/coding-agent/src/prompts/tools/hashline.md
index d3fc02c20..d037183f7 100644
--- a/packages/coding-agent/src/prompts/tools/hashline.md
+++ b/packages/coding-agent/src/prompts/tools/hashline.md
@@ -66,36 +66,35 @@ When braces bound your edit, you SHOULD prefer these shapes:
-{{hline 1 "const DEF = \"guest\";"}}
-{{hline 2 "export function label(name) {"}}
+{{hline 1 "const TITLE = \"Mr\";"}}
+{{hline 2 "export function greet(name) {"}}
{{hline 3 "\treturn ["}}
-{{hline 4 "\t\tname?.trim() || DEF,"}}
-{{hline 5 "\t\t\" • \","}}
-{{hline 6 "\t].join(\"\");"}}
+{{hline 4 "\t\tTITLE,"}}
+{{hline 5 "\t\tname?.trim() || \"guest\","}}
+{{hline 6 "\t].join(\" \");"}}
{{hline 7 "}"}}
# Replace one line (the payload must re-emit the original indentation)
@@ mod.ts
-= {{hrefr 4}}..{{hrefr 4}}
-{{hsep}} name?.trim().toUpperCase() || DEF,
+= {{hrefr 1}}..{{hrefr 1}}
+{{hsep}}const TITLE = "Mrs";
# Replace a full multiline statement (widen to a self-contained boundary)
@@ mod.ts
= {{hrefr 3}}..{{hrefr 6}}
{{hsep}} return [
-{{hsep}} name?.trim() || DEF,
-{{hsep}} "·",
-{{hsep}} " • ",
-{{hsep}} ].join("");
+{{hsep}} "Mrs",
+{{hsep}} name?.trim() || "guest",
+{{hsep}} ].join(" ");
# Insert AFTER/BEFORE a line
@@ mod.ts
-+ {{hrefr 3}}
-{{hsep}} "·",
++ {{hrefr 4}}
+{{hsep}} "Dr",
< {{hrefr 5}}
-{{hsep}} "·",
+{{hsep}} "Dr",
# Append to file
@@ mod.ts
@@ -112,13 +111,12 @@ When braces bound your edit, you SHOULD prefer these shapes:
-# WRONG — replaces 3 lines just to add one.
+# WRONG — replaces 2 lines just to add one.
@@ mod.ts
-= {{hrefr 1}}..{{hrefr 3}}
-{{hsep}}const DEF = "guest";
+= {{hrefr 1}}..{{hrefr 2}}
+{{hsep}}const TITLE = "Mr";
{{hsep}}const DEBUG = false;
-{{hsep}}export function label(name) {
-{{hsep}} return [
+{{hsep}}export function greet(name) {
# RIGHT — same effect, one-line insert
@@ mod.ts
+ {{hrefr 1}}
@@ -127,17 +125,15 @@ When braces bound your edit, you SHOULD prefer these shapes:
# WRONG — replace from the middle of a larger statement (error-prone)
@@ mod.ts
= {{hrefr 4}}..{{hrefr 5}}
-{{hsep}} name?.trim() || DEF,
-{{hsep}} "·",
-{{hsep}} " • ",
+{{hsep}} "Dr",
+{{hsep}} name?.trim() || "guest",
# RIGHT — widen to the full statement
@@ mod.ts
= {{hrefr 3}}..{{hrefr 6}}
{{hsep}} return [
-{{hsep}} name?.trim() || DEF,
-{{hsep}} "·",
-{{hsep}} " • ",
-{{hsep}} ].join("");
+{{hsep}} "Dr",
+{{hsep}} name?.trim() || "guest",
+{{hsep}} ].join(" ");