pi-hashline-edit-pro 0.3.3 → 0.3.5

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.
Files changed (2) hide show
  1. package/README.md +35 -31
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -1,8 +1,8 @@
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** — 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. 4-character content hashes over a 64-character URL-safe base64 alphabet give 24 bits of entropy per anchor, so collisions are effectively zero in any realistic file.
4
4
 
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.
5
+ 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 ways: a 4-character hash length and an occurrence-aware discriminator that makes identical content at different positions hash to different values.
6
6
 
7
7
  Every line returned by `read` carries a short content hash. Edits reference those hashes instead of raw text, so the tool can detect stale context and reject outdated changes before they reach the file.
8
8
 
@@ -10,26 +10,30 @@ Every line returned by `read` carries a short content hash. Edits reference thos
10
10
 
11
11
  The original uses 2-character hashes of a 16-character alphabet, with the hash being a pure function of line content. That's 8 bits / 256 buckets, and two byte-identical lines (e.g. repeated `import` statements, repeated `}`) always share a hash because the hash is `xxHash32(content)`.
12
12
 
13
- This fork makes **two** changes that compound:
13
+ This fork makes two changes that compound:
14
14
 
15
- 1. **Bump hash length to 4 characters** of the 64-char URL-safe base64 alphabet 24 bits / 16 777 216 buckets. Birthday-paradox collisions are effectively nullified for any realistic file.
15
+ 1. **Bump hash length to 4 characters** of the 64-char URL-safe base64 alphabet. That gives 24 bits / 16 777 216 buckets. Birthday-paradox collisions are effectively nullified for any realistic file.
16
16
  2. **Make the hash occurrence-aware.** The hash for line N is `xxHash32("C{occurrence}:{content}")` where `occurrence` is the running count of that content string earlier in the file. Symbol-only lines use `"S{lineNumber}"` as the discriminator. Two `import {...}` statements at different positions now hash to different values, so the model can target a specific occurrence without resorting to `offset` + a small `limit` window.
17
17
 
18
18
  ## Installation
19
19
 
20
+ From npm:
21
+
20
22
  ```bash
21
- # From npm
22
23
  pi install npm:pi-hashline-edit-pro
24
+ ```
23
25
 
24
- # From a local checkout
26
+ From a local checkout:
27
+
28
+ ```bash
25
29
  pi install /path/to/pi-hashline-edit-pro
26
30
  ```
27
31
 
28
32
  ## How It Works
29
33
 
30
- ### `read` tagged line output
34
+ ### `read` -- tagged line output
31
35
 
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:
36
+ Text files are returned with a `HASH|content` prefix on every line. The line number is not part of the wire format, only the 4-character hash followed by the `|` separator and the line content. Example output for the source below:
33
37
 
34
38
  ```js
35
39
  function hello() {
@@ -40,23 +44,23 @@ function hello() {
40
44
  would be returned as:
41
45
 
42
46
  ```text
43
- 0qH3function hello() {
44
- szJr console.log("world");
45
- _zlP}
47
+ 0qH3|function hello() {
48
+ szJr| console.log("world");
49
+ _zlP|}
46
50
  ```
47
51
 
48
- - `HASH` 4-character content hash from the URL-safe base64 alphabet `A-Za-z0-9-_` (e.g. `aB3x`).
52
+ - `HASH` is a 4-character content hash from the URL-safe base64 alphabet `A-Za-z0-9-_` (e.g. `aB3x`).
49
53
 
50
54
  Optional parameters:
51
55
 
52
- - `offset` start reading from this line number (1-indexed).
53
- - `limit` maximum number of lines to return.
56
+ - `offset` -- start reading from this line number (1-indexed).
57
+ - `limit` -- maximum number of lines to return.
54
58
 
55
59
  Images (JPEG, PNG, GIF, WebP) are passed through as attachments and do not participate in the hashline protocol. Binary and directory paths are rejected with a descriptive error. Empty files return an advisory suggesting `prepend`/`append` instead of a synthetic anchor.
56
60
 
57
- ### `edit` hash-anchored modifications
61
+ ### `edit` -- hash-anchored modifications
58
62
 
59
- Edits use the `HASHcontent` anchors from `read` output to target lines precisely:
63
+ Edits use the `HASH|content` anchors from `read` output to target lines precisely:
60
64
 
61
65
  ```json
