diff --git a/packages/coding-agent/src/utils/git.ts b/packages/coding-agent/src/utils/git.ts index 556423baa..35342a75e 100644 --- a/packages/coding-agent/src/utils/git.ts +++ b/packages/coding-agent/src/utils/git.ts @@ -1727,6 +1727,12 @@ export const revList = { async range(cwd: string, base: string, head: string, signal?: AbortSignal): Promise { return splitLines(await runText(cwd, ["rev-list", "--reverse", `${base}..${head}`], { readOnly: true, signal })); }, + /** Commits reachable from `ref` that touched `file`, newest first, capped at `limit`. */ + async touching(cwd: string, ref: string, file: string, limit: number, signal?: AbortSignal): Promise { + return splitLines( + await runText(cwd, ["rev-list", `--max-count=${limit}`, ref, "--", file], { readOnly: true, signal }), + ); + }, }; // ════════════════════════════════════════════════════════════════════════════ diff --git a/packages/hashline/src/prompt.md b/packages/hashline/src/prompt.md index 883716757..2eee092af 100644 --- a/packages/hashline/src/prompt.md +++ b/packages/hashline/src/prompt.md @@ -1,54 +1,45 @@ -Your patch language names lines to replace, delete, or insert at, then lists the new content. Rule of thumb: a header ending in `:` is followed by `+` body rows; `DEL`, `CUT`/`COPY`, and `PASTE` have no body. +Line-anchored patch language: name original lines to replace, delete, or insert at, then list new content. A header ending in `:` takes `+` body rows; `DEL`, `CUT`/`COPY`, `PASTE`, `REM`, `MV` take none. -Every file section starts with `[PATH#TAG]`. `TAG` = 4-hex snapshot tag from your latest `read`/`search`, REQUIRED on every section — no hashless form. Create new files with `write`; hashline only edits existing files. +Every file section starts `[PATH#TAG]`. `TAG` = 4-hex snapshot tag from your latest `read`/`search` — REQUIRED on every section. Create new files with `write`; hashline only edits existing files. -`SWAP N.=M:` — replace original lines N.=M with the body rows below. INCLUSIVE — line M is consumed too. -`SWAP.BLK N:` — replace the whole syntactic block that BEGINS on line N; tree-sitter resolves the closing line. Body rows below. -`DEL N.=M` — delete original lines N.=M. No body. -`DEL.BLK N` — delete the whole syntactic block that BEGINS on line N. -`INS.PRE N:` — insert the body rows immediately before line N. -`INS.POST N:` — insert the body rows immediately after line N. -`INS.BLK.POST N:` — insert the body rows after the END of the block that BEGINS on line N — outside it, at sibling depth. To append inside a block, use `INS.POST`. -`INS.HEAD:` / `INS.TAIL:` — insert the body rows at the very start / end of the file. -`CUT N.=M` / `COPY N.=M` — capture original lines N.=M into the clipboard (CUT also deletes them, like `DEL`). No body. -`CUT.BLK N` / `COPY.BLK N` — capture the whole syntactic block that BEGINS on line N. -`PASTE.PRE N` / `PASTE.POST N` / `PASTE.HEAD` / `PASTE.TAIL` / `PASTE.BLK.POST N` — insert the clipboard at the position. No body — the clipboard IS the body. -`REM` — delete the whole file named by the section header. No body, no line ops. -`MV DEST` — move/rename the section file to `DEST` (a path, quoted when it contains spaces). Line edits above `MV` land on the source first, then the final content is written at `DEST`. -Single line: `SWAP N.=N:` / `DEL N`. The range is the ORIGINAL lines you touch; body length is irrelevant (replacing 1 line with 10 is still `SWAP N.=N:`). +`SWAP N.=M:` — replace original lines N.=M (INCLUSIVE). +`SWAP.BLK N:` — replace the whole syntactic block BEGINNING on line N; its closing line is resolved for you. +`DEL N.=M` / `DEL.BLK N` — delete lines N.=M / the block beginning at N. +`INS.PRE N:` / `INS.POST N:` — insert immediately before / after line N. +`INS.BLK.POST N:` — insert after the END of the block beginning at N, outside it at sibling depth. Append inside a block → `INS.POST`. +`INS.HEAD:` / `INS.TAIL:` — insert at the very start / end of the file. +`CUT N.=M` / `COPY N.=M` — capture lines N.=M into the clipboard (`CUT` also deletes); `.BLK N` forms capture the block. +`PASTE.PRE N` / `PASTE.POST N` / `PASTE.HEAD` / `PASTE.TAIL` / `PASTE.BLK.POST N` — insert the clipboard at the position (the clipboard IS the body). +`REM` — delete the whole section file. `MV DEST` — move/rename to `DEST` (quote paths with spaces); edits above `MV` land on the source first, final content written at `DEST`. +Single line: `SWAP N.=N:` / `DEL N`. Range = ORIGINAL lines touched; body length irrelevant (1 line → 10 is still `SWAP N.=N:`). -Body rows appear only under a `:` header. Every body row is `+TEXT` — add a literal line `TEXT`, verbatim (leading whitespace kept); `+` alone adds a blank line. No other row kind. NEVER write `-old` or a bare/context line. To keep a line, leave it out of every range. Literal lines starting with `-`/`+` still need the body prefix: Markdown `- item` → `+- item`, `+ item` → `++ item`. +Only under a `:` header. Every row is `+TEXT`, verbatim (leading whitespace kept); `+` alone = blank line. NEVER `-old` or bare/context rows — the range deletes; the body is only the final content. Keep a line: leave it out of every range. Literal leading `-`/`+` keeps the prefix: `- item` → `+- item`, `+ item` → `++ item`. -- Line numbers + `[PATH#TAG]` header come from your latest `read`/`search` (`LINE:TEXT` rows). -- Numbers refer to the ORIGINAL file; never shift as hunks apply. -- They die with the call: every applied edit mints a fresh `#TAG` and renumbers — anchor the next edit on the edit response or a fresh `read`. -- Touch only lines your latest `read`/`search` literally displayed as `LINE:TEXT`; the tag certifies the snapshot, not your memory. A hunk anchored on a line you never displayed is REJECTED — re-`read` first. Seeing a line ≠ it holds the code you mean; confirm numbers map to the construct you intend, especially far from your read window. -- Elided regions are UNSEEN: `…`/`..` markers and a collapsed `N-M:` summary row (only boundary lines N and M shown) hide their interior. NEVER place or span a hunk inside one — `read` the range first. -- Never start or end a range mid-expression or mid-block. -- Indent body rows exactly for the depth they should live at. -- On a stale-tag rejection or any surprising result: STOP and re-`read` before further edits. -- One hunk per range; body = final content, never an old/new pair. -- Ranges cover ONLY lines whose content changes. Never widen over unchanged lines — a stale wide range shreds everything it spans. -- Whole construct → `SWAP.BLK N` (tree-sitter resolves the end); lines inside it → `SWAP N.=M`. -- `SWAP.BLK N` resolves EXACTLY the node at N. Leading decorators/attributes/doc-comments are separate nodes: point N at the FIRST decorator to sweep both; standalone line-comments are never swept — use `SWAP N.=M`. -- Block ops (`SWAP.BLK`/`DEL.BLK`/`INS.BLK.POST`) anchor the OPENING line of a MULTI-LINE construct — never its closer, last line, or a bare inner statement. Anchoring one statement resolves to ONE line and is REJECTED: use the plain op (`SWAP N.=N` / `DEL N` / `INS.POST N`), or point N at the real opener. Saw the closer? Use plain `INS.POST M:`. -- Markdown: a heading line IS a block opener — `SWAP.BLK`/`DEL.BLK`/`INS.BLK.POST` on a `##`/`###` heading resolves its WHOLE section (heading through every nested deeper heading, up to the next same-or-higher heading). So `DEL.BLK` drops the section, `SWAP.BLK` rewrites it, `INS.BLK.POST` lands after it (end the inserted body with a blank line to keep the next heading separated). -- Non-adjacent changes = separate hunks; untouched lines stay out of every range. -- Pure additions use `INS.PRE` / `INS.POST` / `INS.HEAD` / `INS.TAIL`, never a widened `SWAP` — retyped keepers are exactly what gets dropped. (A multi-line `SWAP` whose body restates the line just past the range is auto-dropped as an off-by-one keeper with a warning — issue the payload for the range only; never lean on the repair.) -- Move code with `CUT`+`PASTE`, never by retyping it in a body. The clipboard runs top-to-bottom through the whole patch — across `[file]` sections, so content moves between files — and persists across edit calls. The last `CUT`/`COPY` wins; `PASTE` may repeat (it does not consume). Content pastes verbatim, indentation included — re-indenting a move needs `SWAP`. -- `CUT` content must land: un-pasted `CUT` content is rejected or carried to the next call with a warning, and a new `CUT`/`COPY` before pasting it is an error. -- NEVER format/restyle code with this tool; run the project formatter instead. +- Line numbers + `#TAG` come from your latest `read`/`search` (`LINE:TEXT` rows); numbers name ORIGINAL lines, never shifted by applied hunks. +- Applied edits renumber the file and change the `#TAG` — take the next edit's numbers from the edit response or a fresh `read`. +- Touch only displayed lines — hunks on undisplayed lines are REJECTED. Far from your read window? Re-`read`; confirm numbers map to the intended construct. +- Elided regions are UNSEEN (`…`/`..` markers, collapsed `N-M:` summary rows) — NEVER place or span a hunk inside one; `read` the range first. +- NEVER start or end a range mid-expression or mid-block. +- Ranges cover ONLY changed lines — never widen over keepers. Non-adjacent changes = separate hunks. +- Whole construct → `SWAP.BLK N`; lines inside one → `SWAP N.=M`. +- `SWAP.BLK` resolves EXACTLY the node at N: leading decorators/attributes/doc-comments are separate nodes — point N at the FIRST decorator to sweep both; standalone line-comments are never swept (use `SWAP N.=M`). +- Block ops anchor the OPENING line of a MULTI-LINE construct — never the closer, last line, or a bare inner statement; one statement → plain op (`SWAP N.=N:` / `DEL N` / `INS.POST N:`). Saw the closer? `INS.POST M:`. +- Markdown: a heading IS a block opener — block ops on `##`/`###` resolve the WHOLE section (through deeper nested headings, up to the next same-or-higher heading). `INS.BLK.POST` after a section: end the body with a blank line to keep the next heading separated. +- Pure additions → `INS.PRE`/`INS.POST`/`INS.HEAD`/`INS.TAIL`, never a widened `SWAP`. +- Move code with `CUT`+`PASTE`, never retype. Clipboard: top-to-bottom across the whole patch (cross-file moves), persists across edit calls, last `CUT`/`COPY` wins, `PASTE` repeats freely, pastes verbatim with indentation — re-indent via `SWAP`. +- Paste `CUT` content before any new `CUT`/`COPY`. +- NEVER format/restyle code with this tool; run the project formatter. -Original (the exact shape `read` returns): +`read` output shape: ``` [greet.py#A1B2] 1:def greet(name): @@ -57,40 +48,7 @@ Original (the exact shape `read` returns): 4:greet("world") ``` -Insert a guard after line 1: -``` -[greet.py#A1B2] -INS.POST 1: -+ if not name: name = "stranger" -``` - -Replace line 2 with two lines: -``` -[greet.py#A1B2] -SWAP 2.=2: -+ greeting = "Hi" -+ msg = f"{greeting}, {name}" -``` - -Delete line 3: -``` -[greet.py#A1B2] -DEL 3 -``` - -Delete the whole file: -``` -[greet.py#A1B2] -REM -``` - -Rename or move the file: -``` -[greet.py#A1B2] -MV greet_v2.py -``` - -Move after editing: +Edit, then move: ``` [greet.py#A1B2] SWAP 1.=3: @@ -99,16 +57,7 @@ SWAP 1.=3: MV lib/greet.py ``` -Add a header and trailer: -``` -[greet.py#A1B2] -INS.HEAD: -+# generated header -INS.TAIL: -+greet("everyone") -``` - -Insert Markdown bullets — the leading `+` is the body-row marker; the file receives `- task`: +Markdown bullets — the file receives `- task`: ``` [PLAN.md#A1B2] INS.POST 2: @@ -116,7 +65,7 @@ INS.POST 2: + - nested task ``` -Move the `greet` function after line 4 without retyping it, then copy line 4 into a sibling file — the clipboard flows top-to-bottom across sections: +Move `greet` after line 4, copy line 4 to a sibling file — clipboard flows across sections: ``` [greet.py#A1B2] CUT.BLK 1 @@ -126,7 +75,7 @@ COPY 4.=4 PASTE.HEAD ``` -Replace the whole `greet` function block — `SWAP.BLK 1:` resolves lines 1–3 (the `def` header through `print(msg)`); line 4 is a separate statement and stays: +`SWAP.BLK 1:` resolves lines 1–3 (`def` header through `print(msg)`); line 4 is a separate statement and stays: ``` [greet.py#A1B2] SWAP.BLK 1: @@ -134,7 +83,7 @@ SWAP.BLK 1: + print(f"Hello, {name}") ``` -A decorator/doc-comment is a SEPARATE block — `SWAP.BLK` on the `def`/`fn` line keeps it. Point N at the decorator to take both; here line 1 is `@cache`, so anchoring on the `def` (line 2) would orphan `@cache`: +Decorator/doc-comment = SEPARATE block — point N at the decorator to take both; anchoring the `def` (line 2) would orphan `@cache`: ``` [svc.py#C3D4] SWAP.BLK 1: @@ -148,11 +97,11 @@ SWAP.BLK 1: # WRONG — empty `SWAP` to delete. RIGHT: DEL 4 SWAP 4.=4: -# WRONG — range describes post-edit size. RIGHT: SWAP 1.=1: (body length is irrelevant) +# WRONG — range sized to the post-edit content. RIGHT: SWAP 1.=1: (body length irrelevant) SWAP 1.=2: +def greet(name): -# WRONG — `-` rows / bare context lines do not exist. The range deletes; the body is only the new content. +# WRONG — `-` rows / bare context lines do not exist; the range deletes, the body is only new content. SWAP 3.=3: msg = "Hello, " + name - print(msg) @@ -161,31 +110,29 @@ SWAP 3.=3: SWAP 3.=3: + return msg -# WRONG — a pure insertion done as a widened `SWAP`: you want to add one line after 2, -# but you replace 2.=4, retype the keepers, and drop one (here line 4, `greet("world")`). +# WRONG — pure insertion as a widened `SWAP`: retyped keepers get dropped (here line 4). SWAP 2.=4: + msg = "Hello, " + name + extra = compute(name) + print(msg) -# RIGHT — touch nothing you keep; the new line is the whole body. +# RIGHT — touch nothing you keep. INS.POST 2: + extra = compute(name) -# WRONG — `INS.BLK.POST N:` anchored on a closing delimiter / last visible line. RIGHT: plain `INS.POST M:` +# WRONG — `INS.BLK.POST` anchored on the closing delimiter / last visible line. RIGHT: plain `INS.POST M:` INS.BLK.POST 3: +after() # RIGHT INS.POST 3: +after() -# WRONG — body rows under PASTE; the clipboard is the body. RIGHT: capture first, then a bodyless `PASTE.POST 20` (no colon, no rows). +# WRONG — body rows under PASTE; the clipboard is the body. RIGHT: capture first, then a bodyless `PASTE.POST 20`. PASTE.POST 20: +function f() {} -If you remember nothing else: -1. RE-GROUND AFTER EVERY EDIT. Every apply mints a fresh `#TAG` and renumbers — take the next edit's numbers from the edit response or a fresh `read`. Stale tag or surprise? STOP, re-`read`. -2. RANGES ARE TIGHT. Cover only lines that change; a stale wide range shreds everything it spans. Whole construct → `SWAP.BLK N`. -3. THE BODY IS THE FINAL CONTENT. Every body row starts with `+`; Markdown bullets use `+- item`, not `- item`. +1. RE-GROUND AFTER EVERY EDIT — applied edits renumber the file and change the `#TAG`; take next numbers from the edit response or a fresh `read`. Stale tag or surprise? STOP, re-`read`. +2. RANGES ARE TIGHT — cover only lines that change. Whole construct → `SWAP.BLK N`. +3. BODY = FINAL CONTENT — every body row starts with `+`; Markdown bullets use `+- item`, not `- item`.