Hashline Edit

Hashline Edit#

Snapshot-anchored file editing inspired by oh-my-pi’s Hashline format.

Provides the hashline_edit tool: a two-call editing cycle where the read tool populates a snapshot store with a file-level 4-byte SHA-256 tag, and hashline_edit verifies that tag before applying line-range operations.

Workflow#

  1. read <path> — returns the file with a [PATH#TAG] header on the first line of the code block body. The snapshot is stored in-memory.

  2. hashline_edit <path> — the code block body must begin with [PATH#TAG] matching the tag seen in step 1, then list one or more PUT/CUT operations.

Edit syntax#

[PATH#TAG]
PUT N.=M:       — replace lines N through M (inclusive) with new content
+new line 1
+new line 2
PUT <N:         — insert the new content BEFORE line N
+new line
PUT >N:         — insert the new content AFTER line N
+new line
PUT N*:         — replace the syntactic block starting at line N
+new content
CUT N.=M        — delete lines N through M (no content block follows)
CUT N.=M @r    — delete and save to named register r
PUT >N @r       — paste register r content AFTER line N (no content block)
PUT <N @r       — paste register r content BEFORE line N (no content block)
PUT N.=M: @r    — replace lines N–M with register r content (no content block)

Content lines are prefixed with +. An operation’s content block ends at the next operation header or end-of-input. Register-paste operations have no content block.

If the live file’s hash no longer matches the stored snapshot tag the entire edit is rejected with a clear error — no silent corruption.

Instructions

Use after ``read`` to apply precise, safe line edits without re-reading the full file.
The tag from ``read`` anchors your edits to the exact version you saw — any external
change to the file on disk is detected and rejected before a single byte is written.

Prefer this tool over ``patch`` when you have exact line numbers and want to avoid
repeating large context blocks.  You can chain multiple operations in one call;
line numbers always reference the version ``read`` showed you.

### Workflow

1. Call ``read <path>`` — the output starts with ``[PATH#TAG]`` on the first line
   inside the code block.  Remember this TAG.
2. Write a ``hashline_edit <path>`` block beginning with the same ``[PATH#TAG]``
   header, followed by one or more operations.

### Recovery from rejection

If the file changed between ``read`` and your edit, you will see:
  ``hashline_edit: file has changed since snapshot was captured…``
Re-read the file with ``read`` to get a new tag and restate your operations.

### Operations

| Syntax            | Effect                                           |
|-------------------|--------------------------------------------------|
| ``PUT N.=M:``     | Replace lines N through M with new lines         |
| ``PUT N*:``       | Replace the syntactic block at line N            |
| ``PUT <N:``       | Insert new lines BEFORE line N                   |
| ``PUT >N:``       | Insert new lines AFTER line N                    |
| ``CUT N.=M``      | Delete lines N through M                         |
| ``CUT N.=M @r``   | Delete lines N–M and save to register *r*        |
| ``PUT N.=M: @r``  | Replace lines N–M with content of register *r*   |
| ``PUT <N @r``     | Insert register *r* content BEFORE line N        |
| ``PUT >N @r``     | Insert register *r* content AFTER line N         |

``PUT N*:`` replaces a whole indented construct — point it at the ``def``,
``class``, ``if``, ``for``, ``while``, or ``try`` line and the system finds the
closing line for you, including any ``elif``/``else``/``except``/``finally``
clauses attached to that same statement.  Use it instead of ``PUT N.=M:`` when
you want to replace an entire function/block and don't want to count lines by
hand.

Point N at the actual header line (e.g. the ``def`` line, not a decorator or a
comment above it) — the block is resolved from that line's indentation.
Constructs the heuristic can't reliably reproduce: blocks that mix tabs and
spaces, or a header followed by a same-indentation line that is *not* a
continuation clause (e.g. a one-line ``if x: y`` body) — for those, fall back to
``PUT N.=M:`` with an explicit line range.

**Registers** avoid reproducing unchanged content when moving or copying exact
lines within one edit call, reducing tokens and transcription mistakes.
``CUT N.=M @r`` captures deleted lines into register *r* (a short word), then
``PUT >N @r`` or ``PUT <N @r`` pastes them. Registers are resolved from the
original file state, so a cut at line 5 and a paste at line 20 work correctly
regardless of apply order. The PUT destination must be outside the CUT range.

New content lines are prefixed with ``+``.  An empty line ends a content block.
CUT (with or without a register) has no content block.  Register-paste operations
(``PUT ... @r``) also have no content block.  Line numbers reference the version
shown by ``read``.

### Example

```
[greet.py#A1B2C3D4]
PUT 2.=3:
+    print(f"Hi, {name}")
CUT 4.=4
PUT >5:
+    return name
```

### Register example (move a function to the bottom)

```
[module.py#A1B2C3D4]
CUT 3.=10 @fn
PUT >40 @fn
```

Examples

User
update greet.py to use an f-string
Assistant
First, read the file:
read greet.py
System
```greet.py
> [greet.py#A1B2C3D4]
>    1\tdef greet(name):
>    2\t    msg = "Hello, " + name
>    3\t    print(msg)
> ```
Assistant
hashline_edit greet.py
[greet.py#A1B2C3D4]
PUT 2.=3:
+    print(f"Hi, {name}")
System
hashline_edit applied to `greet.py` (1 operation)
class gptme.tools.hashline_edit.HashlineOp

A single parsed edit operation.

__init__(kind: Literal['replace', 'insert_before', 'insert_after', 'delete', 'block_replace'], start: int, end: int, text: str | None, register_name: str | None = None, resolved_register_lines: list[str] | None = None) None
exception gptme.tools.hashline_edit.ParseError