62
66
  {
@@ -73,55 +77,55 @@ Edits use the `HASH│content` anchors from `read` output to target lines precis
73
77
  | `append` | Insert lines after `pos`. Omit `pos` to append at EOF. | `pos` optional, `lines` |
74
78
  | `prepend` | Insert lines before `pos`. Omit `pos` to prepend at BOF. | `pos` optional, `lines` |
75
79
 
76
- - **Request structure validation.** The request envelope (path, edits, returnMode, returnRanges) and individual edit items are validated before any file I/O. Unknown fields, missing required fields, invalid types, and malformed anchors are rejected with `[E_BAD_SHAPE]` or `[E_BAD_OP]`. This catches structural errors early with actionable messages.
80
+ - **Request structure validation.** The request envelope (path, edits, returnMode, returnRanges) and individual edit items are validated before any file I/O. Unknown fields, missing required fields, invalid types, and malformed anchors are rejected with `[E_BAD_SHAPE]` or `[E_BAD_OP]`.
77
81
  - **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`).
78
82
 
79
83
  All edits in a single call validate against the same pre-edit snapshot and apply bottom-up, so line numbers stay consistent across operations.
80
84
 
81
85
  ### Chained edits
82
86
 
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.
87
+ 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.
84
88
 
85
89
  ### Auto-read after write
86
90
 
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:
91
+ 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:
88
92
 
89
- 1. `write` a file result includes hashline anchors
93
+ 1. `write` a file, result includes hashline anchors
90
94
  2. `edit` using those anchors directly
91
95
 
92
96
  For large files (>2000 lines), the auto-read output is truncated with a pagination hint. Use `read` with `offset` to see more.
93
97
 
94
98
  ### Diff for the host
95
99
 
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.
100
+ 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.
97
101
 
98
102
  ## Design Decisions
99
103
 
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.
104
+ - **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.
101
105
  - **No fallback relocation.** Mismatched anchors are never silently relocated to a "close enough" line. This trades convenience for correctness.
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.
106
+ - **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]`. 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.
103
107
  - **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`).
104
108
  - **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.
105
109
  - **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.
106
110
 
107
111
  ## Hashing
108
112
 
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**.
113
+ 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-_`. That's 64 distinct characters, 6 bits per position, 24 bits of entropy per anchor.
110
114
 
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.
115
+ 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.
112
116
 
113
- Hashes are **occurrence-aware**: a discriminator prefix is mixed into the xxHash input before the line content. Symbol-only lines (lone `}`, etc.) use `S{lineNumber}` as the discriminator; content lines use `C{occurrence}` where `occurrence` is the running count of that canonical content earlier in the file. This way:
117
+ Hashes are occurrence-aware: a discriminator prefix is mixed into the xxHash input before the line content. Symbol-only lines (lone `}`, etc.) use `S{lineNumber}` as the discriminator; content lines use `C{occurrence}` where `occurrence` is the running count of that canonical content earlier in the file. This way:
114
118
 
115
119
  - `}` on line 5 and `}` on line 17 hash differently (different `S{...}` prefix).
116
- - `import { foo } from 'bar';` on line 3 and the same string on line 47 hash differently (different `C{...}` prefix 1 vs 2).
120
+ - `import { foo } from 'bar';` on line 3 and the same string on line 47 hash differently (different `C{...}` prefix, 1 vs 2).
117
121
 
118
122
  The runtime always precomputes the full per-line hash array for a file via `computeLineHashes(content)`, then looks up by line number during validation and during `read` / `edit` response formatting. There is no per-line recomputation that could disagree with what the model saw in its last read.
119
123
 
120
124
  `HASH_LENGTH` and `HASH_ALPHABET` are constants at the top of `src/hashline/hash.ts`; bump the length to 5 if you ever need even more entropy.
121
125
 
122
- ### Trade-off: the bare-prefix detector
126
+ ### Bare-prefix detector
123
127
 
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.
128
+ 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.
125
129
 
126
130
  ## Development
127
131
 
@@ -136,8 +140,8 @@ Set `PI_HASHLINE_DEBUG=1` to show an "active" notification at session start.
136
140
 
137
141
  ## Credits
138
142
 
139
- - [RimuruW](https://github.com/RimuruW) original `pi-hashline-edit` and the strict-semantics policy
140
- - [can1357](https://github.com/can1357) original [oh-my-pi](https://github.com/can1357/oh-my-pi) implementation and the hashline concept
143
+ - [RimuruW](https://github.com/RimuruW) -- original `pi-hashline-edit` and the strict-semantics policy
144
+ - [can1357](https://github.com/can1357) -- original [oh-my-pi](https://github.com/can1357/oh-my-pi) implementation and the hashline concept
141
145
 
142
146
  ## License
143
147
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-hashline-edit-pro",
3
- "version": "0.3.3",
3
+ "version": "0.3.5",
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",