pi-hashline-edit-pro 0.3.2 → 0.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # pi-hashline-edit-pro
2
2
 
3
- A [pi-coding-agent](https://github.com/badlogic/pi-mono/tree/main/packages/coding-agent) extension that replaces the built-in `read` and `edit` tools with a hash-anchored line-editing workflow. **Strict semantics** — no silent relocation, no autocorrection, no fuzzy fallback. **Higher-entropy anchors** — `#`-prefixed 4-character content hashes over a 64-character URL-safe base64 alphabet (24 bits / 16 777 216 buckets) so birthday-paradox collisions are effectively zero in any realistic file.
3
+ A [pi-coding-agent](https://github.com/badlogic/pi-mono/tree/main/packages/coding-agent) extension that replaces the built-in `read` and `edit` tools with a hash-anchored line-editing workflow. **Strict semantics** — no silent relocation, no autocorrection, no fuzzy fallback. **Higher-entropy anchors** — 4-character content hashes over a 64-character URL-safe base64 alphabet (24 bits / 16 777 216 buckets) so birthday-paradox collisions are effectively zero in any realistic file.
4
4
 
5
5
  This is a fork of [pi-hashline-edit](https://github.com/RimuruW/pi-hashline-edit) by RimuruW. The strict-semantics policy is unchanged. This fork extends the upstream design in two compounding ways: a 4-character hash length and an occurrence-aware discriminator that makes identical content at different positions hash to different values.
6
6
 
@@ -29,7 +29,7 @@ pi install /path/to/pi-hashline-edit-pro
29
29
 
30
30
  ### `read` — tagged line output
31
31
 
32
- Text files are returned with a `#HASH:content` prefix on every line. The line number is no longer part of the wire format — only the `#`-prefixed 4-character hash followed by the line content. Example output for the source below; the hashes are the real xxHash-derived values for the file content shown:
32
+ Text files are returned with a `HASHcontent` prefix on every line. The line number is no longer part of the wire format — only the 4-character hash followed by the `│` separator and the line content. Example output for the source below; the hashes are the real xxHash-derived values for the file content shown:
33
33
 
34
34
  ```js
35
35
  function hello() {
@@ -40,12 +40,12 @@ function hello() {
40
40
  would be returned as:
41
41
 
42
42
  ```text
43
- #0qH3:function hello() {
44
- #szJr: console.log("world");
45
- #_zlP:}
43
+ 0qH3function hello() {
44
+ szJr console.log("world");
45
+ _zlP}
46
46
  ```
47
47
 
48
- - `HASH` — `#`-prefixed 4-character content hash from the URL-safe base64 alphabet `A-Za-z0-9-_` (e.g. `#aB3x`).
48
+ - `HASH` — 4-character content hash from the URL-safe base64 alphabet `A-Za-z0-9-_` (e.g. `aB3x`).
49
49
 
50
50
  Optional parameters:
51
51
 
@@ -56,13 +56,13 @@ Images (JPEG, PNG, GIF, WebP) are passed through as attachments and do not parti
56
56
 
57
57
  ### `edit` — hash-anchored modifications
58
58
 
59
- Edits use the `#HASH:content` anchors from `read` output to target lines precisely:
59
+ Edits use the `HASHcontent` anchors from `read` output to target lines precisely:
60
60
 
61
61
  ```json
62
62
  {
63
63
  "path": "src/main.ts",
64
64
  "edits": [
65
- { "op": "replace", "start": "#ve7o", "end": "#ve7o", "lines": [" console.log('hashline');"] }
65
+ { "op": "replace", "start": "ve7o", "end": "ve7o", "lines": [" console.log('hashline');"] }
66
66
  ]
67
67
  }
68
68
  ```
@@ -80,32 +80,33 @@ All edits in a single call validate against the same pre-edit snapshot and apply
80
80
 
81
81
  ### Chained edits
82
82
 
83
- After a successful edit, the result text contains an `--- Anchors ---` block with fresh `#HASH:content` references for the changed region. These can be used directly in the next `edit` call on the same file without a full re-read, provided the next edit targets the same or nearby lines. For distant changes, use `read` first.
83
+ After a successful edit, the result text contains an `--- Anchors ---` block with fresh `HASHcontent` references for the changed region. These can be used directly in the next `edit` call on the same file without a full re-read, provided the next edit targets the same or nearby lines. For distant changes, use `read` first.
84
84
 
85
85
  ### Auto-read after write
86
86
 
87
- After a successful `write`, the extension automatically reads the file and appends a `--- Auto-read (hashline anchors) ---` block to the result. This gives the model immediate `#HASH:content` anchors for the newly written file without requiring a separate `read` call. The workflow becomes:
87
+ After a successful `write`, the extension automatically reads the file and appends a `--- Auto-read (hashline anchors) ---` block to the result. This gives the model immediate `HASHcontent` anchors for the newly written file without requiring a separate `read` call. The workflow becomes:
88
88
 
89
89
  1. `write` a file → result includes hashline anchors
90
90
  2. `edit` using those anchors directly
91
91
 
92
92
  For large files (>2000 lines), the auto-read output is truncated with a pagination hint. Use `read` with `offset` to see more.
93
+
93
94
  ### Diff for the host
94
95
 
95
- The post-edit diff (with `+`/`-` markers and new `#HASH:content` anchors) is exposed to the host UI via `details.diff`. It is intentionally **not** in the LLM-visible text — the model only needs the fresh anchors in `text` to chain follow-up edits, and re-emitting the diff would cost extra tokens.
96
+ The post-edit diff (with `+`/`-` markers and new `HASHcontent` anchors) is exposed to the host UI via `details.diff`. It is intentionally **not** in the LLM-visible text — the model only needs the fresh anchors in `text` to chain follow-up edits, and re-emitting the diff would cost extra tokens.
96
97
 
97
98
  ## Design Decisions
98
99
 
99
- - **Stale anchors fail.** A hash mismatch means the file has changed since the last `read`. The error includes fresh `>>> #HASH:content` lines for the affected region; the model copies the HASH portion and retries.
100
+ - **Stale anchors fail.** A hash mismatch means the file has changed since the last `read`. The error includes fresh `>>> HASHcontent` lines for the affected region; the model copies the HASH portion and retries.
100
101
  - **No fallback relocation.** Mismatched anchors are never silently relocated to a "close enough" line. This trades convenience for correctness.
101
- - **Strict patch content.** If `lines` contains `+#HASH:` display prefixes (or `-N ` diff rows), the edit is rejected with `[E_INVALID_PATCH]`. Bare `#HASH:` content (the first 6 chars of a `lines` entry looking like `#` + 4 base64 chars + `:`) is also rejected with `[E_BARE_HASH_PREFIX]` — issue #24. When the suspect's prefix happens to match a real file-line anchor, the error message flags that as strong evidence the model copied an anchor from the read output; the model should rephrase the line (quote it, escape the colon, or use a different identifier shape) and retry.
102
+ - **Strict patch content.** If `lines` contains `+HASH│` display prefixes (or `-N ` diff rows), the edit is rejected with `[E_INVALID_PATCH]`. Bare `HASH│` content (the first 5 chars of a `lines` entry looking like 4 base64 chars + `│`) is also rejected with `[E_BARE_HASH_PREFIX]` — issue #24. When the suspect's prefix happens to match a real file-line anchor, the error message flags that as strong evidence the model copied an anchor from the read output; the model should rephrase the line (quote it, escape the separator, or use a different identifier shape) and retry.
102
103
  - **Legacy dialect rejected.** The native top-level `oldText`/`newText` (and `old_text`/`new_text`) dialect and `op: "replace_text"` are rejected with `[E_LEGACY_SHAPE]`. The error message tells the model to call `read` first and send `{op:"replace", start:"<HASH>", end:"<HASH>", lines:[...]}` (or `append`/`prepend` with `pos`).
103
104
  - **Atomic writes.** Files are written via temp-file-then-rename to avoid corruption from interrupted writes. Symlink chains are resolved so the target file is updated without replacing the symlink. Hard-linked files are updated in place to preserve the shared inode. File permissions are preserved across atomic renames.
104
105
  - **Per-file mutation queue.** Edits queue by the canonical write target, so concurrent edits through different symlink paths still serialize onto the same underlying file.
105
106
 
106
107
  ## Hashing
107
108
 
108
- Hashes are computed with [xxhashjs](https://github.com/pierrec/js-xxhash) (xxHash32), then mapped to a `#`-prefixed 4-character string from the URL-safe base64 alphabet `A-Za-z0-9-_` — 64 distinct characters, 6 bits per position, **24 bits of entropy per anchor**.
109
+ Hashes are computed with [xxhashjs](https://github.com/pierrec/js-xxhash) (xxHash32), then mapped to a 4-character string from the URL-safe base64 alphabet `A-Za-z0-9-_` — 64 distinct characters, 6 bits per position, **24 bits of entropy per anchor**.
109
110
 
110
111
  The alphabet is sized for an LLM consumer. The model tokenizes — it doesn't squint at pixel glyphs — so the human-readability heuristics used by smaller hand-curated alphabets (no G/L/I/O because they look like digits, no vowels so the hash doesn't accidentally spell a word, no hex digits so it can't be confused with `0xFF`) don't apply. The full 64 chars give maximum entropy per character, with case and digits included.
111
112
 
@@ -120,7 +121,7 @@ The runtime always precomputes the full per-line hash array for a file via `comp
120
121
 
121
122
  ### Trade-off: the bare-prefix detector
122
123
 
123
- With the `#` prefix format, the bare-prefix detector regex `^\s*#[A-Za-z0-9_-]{4}:` is highly specific — it only matches lines starting with `#` followed by exactly 4 base64 chars and `:`. This eliminates false positives from common code patterns like `init:`, `data:`, `else:`, etc. that plagued the old 4-char-only detector. The detector rejects edit lines matching this pattern with `[E_BARE_HASH_PREFIX]` to prevent the model from accidentally pasting hash anchors into file content.
124
+ With the `│` delimiter format, the bare-prefix detector regex `^\s*[A-Za-z0-9_-]{4}│` is highly specific — it only matches lines starting with exactly 4 base64 chars and `│`. This eliminates false positives from common code patterns like `init:`, `data:`, `else:`, etc. The detector rejects edit lines matching this pattern with `[E_BARE_HASH_PREFIX]` to prevent the model from accidentally pasting hash anchors into file content.
124
125
 
125
126
  ## Development
126
127
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-hashline-edit-pro",
3
- "version": "0.3.2",
3
+ "version": "0.3.3",
4
4
  "description": "Strict hashline read/edit tool override for pi-coding-agent with hash-anchored edits (4-char, 24-bit)",
5
5
  "repository": {
6
6
  "type": "git",
@@ -1 +1 @@
1
- Edit a text file via #HASH anchors from read
1
+ Edit a text file via HASH anchors from read
package/prompts/edit.md CHANGED
@@ -1,8 +1,8 @@
1
- Patch a text file using `#HASH` anchors copied verbatim from `read`.
1
+ Patch a text file using `HASH` anchors copied verbatim from `read`.
2
2
 
3
3
  Put all operations on one file in a single `edit` call. Stack every region into the `edits` array, even when they are far apart. Anchors within one call must all come from the same pre-edit read; the runtime applies them atomically against that one snapshot, so you do not adjust anchors for line-number shifts between edits in the same call.
4
4
 
5
- Anchors are `#` + 4 characters (e.g. `#aB3x`), alphabet `A-Za-z0-9-_`. The wire format for `start`/`end`/`pos` is the anchor only — no line number, no trailing content, no line content.
5
+ Anchors are 4 characters (e.g. `aB3x`), alphabet `A-Za-z0-9-_`. The wire format for `start`/`end`/`pos` is the anchor only — no line number, no trailing content, no line content.
6
6
 
7
7
  Ops:
8
8
  - `replace` — replace the inclusive range `start`..`end`. Both anchors are required. Single line: `start = end`. To delete a range, use `lines: []`. Do NOT use the `pos` field on `replace`; use `start`.
@@ -14,14 +14,14 @@ Examples:
14
14
  1. Single line replace:
15
15
  ```json
16
16
  { "path": "src/main.ts", "edits": [
17
- { "op": "replace", "start": "#MQXV", "end": "#MQXV", "lines": ["const x = 1;"] }
17
+ { "op": "replace", "start": "MQXV", "end": "MQXV", "lines": ["const x = 1;"] }
18
18
  ] }
19
19
  ```
20
20
 
21
21
  2. Range replace (3 lines → 3 new lines):
22
22
  ```json
23
23
  { "path": "src/main.ts", "edits": [
24
- { "op": "replace", "start": "#ZPMQ", "end": "#VRWS", "lines": [
24
+ { "op": "replace", "start": "ZPMQ", "end": "VRWS", "lines": [
25
25
  "function greet(name) {",
26
26
  " return `Hello, ${name}`;",
27
27
  "}"
@@ -32,27 +32,27 @@ Examples:
32
32
  3. Multiple regions in one call (delete two non-adjacent ranges, insert before a third anchor):
33
33
  ```json
34
34
  { "path": "src/server.ts", "edits": [
35
- { "op": "replace", "start": "#aB3x", "end": "#xY7q", "lines": [] },
36
- { "op": "replace", "start": "#MQXV", "end": "#ZPMQ", "lines": [] },
37
- { "op": "prepend", "pos": "#VRWS", "lines": ["// inserted before VRWS"] }
35
+ { "op": "replace", "start": "aB3x", "end": "xY7q", "lines": [] },
36
+ { "op": "replace", "start": "MQXV", "end": "ZPMQ", "lines": [] },
37
+ { "op": "prepend", "pos": "VRWS", "lines": ["// inserted before VRWS"] }
38
38
  ] }
39
39
  ```
40
40
 
41
41
  Rules:
42
42
  - `replace` requires both `start` and `end`. A single-line replace is `start=X, end=X`. To replace more than one line, set `end` to a different line's anchor.
43
- - `start`, `end`, `pos` are HASH anchors only (e.g. `#aB3x`). Other forms are rejected with `[E_BAD_REF]`.
44
- - `lines` is literal file content. No `#HASH:` prefix, no leading `+`/`-` (those are read/diff metadata, not file content). Lines starting with `#` + 4 base64 chars + `:` are checked; if detected, the edit is rejected with `[E_BARE_HASH_PREFIX]`. For `.py` files, this becomes a `[W_BARE_HASH_PREFIX]` warning instead (Python syntax like `else:`, `except:` triggers the detector).
43
+ - `start`, `end`, `pos` are HASH anchors only (e.g. `aB3x`). Other forms are rejected with `[E_BAD_REF]`.
44
+ - `lines` is literal file content. No `HASH│` prefix, no leading `+`/`-` (those are read/diff metadata, not file content). Lines starting with 4 base64 chars + `│` are checked; if detected, the edit is rejected with `[E_BARE_HASH_PREFIX]`. For `.py` files, this becomes a `[W_BARE_HASH_PREFIX]` warning instead (Python syntax like `else:`, `except:` triggers the detector).
45
45
  - Copy anchors from the most recent `read` of the file. Do not guess or construct them.
46
46
  - All edits in one call must be non-conflicting. The runtime rejects with `[E_EDIT_CONFLICT]` if: two `replace` ranges overlap; two `append`/`prepend` target the same insertion boundary (e.g. two EOF appends on a newline-terminated file); or an `append`/`prepend` falls inside a `replace` range in the same call. Fix: merge into one, use different boundaries, or split into a follow-up `edit` call.
47
47
  - If `lines` matches the current content byte-for-byte, the edit is classified as `Classification: noop` (file unchanged, not an error).
48
48
 
49
- On success (`changed` mode, default), the response text contains an `--- Anchors ---` block with fresh `#HASH:content` for the changed region (2 lines of context, capped at ~12 lines / 50 KB). Use those for nearby follow-up edits instead of re-reading. If the response says `Anchors omitted; use read for subsequent edits`, the region was too large — call `read` again. For distant follow-ups, or on any error, call `read` again. `full` and `ranges` modes put previews in `details`; the model only needs what's in the text.
49
+ On success (`changed` mode, default), the response text contains an `--- Anchors ---` block with fresh `HASHcontent` for the changed region (2 lines of context, capped at ~12 lines / 50 KB). Use those for nearby follow-up edits instead of re-reading. If the response says `Anchors omitted; use read for subsequent edits`, the region was too large — call `read` again. For distant follow-ups, or on any error, call `read` again. `full` and `ranges` modes put previews in `details`; the model only needs what's in the text.
50
50
 
51
- Errors are text starting with a bracketed code (e.g. `[E_BAD_SHAPE]`, `[E_STALE_ANCHOR]`, `[E_BAD_OP]`, `[E_INVALID_PATCH]`, `[E_LEGACY_SHAPE]`, `[E_EDIT_CONFLICT]`, `[E_BAD_REF]`, `[E_AMBIGUOUS_ANCHOR]`, `[E_BARE_HASH_PREFIX]`, `[E_WOULD_EMPTY]`). The message tells you what to retry; stale-anchor errors include `>>> #HASH:content` lines, ready to copy.
51
+ Errors are text starting with a bracketed code (e.g. `[E_BAD_SHAPE]`, `[E_STALE_ANCHOR]`, `[E_BAD_OP]`, `[E_INVALID_PATCH]`, `[E_LEGACY_SHAPE]`, `[E_EDIT_CONFLICT]`, `[E_BAD_REF]`, `[E_AMBIGUOUS_ANCHOR]`, `[E_BARE_HASH_PREFIX]`, `[E_WOULD_EMPTY]`). The message tells you what to retry; stale-anchor errors include `>>> HASHcontent` lines, ready to copy.
52
52
 
53
53
  The legacy `oldText`/`newText` shape (top-level or as `op: "replace_text"`) is rejected with `[E_LEGACY_SHAPE]`. Use hash-anchored edits instead.
54
54
 
55
55
  Auto-read after write:
56
- - After a successful `write`, the result includes a `--- Auto-read (hashline anchors) ---` block with `#HASH:content` for the written file.
56
+ - After a successful `write`, the result includes a `--- Auto-read (hashline anchors) ---` block with `HASHcontent` for the written file.
57
57
  - Use those anchors directly for `edit` calls without a separate `read`.
58
58
  - This enables a seamless write → edit workflow with no extra tool calls.
@@ -1,3 +1,3 @@
1
1
  - Use read before edit when you do not have current HASH anchors for the file.
2
- - Copy exactly the HASH (the `#` + 4 characters before the `:`); never include the `:` or line content in `pos`/`end`.
3
- - A HASH always starts with `#`; the body may contain `-` as a normal alphabet character.
2
+ - Copy exactly the HASH (the 4 characters before the `│`); never include the `│` or line content in `pos`/`end`.
3
+ - A HASH may contain `-` as a normal alphabet character.
@@ -1 +1 @@
1
- Read a text file with #HASH:content anchors for edit (copy the #HASH into `start`/`end`/`pos`)
1
+ Read a text file with HASHcontent anchors for edit (copy the HASH into `start`/`end`/`pos`)
package/prompts/read.md CHANGED
@@ -1,12 +1,12 @@
1
- Read a text file. Each line is returned as `#HASH:content`. The HASH starts with `#` followed by 4 base64 characters before the first `:`; the content after is the line verbatim. Pass the HASH (e.g. `#aB3x`) into `edit`'s `start`/`end` (for `replace`) or `pos` (for `append`/`prepend`) — never include the line content.
1
+ Read a text file. Each line is returned as `HASHcontent`. The HASH is 4 base64 characters; the content after the `│` separator is the line verbatim. Pass the HASH (e.g. `aB3x`) into `edit`'s `start`/`end` (for `replace`) or `pos` (for `append`/`prepend`) — never include the line content.
2
2
 
3
3
  HASH shape:
4
- - 5 characters total: `#` prefix + 4 characters from the URL-safe base64 alphabet `A-Za-z0-9-_` (e.g. `#aB3x`, `#4yN-`, `#-qkl`).
4
+ - 4 characters from the URL-safe base64 alphabet `A-Za-z0-9-_` (e.g. `aB3x`, `4yN-`, `-qkl`).
5
5
  - The line number is not part of the wire format. Anchor by HASH, never by reading a line number off the rendered output.
6
6
 
7
7
  HASH → edit:
8
- - Copy the full 5-character HASH (including the `#` prefix). Use that HASH as `start` or `end` (for `replace`) or `pos` (for `append`/`prepend`) in the next `edit` call.
9
- - Do not include the `:`, the line content, or surrounding whitespace. The wire format for `start`/`end`/`pos` is the HASH only.
8
+ - Copy the full 4-character HASH. Use that HASH as `start` or `end` (for `replace`) or `pos` (for `append`/`prepend`) in the next `edit` call.
9
+ - Do not include the `│`, the line content, or surrounding whitespace. The wire format for `start`/`end`/`pos` is the HASH only.
10
10
 
11
11
  Pagination:
12
12
  - Large files return a truncated preview with a `nextOffset` line. Call `read` again with `offset=nextOffset` to continue.
@@ -14,15 +14,15 @@ Pagination:
14
14
  - Empty files return an advisory suggesting `prepend`/`append` instead of a synthetic anchor.
15
15
 
16
16
  Error recovery:
17
- - `[E_STALE_ANCHOR]` — the file changed since your last read. The error includes fresh `>>> #HASH:content` lines; copy the HASH portion (the `#` + 4 chars before `:`) and retry.
18
- - `[E_BAD_REF]` — malformed HASH. Re-read and try again with a valid HASH anchor (e.g. `#aB3x`).
17
+ - `[E_STALE_ANCHOR]` — the file changed since your last read. The error includes fresh `>>> HASHcontent` lines; copy the HASH portion (the 4 chars before `│`) and retry.
18
+ - `[E_BAD_REF]` — malformed HASH. Re-read and try again with a valid HASH anchor (e.g. `aB3x`).
19
19
 
20
20
  File kinds:
21
- - Text files are returned as `#HASH:content` lines.
21
+ - Text files are returned as `HASHcontent` lines.
22
22
  - Images (JPEG, PNG, GIF, WebP) are returned as visual attachments; the HASH-line protocol does not apply.
23
23
  - Binary files and directories are rejected with a descriptive error.
24
24
 
25
25
  Auto-read after write:
26
- - After a successful `write`, the result includes a `--- Auto-read (hashline anchors) ---` block with `#HASH:content` for the written file.
26
+ - After a successful `write`, the result includes a `--- Auto-read (hashline anchors) ---` block with `HASHcontent` for the written file.
27
27
  - Use those anchors directly for `edit` calls without a separate `read`.
28
28
  - The auto-read output follows the same format and rules as `read` output.
package/src/edit-diff.ts CHANGED
@@ -36,9 +36,9 @@ function formatDiffPreviewLine(
36
36
  hash: string | undefined,
37
37
  ): string {
38
38
  if (hash === undefined) {
39
- return `${prefix}${" ".repeat(ANCHOR_LENGTH)}:${line}`;
39
+ return `${prefix}${" ".repeat(ANCHOR_LENGTH)}│${line}`;
40
40
  }
41
- return `${prefix}${hash}:${line}`;
41
+ return `${prefix}${hash}│${line}`;
42
42
  }
43
43
 
44
44
  export function generateDiffString(
@@ -170,7 +170,7 @@ function truncateOutlineEntry(text: string, max = 88): string {
170
170
  function collectOutlineEntries(previewText: string): string[] {
171
171
  const structural: string[] = [];
172
172
  for (const line of previewText.split("\n")) {
173
- const match = line.match(/^\s*#([A-Za-z0-9_\-]{4}):(.*)$/);
173
+ const match = line.match(/^\s*([A-Za-z0-9_\-]{4})(.*)$/);
174
174
  if (!match) continue;
175
175
  const content = match[2]!.trim();
176
176
  if (content.length === 0) continue;
@@ -226,7 +226,7 @@ function formatRequestedRangePreviews(
226
226
  },
227
227
  precomputedHashes,
228
228
  );
229
- const hasReturnedLines = /^\s*#[A-Za-z0-9_\-]{4}:/m.test(preview.text);
229
+ const hasReturnedLines = /^\s*[A-Za-z0-9_\-]{4}│/m.test(preview.text);
230
230
  const actualEnd = hasReturnedLines
231
231
  ? preview.nextOffset !== undefined
232
232
  ? preview.nextOffset - 1
package/src/edit.ts CHANGED
@@ -61,7 +61,7 @@ function stringEnumSchema<const Values extends readonly string[]>(
61
61
 
62
62
  const hashlineEditLinesSchema = Type.Array(Type.String(), {
63
63
  description:
64
- "replacement content, one array entry per line, no HASH: prefix",
64
+ "replacement content, one array entry per line, no HASH| prefix",
65
65
  });
66
66
 
67
67
  const returnRangeSchema = Type.Object(
@@ -92,19 +92,19 @@ const hashlineEditItemSchema = Type.Object(
92
92
  start: Type.Optional(
93
93
  Type.String({
94
94
  description:
95
- "required range-start anchor for op \"replace\" (hash anchor like \"#aB3x\" copied from read output); no content may follow the anchor",
95
+ "required range-start anchor for op \"replace\" (hash anchor like \"aB3x\" copied from read output); no content may follow the anchor",
96
96
  }),
97
97
  ),
98
98
  end: Type.Optional(
99
99
  Type.String({
100
100
  description:
101
- "required range-end anchor for op \"replace\" (hash anchor like \"#aB3x\"). To replace a single line, set start = end = the line's anchor",
101
+ "required range-end anchor for op \"replace\" (hash anchor like \"aB3x\"). To replace a single line, set start = end = the line's anchor",
102
102
  }),
103
103
  ),
104
104
  pos: Type.Optional(
105
105
  Type.String({
106
106
  description:
107
- "anchor for op \"append\" or \"prepend\" (hash anchor like \"#aB3x\"). Omit for file-boundary insertion (EOF/BOF).",
107
+ "anchor for op \"append\" or \"prepend\" (hash anchor like \"aB3x\"). Omit for file-boundary insertion (EOF/BOF).",
108
108
  }),
109
109
  ),
110
110
  lines: Type.Optional(hashlineEditLinesSchema),
@@ -510,7 +510,7 @@ export function formatHashlineRegion(
510
510
  );
511
511
  }
512
512
  return lines
513
- .map((line, index) => `${hashes[index]}:${line}`)
513
+ .map((line, index) => `${hashes[index]}│${line}`)
514
514
  .join("\n");
515
515
  }
516
516
 
@@ -4,9 +4,9 @@ import * as XXH from "xxhashjs";
4
4
 
5
5
  export const HASH_LENGTH = 4;
6
6
 
7
- export const HASH_PREFIX = "#";
7
+ export const HASH_PREFIX = "";
8
8
 
9
- export const ANCHOR_LENGTH = HASH_PREFIX.length + HASH_LENGTH;
9
+ export const ANCHOR_LENGTH = HASH_LENGTH;
10
10
 
11
11
  const HASH_ALPHABET =
12
12
  "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
@@ -15,7 +15,6 @@ const HASH_ALPHABET_MASK = (1 << HASH_ALPHABET_BITS) - 1;
15
15
  const HASH_ALPHABET_REGEX_SAFE = HASH_ALPHABET.replace(/-/g, "\\-");
16
16
  const HASH_ALPHABET_RE = new RegExp(`^[${HASH_ALPHABET_REGEX_SAFE}]+$`);
17
17
  export const HASH_CHARS_CLASS = `${HASH_PREFIX}[${HASH_ALPHABET_REGEX_SAFE}]{${HASH_LENGTH}}`;
18
-
19
18
  function hashToString(h: number): string {
20
19
  const totalBits = HASH_LENGTH * HASH_ALPHABET_BITS;
21
20
  const shift = 32 - totalBits;
@@ -28,18 +27,18 @@ function hashToString(h: number): string {
28
27
  HASH_ALPHABET_MASK
29
28
  ]!;
30
29
  }
31
- return HASH_PREFIX + out;
30
+ return out;
32
31
  }
33
32
 
34
33
  export const HASHLINE_PREFIX_RE = new RegExp(
35
- `^\\s*(?:>>>|>>)?\\s*${HASH_CHARS_CLASS}:`,
34
+ `^\\s*(?:>>>|>>)?\\s*${HASH_CHARS_CLASS}│`,
36
35
  );
37
36
  export const HASHLINE_PREFIX_PLUS_RE = new RegExp(
38
- `^\\+\\s*${HASH_CHARS_CLASS}:`,
37
+ `^\\+\\s*${HASH_CHARS_CLASS}│`,
39
38
  );
40
39
  export const DIFF_MINUS_RE = /^-\s*\d+\s{4}/;
41
40
 
42
- export const HASHLINE_BARE_PREFIX_RE = new RegExp(`^\\s*(${HASH_CHARS_CLASS}):`);
41
+ export const HASHLINE_BARE_PREFIX_RE = new RegExp(`^\\s*(${HASH_CHARS_CLASS})│`);
43
42
 
44
43
  const RE_SIGNIFICANT = /[\p{L}\p{N}]/u;
45
44
 
@@ -1,7 +1,6 @@
1
1
 
2
2
  import {
3
3
  ANCHOR_LENGTH,
4
- HASH_PREFIX,
5
4
  HASH_ALPHABET_RE,
6
5
  HASH_CHARS_CLASS,
7
6
  HASHLINE_PREFIX_PLUS_RE,
@@ -16,14 +15,14 @@ function diagnoseHashRef(ref: string): string {
16
15
  const trimmed = ref.trim();
17
16
 
18
17
  if (!trimmed.length) {
19
- return `[E_BAD_REF] Invalid anchor. Expected a hash anchor like "#aB3x" (prefix "#" + 4 base64 chars).`;
18
+ return `[E_BAD_REF] Invalid anchor. Expected a 4-character base64 anchor (e.g. \"aB3x\").`;
20
19
  }
21
20
 
22
- if (/^\d+\s*#/.test(trimmed)) {
23
- return `[E_BAD_REF] Invalid anchor. Use the hash alone (e.g. "#aB3x") — no line numbers or trailing content.`;
21
+ if (/^\d+/.test(trimmed)) {
22
+ return `[E_BAD_REF] Invalid anchor. Use the hash alone (e.g. \"aB3x\") — no line numbers or trailing content.`;
24
23
  }
25
24
 
26
- return `[E_BAD_REF] Invalid anchor "${trimmed}". Expected a hash anchor like "#aB3x".`;
25
+ return `[E_BAD_REF] Invalid anchor \"${trimmed}\". Expected a 4-character base64 anchor (e.g. \"aB3x\").`;
27
26
  }
28
27
 
29
28
  function parseAnchorRef(ref: string): Anchor {
@@ -31,8 +30,7 @@ function parseAnchorRef(ref: string): Anchor {
31
30
 
32
31
  if (
33
32
  trimmed.length === ANCHOR_LENGTH &&
34
- trimmed.startsWith(HASH_PREFIX) &&
35
- HASH_ALPHABET_RE.test(trimmed.slice(HASH_PREFIX.length))
33
+ HASH_ALPHABET_RE.test(trimmed)
36
34
  ) {
37
35
  return { hash: trimmed };
38
36
  }
@@ -51,7 +49,7 @@ function assertNoDisplayPrefixes(lines: string[]): void {
51
49
  DIFF_MINUS_RE.test(line)
52
50
  ) {
53
51
  throw new Error(
54
- `[E_INVALID_PATCH] "lines" must contain literal file content, not HASH: or diff prefixes. Offending line: ${JSON.stringify(line)}`
52
+ `[E_INVALID_PATCH] \"lines\" must contain literal file content, not HASH| or diff prefixes. Offending line: ${JSON.stringify(line)}`
55
53
  );
56
54
  }
57
55
  }
@@ -107,12 +107,12 @@ export function formatMismatchError(
107
107
  const lines = sample
108
108
  .map((line) => {
109
109
  const content = fileLines[line - 1] ?? "";
110
- return ` ${line}: ${fileHashes[line - 1]}:${content}`;
110
+ return ` ${line}: ${fileHashes[line - 1]}│${content}`;
111
111
  })
112
112
  .join("\n");
113
- out.push(
114
- ` Hash "${m.ref.hash}" matches lines ${sample.join(", ")}${more}.\n${lines}`,
115
- );
113
+ out.push(
114
+ ` Hash "${m.ref.hash}" matches lines ${sample.join(", ")}${more}.\n${lines}`,
115
+ );
116
116
  }
117
117
  }
118
118
 
@@ -120,7 +120,7 @@ export function formatMismatchError(
120
120
  out.push("Current state (first lines):");
121
121
  const sampleSize = Math.min(fileLines.length, 5);
122
122
  for (let i = 0; i < sampleSize; i++) {
123
- out.push(`>>> ${fileHashes[i]}:${fileLines[i]}`);
123
+ out.push(`>>> ${fileHashes[i]}│${fileLines[i]}`);
124
124
  }
125
125
  if (fileLines.length > sampleSize) {
126
126
  out.push(`... ${fileLines.length - sampleSize} more.`);
@@ -284,7 +284,7 @@ export function assertNoBareHashPrefixLines(
284
284
  const fileHashSet = new Set(fileHashes);
285
285
  const matched = suspects.filter((s) => fileHashSet.has(s.hash));
286
286
  const matchedCount = matched.length;
287
- const exampleLine = `${suspects[0]!.hash}:${suspects[0]!.line}`;
287
+ const exampleLine = `${suspects[0]!.hash}│${suspects[0]!.line}`;
288
288
 
289
289
  if (isPython) {
290
290
  const hint = matchedCount > 0
@@ -299,7 +299,7 @@ export function assertNoBareHashPrefixLines(
299
299
  : `${matchedCount} match file line hashes — likely a copied hash.`;
300
300
 
301
301
  throw new Error(
302
- `[E_BARE_HASH_PREFIX] ${suspects.length} edit line(s) start with a hash-like prefix (e.g. ${JSON.stringify(exampleLine)}). ${linesHint} Use literal file content in "lines" — never paste #HASH:content from read output.`
302
+ `[E_BARE_HASH_PREFIX] ${suspects.length} edit line(s) start with a hash-like prefix (e.g. ${JSON.stringify(exampleLine)}). ${linesHint} Use literal file content in \"lines\" — never paste HASHcontent from read output.`
303
303
  );
304
304
  }
305
305
 
@@ -344,7 +344,7 @@ export function validateAnchorEdits(
344
344
  case "replace": {
345
345
  const startResolved = tryResolve(edit.start);
346
346
  const endResolved = tryResolve(edit.end);
347
- if (!startResolved || !endResolved) {
347
+ if (!startResolved || !endResolved) {
348
348
  continue;
349
349
  }
350
350
  if (startResolved.line > endResolved.line) {