![pi-hashline-context-edit](assets/banner.png)

# pi-hashline-context-edit

This [pi-coding-agent](https://github.com/badlogic/pi-mono/tree/main/packages/coding-agent) extension replaces the built-in `read` and `edit` tools. It uses hash-anchored line editing.

## Fork notice

`pi-hashline-context-edit` is a fork of `pi-hashline-edit`, which uses ideas from [oh-my-pi](https://github.com/can1357/oh-my-pi). This fork keeps the strict hashline editing contract. It adds stale-context detection, optional raw reads, and deterministic stale-anchor recovery.

The fork includes these changes:

- Contextual FNV-1a hashes use the line number and nearby visible lines. A changed line invalidates nearby stale anchors.
- Inline hashing removes the `xxhashjs` dependency.
- The `read({ raw: true })` option supports unanchored inspection and saves tokens.
- A normal-read snapshot cache supports safe three-way merges after non-overlapping external file changes.
- Diff previews and edit results compute hashes from the full post-edit file context.
- Documentation, prompts, ADRs, tests, and package metadata describe the contextual-hash workflow.

A normal `read` gives each line a short contextual hash. Edits reference these hashes instead of raw text. The tool detects stale context before an edit reaches the file.

## Installation

1. To install from GitHub, run this command:

   ```bash
   pi install git:github.com/obalado/pi-hashline-context-edit
   ```

2. To install from a local checkout, run this command:

   ```bash
   pi install /path/to/pi-hashline-context-edit
   ```

## How it works

### `read` — tagged line output

The `read` tool prefixes each text-file line with `LINE#HASH:`. It can left-pad line numbers to align the `#HASH:` columns:

```text
 8#VR:function hello() {
 9#KT:  console.log("world");
10#BH:}
```

- `LINE` — 1-indexed line number.
- `HASH` — 2-character contextual hash from the alphabet `ZPMQVRWSNKTXJBYH`.

Optional parameters:

- `offset` — Start at this 1-indexed line number.
- `limit` — Return at most this number of lines.
- `raw` — Return raw file lines without `LINE#HASH:` prefixes. Use this option only for inspection or token savings.

Before an anchor-based edit, use a normal `read`.

The `read` tool passes JPEG, PNG, GIF, and WebP images through as attachments. Images do not use the hashline protocol. The tool rejects binary files and directories with a descriptive error. For an empty file, it suggests `prepend` or `append` instead of a synthetic anchor.

### `edit` — hash-anchored modifications

Use the `LINE#HASH` anchors from `read` output to target lines:

```json
{
  "path": "src/main.ts",
  "edits": [
    { "op": "replace", "pos": "11#KT", "lines": ["  console.log('hashline');"] }
  ]
}
```

| Op | Purpose | Fields |
|---|---|---|
| `replace` | Replace one line or an inclusive range. | `pos` required, `end` optional, `lines` |
| `append` | Insert lines after `pos`, or append at EOF without `pos`. | `pos` optional, `lines` |
| `prepend` | Insert lines before `pos`, or prepend at BOF without `pos`. | `pos` optional, `lines` |
| `replace_text` | Replace an exact, unique substring. Require exactly one match. | `oldText`, `newText` |

Each call validates all edits against one pre-edit snapshot. It applies edits from bottom to top, so line numbers stay consistent.

### Chained edits

A successful edit uses the default `changed` return mode. Its result includes fresh `LINE#HASH` references in an `--- Anchors A-B ---` block. For the next nearby edit, use these anchors without reading the full file again. Before a distant edit, read the file.

### Diff preview

The tool stores the full diff in `details.diff` for the host UI. The model-visible text contains fresh anchors, warnings, and retry instructions.

## Design decisions

- **Stale anchors fail or merge safely.** A hash mismatch shows that the file changed after the last `read`. The tool can merge an edit when it has a normal-read snapshot and changes do not overlap. It reports `[W_MERGED_STALE_ANCHORS]` after a merge. Otherwise, the error includes fresh `LINE#HASH` snippets for a retry.
- **No fallback relocation.** The tool never moves a mismatched anchor to a nearby line. Merge recovery combines only non-overlapping changes. The tool rejects conflicts.
- **Strict patch content.** The tool rejects `lines` with `LINE#HASH:` display prefixes or diff `+` or `-` markers. It reports `[E_INVALID_PATCH]`. Send literal file content because the runtime does not remove accidental prefixes.
- **Native edit normalization.** The tool converts a top-level `oldText` and `newText` payload into `op: "replace_text"`. It uses the same exact, unique-match rules as other `replace_text` edits. It rejects inexact or non-unique matches. It does not use fuzzy fallback or a separate compatibility notice.
- **Atomic writes.** The tool writes to a temporary file and then renames that file. This process prevents interrupted writes from corrupting files. The tool resolves symlink chains without replacing the symlink. It updates hard-linked files in place to preserve the shared inode. It preserves file permissions during atomic renames.
- **Per-file mutation queue.** The tool queues edits by their canonical write target. Concurrent edits through different symlink paths still run in order on the same file.

## Hashing

The inline FNV-1a 32-bit hash uses the line number and nearby context:

```text
lineNumber \0 previousLine \0 currentLine \0 nextLine
```

The low byte maps to two characters from the custom alphabet `ZPMQVRWSNKTXJBYH`. This alphabet excludes hex digits, common vowels, and ambiguous letters: D, G, I, L, and O. A reference such as `5#MQ` stays short and clear.

Each hash includes the previous and next visible lines. Changing one line invalidates its anchor and its immediate neighbors. This behavior catches common line-shift and stale-context errors while keeping anchors short.

## Development

1. Install [Node.js](https://nodejs.org) and npm.

2. Install the dependencies:

   ```bash
   npm install
   ```

3. Run the tests:

   ```bash
   npm test
   ```

To show an "active" notification at session start, set `PI_HASHLINE_DEBUG=1`.

## Credits

Thanks to [can1357](https://github.com/can1357) for the original [oh-my-pi](https://github.com/can1357/oh-my-pi) implementation and the hashline concept.

## License

[MIT](LICENSE)
