Files
oh-my-pi/docs/tools/edit.md
T
can1357 91d15b2ec8 fix(hashline)!: removed single-number hunk header shorthand
- Rejected bare `A` anchors; single-line ranges must now be spelled `A A`.
- Added a descriptive error for single-number headers to guide model output.
- Updated grammar, tokenizer, prompt docs, and tests to reflect the change.
2026-05-28 10:36:49 +02:00

9.4 KiB

edit

Applies source edits; default mode is the hashline patch language consumed from a single input string.

Source

  • Entry: packages/coding-agent/src/edit/index.ts
  • Model-facing prompt: packages/hashline/src/prompt.md
  • Key collaborators:
    • packages/coding-agent/src/utils/edit-mode.ts — selects active edit mode
    • packages/hashline/src/grammar.lark — hashline grammar
    • packages/hashline/src/format.ts — sigils and header constants (, #, @@, +, &, ,)
    • packages/hashline/src/input.ts — parses ¶PATH#TAG sections
    • packages/hashline/src/tokenizer.ts / packages/hashline/src/parser.ts — tokenizes and parses ops
    • packages/hashline/src/apply.ts — applies parsed edits to file text
    • packages/hashline/src/mismatch.ts — stale-anchor mismatch formatting
    • packages/hashline/src/recovery.ts — snapshot-based stale-anchor recovery
    • packages/hashline/src/snapshots.ts — mints and resolves per-path opaque snapshot tags

Inputs

Hashline mode (default)

Field Type Required Description
input string Yes One or more file sections. Anchored sections start with ¶PATH#TAG; hashless ¶PATH is allowed only for new-file creation or BOF/EOF-only inserts. Optional *** Begin Patch / *** End Patch envelope is ignored if present.

Patch language inside input:

  • File header: ¶PATH#TAG (or ¶PATH for new-file / virtual-only hunks). TAG is three uppercase-hex chars minted by the session snapshot store.
  • Hunk header: bare A B selects original lines A..B. Two numbers are REQUIRED — single-line ranges are written A A (5 5), not 5. The range separator is normally whitespace; the parser also silently accepts A-B, A..B, and A…B (unicode ellipsis). Virtual variants BOF and EOF target positions before line 1 / after the last line.
  • Body rows (one per line, immediately under the hunk header):
    • +TEXT — add the literal line TEXT verbatim, including all leading whitespace.
    • + alone — add one blank line.
    • &A..B — re-emit original file lines A..B. Use this to keep some of the lines you selected. &A is accepted as &A..A.
  • Semantics:
    • The new content of the selected range is just the body rows top-to-bottom.
    • Empty body deletes the range entirely.
    • BOF / EOF with empty body is a no-op (nothing to insert).

Anchors come from read/search output. read emits a ¶PATH#TAG header from the session snapshot store and lines as LINE:TEXT; copy the header into the edit section and copy only the line number into hunk headers.

Tolerated input shapes (lenient parsing)

Because models reproduce nearby shapes (read output, apply_patch envelopes, unified-diff hunks), the parser is liberal about a handful of harmless variants:

  • A (bare single number) — REJECTED. The parser throws single-number hunk header "A" is no longer accepted. Spell single-line ranges as A A.
  • A-B, A..B, A…B — accepted as A B (any of hyphen, double-dot, or unicode ellipsis works as a silent separator).
  • &A — accepted as &A..A.
  • Bare body rows with no +/& prefix are auto-prepended with + and a BARE_BODY_AUTO_PIPED_WARNING is appended, BUT only when every row in that block is uniformly bare. Mixed +/raw blocks still throw.
  • +&A..B rows (model mistakenly prefixed a repeat with +) are silently rerouted as &A..B repeats with PLUS_PREFIXED_REPEAT_WARNING.
  • Identical-range hunks in the same patch are coalesced last-wins with REPLACE_PAIR_COALESCED_WARNING.
  • An overlapping bare hunk followed by a concrete hunk is treated as a stale "before then after" pair; the bare hunk is dropped with REPLACE_PAIR_COALESCED_OVERLAP_WARNING.
  • *** Begin Patch / *** End Patch envelopes are silently consumed. *** Abort terminates parsing silently — ops parsed before the marker still apply, no warning surfaced.
  • *** Update File: / *** Add File: / *** Delete File: / *** Move to: apply_patch sentinels throw an apply_patch sentinel … is not valid in hashline error.
  • @@-bracketed hunk headers (whether the apply_patch @@ context @@ form or the unified-diff @@ -N,M +N,M @@ shape) are rejected with an explicit "drop the @@ ... @@ brackets" message — hashline hunks are bare A B lines.

Outputs

  • Single-shot tool result; hashline mode does not use a resolve preview/apply handshake.
  • content contains one text block per call. For a successful single-file edit it is either:
    • <path>: plus a compact diff preview from packages/hashline/src/diff-preview.ts, or
    • Updated <path> / Created <path> when no compact preview text is emitted.
  • Parse, apply, or recovery warnings are appended as:
Warnings:
...
  • details is EditToolDetails from packages/coding-agent/src/edit/renderer.ts:
    • diff: unified diff string
    • firstChangedLine: first changed post-edit line
    • diagnostics: LSP/format result if available
    • op: "create" or "update" for hashline mode
    • meta: output metadata
    • perFileResults: present for multi-section input
  • Multi-section input returns one aggregated result with combined text and per-file details.

Worked examples

Reference file (the exact shape read returns):

¶a.ts#0A3
1:const X = "a";
2:const Y = X;
3:
4:console.log(X);
5:console.log(Y);
6:export { X, Y };

Replace line 1 with two lines:

¶a.ts#0A3
1
+const X = "b";
+export const Y = X;

Insert BELOW line 5 (keep line 5, add after):

¶a.ts#0A3
5
&5
+console.log(X + Y);

Insert ABOVE line 5 (add before, keep line 5):

¶a.ts#0A3
5
+console.log(X + Y);
&5

Delete lines 4..5 entirely:

¶a.ts#0A3
4 5

Insert at start and end of file:

¶a.ts#0A3
BOF
+// header
EOF
+// trailer

Multi-file:

¶src/a.ts#0A3
4
+const enabled = true;
¶src/b.ts#1F7
20

Limits & Caps

  • File snapshot tags are exactly three uppercase-hex chars minted by the per-session snapshot store.
  • The visible mismatch report shows 2 lines of context on each side (MISMATCH_CONTEXT) in packages/hashline/src/messages.ts.
  • Stale-anchor recovery uses fuzzFactor: 0 in packages/hashline/src/recovery.ts.
  • HL_FILE_PREFIX is , HL_PAYLOAD_REPLACE is +, HL_PAYLOAD_REPEAT is &, HL_RANGE_SEP is .. (repeat-row bodies only), and HL_FILE_HASH_SEP is # (packages/hashline/src/format.ts). Hunk headers carry no sigil; the range is just two whitespace-separated line numbers.

Errors

  • Missing section header:
    • input must begin with "¶PATH#HASH" on the first non-blank line for anchored edits; got: ...
  • Missing tag for anchored edit:
    • Missing hashline snapshot tag for anchored edit to <path>; use ¶<path>#tag from your latest read/search output.
  • Stray payload line:
    • line N: payload line has no preceding hunk header. Use an \A B` (or `BOF` / `EOF`) line above the body. Got "...".`
  • Raw body row with no + / & prefix in a mixed-prefix block:
    • line N: payload row in a hashline hunk must start with + or &A..B. Got "...".
  • Range out of order:
    • line N: range A..B ends before it starts.
  • Overlapping hunks on the same anchor:
    • line N: anchor line X is already targeted by another hunk on line Y. Issue ONE hunk per range; payload is only the final desired content, never a before/after pair.
  • apply_patch / unified-diff contamination:
    • line N: apply_patch sentinel "*** …" is not valid in hashline. File sections start with \¶path#HASH` (no `Update File:` / `Add File:` keyword). Hunks are bare `A B` lines with `+TEXT` / `&A..B` body rows.`
    • line N: unified-diff hunk header (\@@ -N,M +N,M @@`) is not valid in hashline. Hashline hunks are bare `A B` lines (or `BOF` / `EOF` keywords).`
    • line N: \@@`-bracketed hunk header "@@ …" is not valid in hashline. Drop the `@@ ... @@` brackets and write the range directly: `5 7` (`BOF` / `EOF` for virtual positions).`
    • line N: single-number hunk header "N" is no longer accepted. Spell single-line ranges as \N N` (two numbers); hashline hunks are bare `A B` lines (or `BOF` / `EOF`).`
  • Out-of-range anchor:
    • Line N does not exist (file has M lines)
  • Stale snapshot tag throws MismatchError. The error contains re-read guidance and nearby current file lines as *LINE:TEXT / LINE:TEXT.
  • No-op edit:
    • Edits to <path> parsed and applied cleanly, but produced no change: your body row(s) are byte-identical to the file at the targeted lines. The bug is somewhere else — re-read the file before issuing another edit. Do NOT widen the payload or add lines; verify the anchor first.
  • Recovery failure is silent internally: if cache-based merge cannot prove a valid result, the mismatch error is surfaced unchanged.

Warnings

  • Detected two identical-range hashline hunks; kept only the second hunk. … (REPLACE_PAIR_COALESCED_WARNING)
  • Detected an overlapping bare hashline hunk immediately followed by a concrete hunk; dropped the earlier bare hunk. … (REPLACE_PAIR_COALESCED_OVERLAP_WARNING)
  • Auto-prefixed bare body row(s) with +. Always start payload rows with +TEXT (literal) or &A..B (repeat) … (BARE_BODY_AUTO_PIPED_WARNING)
  • A body row started with +&A..B. +(literal text) and&A..B (repeat) are sibling row kinds … (PLUS_PREFIXED_REPEAT_WARNING)
  • Recovery banners: RECOVERY_EXTERNAL_WARNING, RECOVERY_SESSION_CHAIN_WARNING, RECOVERY_SESSION_REPLAY_WARNING (packages/hashline/src/messages.ts).