198 lines
11 KiB
Markdown
198 lines
11 KiB
Markdown
# 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` — canonical constrained-decoding grammar
|
|
- `packages/hashline/src/format.ts` — sigils and header constants (`¶`, `#`, `+`, `replace`, `delete`, `insert`)
|
|
- `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 must start with `¶PATH#TAG`; `TAG` is the four-hex snapshot tag emitted by the latest `read`/`search`/`write`/successful `edit`. Optional `*** Begin Patch` / `*** End Patch` envelope is ignored if present. |
|
|
|
|
Patch language inside `input`:
|
|
|
|
- **File header**: `¶PATH#TAG`. `TAG` is four uppercase-hex chars minted by the session snapshot store.
|
|
- **Operations**:
|
|
- `replace N..M:` — replace original lines N..M with the body rows below.
|
|
- `replace block N:` — replace the whole tree-sitter block beginning on line N (its header line through its closing line) with the body rows. The line span is resolved at apply time from the file's parse tree; point N at the line that opens the construct. Errors (and steers to `replace N..M:`) when the language is unsupported, line N is blank or a closing delimiter, no node begins there, or the resolved block has a syntax error.
|
|
- `delete N..M` — delete original lines N..M. No body.
|
|
- `delete block N` — delete the whole tree-sitter block beginning on line N (resolved like `replace block N`). No body. Same resolution failure modes and `delete N..M` fallback.
|
|
- `insert before N:` — insert body rows immediately before line N.
|
|
- `insert after N:` — insert body rows immediately after line N.
|
|
- `insert head:` — insert body rows at the start of the file.
|
|
- `insert tail:` — insert body rows at the end of the file.
|
|
- **Body rows**:
|
|
- Only body-bearing headers end in `:`.
|
|
- Every body row is `+TEXT`; `+` alone adds a blank line.
|
|
- `delete` never has body rows.
|
|
- There is no repeat row kind. To keep a line, leave it out of every range; split edits into multiple hunks when needed.
|
|
- `-` rows are invalid. Literal text beginning with `-` or `+` must be written as `+-text` / `++text`.
|
|
|
|
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)
|
|
|
|
The canonical grammar is strict, but the hand parser accepts a few non-dangerous variants:
|
|
|
|
- `replace N:` — accepted as `replace N..N:`.
|
|
- `delete N` — accepted as single-line delete.
|
|
- Missing trailing colon on `replace` or `insert` — accepted.
|
|
- `replace N-M:`, `replace N…M:`, and `replace N M:` — accepted as `replace N..M:`.
|
|
- Bare body rows with no `+` prefix are auto-prepended with `+` and a `BARE_BODY_AUTO_PIPED_WARNING` is appended.
|
|
- `*** Begin Patch` / `*** End Patch` envelopes are silently consumed. `*** Abort` terminates parsing silently — ops parsed before the marker still apply, no warning surfaced.
|
|
- Some malformed `¶` headers are recovered after stripping apply-patch path noise such as `Update File:` / `Add File:` and extra `***`, but the recovered header still needs a valid four-hex tag for the patcher to apply it.
|
|
- `*** Update File:` / `*** Add File:` / `*** Delete File:` / `*** Move to:` apply_patch sentinels inside the diff body throw an `apply_patch sentinel … is not valid in hashline` error.
|
|
- `@@`-bracketed hunk headers are rejected with guidance to write a verb header.
|
|
- Bare `N` and bare `N M` / `N..M` headers are rejected with guidance to write `replace` or `delete`.
|
|
- `delete N..M:` and any body rows under `delete` / `delete block` are rejected.
|
|
- Empty `replace` / `insert` / `replace block` hunks are rejected.
|
|
- `-` body rows are rejected with `MINUS_ROW_REJECTED`.
|
|
- `replace block N:` / `delete block N` require a wired tree-sitter resolver; `replace block` additionally needs at least one `+TEXT` body row, while `delete block` takes none. An unresolvable block (unsupported language, blank/closing-delimiter line, no node beginning on N, or a syntax error in the resolved block) is rejected on the apply/final-preview path; the streaming preview silently drops it instead.
|
|
|
|
## 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:
|
|
|
|
```text
|
|
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):
|
|
|
|
```text
|
|
¶a.ts#0A3B
|
|
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:
|
|
|
|
```text
|
|
¶a.ts#0A3B
|
|
replace 1..1:
|
|
+const X = "b";
|
|
+export const Y = X;
|
|
```
|
|
|
|
Insert below line 5:
|
|
|
|
```text
|
|
¶a.ts#0A3B
|
|
insert after 5:
|
|
+console.log(X + Y);
|
|
```
|
|
|
|
Insert above line 5:
|
|
|
|
```text
|
|
¶a.ts#0A3B
|
|
insert before 5:
|
|
+console.log(X + Y);
|
|
```
|
|
|
|
Delete lines 4..5 entirely:
|
|
|
|
```text
|
|
¶a.ts#0A3B
|
|
delete 4..5
|
|
```
|
|
|
|
Insert at start and end of file:
|
|
|
|
```text
|
|
¶a.ts#0A3B
|
|
insert head:
|
|
+// header
|
|
insert tail:
|
|
+// trailer
|
|
```
|
|
|
|
Multi-file:
|
|
|
|
```text
|
|
¶src/a.ts#0A3B
|
|
replace 4..4:
|
|
+const enabled = true;
|
|
¶src/b.ts#1F7C
|
|
delete 20
|
|
```
|
|
|
|
## Limits & Caps
|
|
- File snapshot tags are exactly four 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_RANGE_SEP` is `..`, `HL_FILE_HASH_SEP` is `#`, and hunk keyword constants are `replace` / `delete` / `insert` (`packages/hashline/src/format.ts`).
|
|
|
|
## Errors
|
|
- Missing section header:
|
|
- `input must begin with "¶PATH#HASH" on the first non-blank line for anchored edits; got: ...`
|
|
- Missing tag for any section:
|
|
- `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 \`replace N..M:\`, \`delete N..M\`, or \`insert before|after|head|tail:\` above the body. Got "...".`
|
|
- Minus row:
|
|
- ``line N: `-` rows are not valid; hashline ranges already name the lines being changed. To insert a literal line starting with `-`, write `+-…`.``
|
|
- Empty body-bearing hunk:
|
|
- `line N: \`replace N..M:\` needs at least one \`+TEXT\` body row. To delete lines, use \`delete N..M\`.`
|
|
- `line N: \`insert\` needs at least one \`+TEXT\` body row.`
|
|
- `line N: \`replace block N:\` needs at least one \`+TEXT\` body row. To delete a block, use \`delete N..M\` with the block's line range.`
|
|
- Unresolvable `replace block N:` (apply / final-preview path only):
|
|
- `line N: \`replace block X:\` could not resolve a syntactic block beginning on line X. The language may be unsupported, the line may be blank or a closing delimiter, or the block may not parse. Use \`replace X..M:\` with the block's explicit end line instead.`
|
|
- Delete with body:
|
|
- `line N: \`delete N..M\` does not take body rows. Remove the body, or use \`replace N..M:\`.`
|
|
- `line N: \`delete block N\` does not take body rows. Remove the body, or use \`replace block N:\` to replace the block.`
|
|
- 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). Use \`replace N..M:\`, \`delete N..M\`, or \`insert before|after|head|tail:\` ops.`
|
|
- `line N: unified-diff hunk header (\`@@ -N,M +N,M @@\`) is not valid in hashline. Use \`replace N..M:\`, \`delete N..M\`, or \`insert before|after|head|tail:\` ops.`
|
|
- `line N: \`@@\`-bracketed hunk header "@@ …" is not valid in hashline. Drop the \`@@ ... @@\` brackets and write a verb header such as \`replace N..M:\`.`
|
|
- `line N: hunk headers need a verb. Use \`replace N..N:\` to replace, or \`delete N\` to delete.`
|
|
- `line N: bare range hunk header "N M" is not valid. Hunk headers need a verb: write \`replace N..M:\` or \`delete N..M\`.`
|
|
- Out-of-range anchor:
|
|
- `Line N does not exist (file has M lines)`
|
|
- Stale snapshot tag: the `Patcher` first attempts snapshot-based recovery. When recovery cannot prove a valid result it throws `MismatchError`, which distinguishes recognized-but-drifted hashes from never-recorded hashes. The error includes the current file hash plus context around each anchor.
|
|
- 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
|
|
- `Auto-prefixed bare body row(s) with +. Body rows must be +TEXT literal lines …` (`BARE_BODY_AUTO_PIPED_WARNING`)
|
|
- Recovery banners: `RECOVERY_EXTERNAL_WARNING`, `RECOVERY_SESSION_CHAIN_WARNING`, `RECOVERY_SESSION_REPLAY_WARNING` (`packages/hashline/src/messages.ts`).
|