hashline — Edit by Content Anchor, Not Fragile Line Numbers
TL;DR
After installing @piex-dev/hashline, default edits go from "guessing line numbers" to "editing against the version you read": fewer silent mistakes, at the cost of occasional re-reads. A tax worth paying.
Overview
The edit tool of an AI coding assistant has a fundamental conflict: the model describes where to change with line numbers, but line numbers are deeply unstable in the real world. A user edits the file while the model thinks; a prior edit shifted lines; an unread region gets edited against an "assumed" line number — all cause silent, wrong writes.
@piex-dev/hashline overrides Pi's built-in edit with content anchors: tag on read, verify on write, reject lines never shown.
How it works
On read, a [path#TAG] is injected and seen-lines recorded; an edit must carry the same tag. The whole-file 4-hex tag acts like an optimistic lock: any external change invalidates it and forces a re-read. Stricter than per-line hashing, but it buys tree-sitter block ops and boundary repair.
[src/main.ts#A3F2]
SWAP 2.=2:
+const x = 2;
Phase 1 guards: 3 consecutive byte-identical noops → [E_NOOP_LOOP]; re-sending the same payload after success with an unchanged file → [E_DUPLICATE_EDIT]; CRLF / code-fence / extra-blank-line dialects normalized.
Usage
Install
pi install npm:@piex-dev/hashline
Active immediately: hashline overrides Pi's built-in edit tool and hooks read results. No extra switch, no config file.
Source: extensions/hashline
Configuration
Works out of the box, no required config. For local development, install runtime deps first:
cd extensions/hashline && npm install && cd ../..
Deps: @oh-my-pi/hashline ^17.1.3 (runtime), patch-package ^8.0.1 (runtime), @earendil-works/pi-coding-agent (peer), typebox (peer).
Workflow
1. LLM calls read /tmp/file.js
2. hashline hook → inject header: [/tmp/file.js#A1B2]
3. LLM receives tagged file content
4. LLM emits a hashline patch
5. Patcher verifies #A1B2 + seen-lines → applies edit
6. Returns new tag: #C3D4
Verify
pi -e ./extensions/hashline/src/hashline.ts -p "what is 1+1" --no-session
Implementation
Wraps @oh-my-pi/hashline: hashline.ts overrides edit and hooks read; PiexNodeFilesystem drives node:fs + realpath directly; EditGuard handles noop/duplicate; Bun polyfill only provides xxHash32.
| Capability | Status |
|---|---|
| tree-sitter block / REM·MV | ✅ via engine |
| Noop / Duplicate / normalize | ✅ Phase 1 |
| Stale auto-recovery / multi-version snapshot | ❌ Phase 2 |
Markdown fence protection (patch-package patch + Phase 2.5)
Problem
The upstream boundary-repair engine is designed for code (} / ) / JSX close and other structural edges) and mis-handles Markdown fenced code blocks. A ``` fence line is delimiter-neutral; when a SWAP range correctly includes the fence, the payload restates it, and the surviving line just outside the range happens to be the adjacent block's fence, the engine drops the payload's trailing fence as a "restated boundary" — leaving the block unclosed, shifting every later fence pair, silently.
Two-layer fix (see patches/@oh-my-pi+hashline+17.1.3.patch and hashline.ts):
- Fence-role guard (upstream patch): in
countDuplicateLeading/TrailingBoundaryLines, reject an echo candidate when it contains a fence line and the range's own boundary line is itself a fence — the outside fence and the range fence belong to the same pairing sequence, so dropping the payload fence inevitably breaks pairing. Legitimate absorption still works: a range short of a fence (tight two-sided echo) or missing the closing fence (range's last line is content) still repairs normally. Code files are unaffected (no fence lines). - Markdown fence balance check (Phase 2.5): after editing a
.md/.markdownfile, compare fence-line parity before/after; when an edit turns an even count odd, emit[WARN]with line numbers. Covers repair-misdeletes, SWAP-range miscounts, and model output that drops a fence — turning silent corruption into a visible warning.
Heuristic limit
An odd fence count ≈ a possibly-unclosed block; it does not model ``` vs ~~~ as independent streams, nor catch "even count but already mis-paired". Same pattern as the Phase 2.4 HTML tag check: only newly introduced imbalance is reported; a pre-existing odd count is not re-warned, and an odd → even repair stays silent.
Design notes
| Project | Mechanism | piex choice |
|---|---|---|
| oh-my-pi hashline | whole-file tag + tree-sitter block grammar + boundary repair engine | Adopted: wrap @oh-my-pi/hashline, inherit tag, SWAP.BLK, REM/MV and the engine. Not adopted: in-tree integration, Bun FS, LSP writethrough; use Node + an outer EditGuard instead |
| pi-hashline-edit | per-line context hash + 3-way merge recovery + JSON DSL | Not adopted core algorithm (route divergence), borrowed the fault-tolerance intent (noop/dup guard). ADRs kept as Phase 2 Stale-recovery reference |
| pi-hashline-edit-pro | per-line content hash + stable mapping | Not adopted algorithm, borrowed "minimize re-reads" as a long-term goal |
Core trade-off: whole-file tag (strict and simple) over per-line tag (locally usable), with a wrapper layer for fault tolerance. Three phases: Phase 1 tolerance (done) → Phase 2 recovery → Phase 3 undo/LSP integration.
Changelog
Roadmap
Conflict policy leans toward reject (safe but costly) → add 3-way merge / Recovery; snapshots are thin → multi-version LRU + anchored Grep; duplicate granularity per section; wrapper-layer unit tests and optional LSP integration.
Phase 1 ✅ Noop Loop / Duplicate Edit / dialect normalize
Phase 2 Stale recovery · multi-version snapshot · anchored Grep
Phase 3 Undo · Auto/Raw Read · LSP integration · denser tests
Version history
| Version | Date | Changes |
|---|---|---|
| 0.1.3 | 2026-08-02 | Auto-repair warnings now show the actual dropped lines (up to 3 lines, 80-char truncated preview); the update diff echo gains an "anchor on the new line numbers" next-edit hint; the tag mismatch message points back to this session's diff echo; WARN assertion tests added for every repair path (boundary echo, duplicate prefix/suffix) |
| 0.1.2 | 2026-07-25 | Upgraded @oh-my-pi/hashline to ^17.1.3; auto-apply two upstream patches via patch-package + postinstall (findDuplicateSuffix/findDuplicatePrefix boundary echo false-positive fix, import.meta.dir Node 18 compatibility fix) |
| 0.1.1 | 2026-07-14 | Initial release: wraps @oh-my-pi/hashline, overrides built-in edit; Phase 1 guards (Noop Loop / Duplicate Edit / dialect normalize); Node.js native FS + realpath path normalization |
Appendix: comparison with other hashline implementations
Three industry routes: oh-my-pi whole-file tag, pi-hashline-edit context per-line hash, pi-hashline-edit-pro stable mapping. piex picks the first as engine, borrowing fault tolerance from the latter. Full comparison in the source.
Source Markdown: docs/packages/hashline.md