@oh-my-pi/pi-coding-agent 13.0.0 → 13.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/package.json +7 -7
  3. package/scripts/format-prompts.ts +33 -3
  4. package/src/commit/prompts/analysis-system.md +3 -3
  5. package/src/commit/prompts/changelog-system.md +3 -3
  6. package/src/commit/prompts/summary-system.md +5 -5
  7. package/src/extensibility/custom-tools/wrapper.ts +1 -0
  8. package/src/extensibility/extensions/wrapper.ts +2 -0
  9. package/src/extensibility/hooks/tool-wrapper.ts +1 -0
  10. package/src/lsp/index.ts +1 -0
  11. package/src/patch/diff.ts +2 -2
  12. package/src/patch/hashline.ts +88 -119
  13. package/src/patch/index.ts +138 -224
  14. package/src/patch/shared.ts +21 -35
  15. package/src/prompts/compaction/compaction-short-summary.md +1 -1
  16. package/src/prompts/system/agent-creation-architect.md +6 -6
  17. package/src/prompts/system/system-prompt.md +1 -1
  18. package/src/prompts/tools/bash.md +1 -1
  19. package/src/prompts/tools/find.md +9 -0
  20. package/src/prompts/tools/hashline.md +130 -154
  21. package/src/prompts/tools/patch.md +1 -1
  22. package/src/prompts/tools/python.md +2 -2
  23. package/src/prompts/tools/replace.md +1 -1
  24. package/src/prompts/tools/task.md +18 -19
  25. package/src/task/index.ts +1 -0
  26. package/src/tools/ask.ts +1 -0
  27. package/src/tools/bash.ts +1 -0
  28. package/src/tools/browser.ts +1 -0
  29. package/src/tools/calculator.ts +1 -0
  30. package/src/tools/cancel-job.ts +1 -0
  31. package/src/tools/exit-plan-mode.ts +1 -0
  32. package/src/tools/fetch.ts +1 -0
  33. package/src/tools/find.ts +1 -0
  34. package/src/tools/grep.ts +1 -0
  35. package/src/tools/notebook.ts +1 -0
  36. package/src/tools/plan-mode-guard.ts +2 -2
  37. package/src/tools/poll-jobs.ts +1 -0
  38. package/src/tools/python.ts +1 -0
  39. package/src/tools/read.ts +1 -0
  40. package/src/tools/ssh.ts +1 -0
  41. package/src/tools/submit-result.ts +1 -0
  42. package/src/tools/todo-write.ts +1 -0
  43. package/src/tools/write.ts +1 -0
  44. package/src/web/search/index.ts +1 -0
@@ -22,6 +22,7 @@ import {
22
22
  truncateDiffByHunk,
23
23
  } from "../tools/render-utils";
24
24
  import { Ellipsis, Hasher, type RenderCache, renderStatusLine, truncateToWidth } from "../tui";
25
+ import type { HashlineToolEdit } from "./index";
25
26
  import type { DiffError, DiffResult, Operation } from "./types";
26
27
 
27
28
  // ═══════════════════════════════════════════════════════════════════════════
@@ -58,7 +59,7 @@ export interface EditToolDetails {
58
59
  /** Operation type (patch mode only) */
59
60
  op?: Operation;
60
61
  /** New path after move/rename (patch mode only) */
61
- rename?: string;
62
+ move?: string;
62
63
  /** Structured output metadata */
63
64
  meta?: OutputMeta;
64
65
  }
@@ -70,6 +71,7 @@ export interface EditToolDetails {
70
71
  interface EditRenderArgs {
71
72
  path?: string;
72
73
  file_path?: string;
74
+ file?: string;
73
75
  oldText?: string;
74
76
  newText?: string;
75
77
  patch?: string;
@@ -83,16 +85,9 @@ interface EditRenderArgs {
83
85
  */
84
86
  previewDiff?: string;
85
87
  // Hashline mode fields
86
- edits?: HashlineEditPreview[];
88
+ edits?: Partial<HashlineToolEdit>[];
87
89
  }
88
90
 
89
- type HashlineEditPreview =
90
- | { op: "replace"; tag: string; content: string[] }
91
- | { op: "replace"; first: string; last: string; content: string[] }
92
- | { op: "append"; after?: string; content: string[] }
93
- | { op: "prepend"; before?: string; content: string[] }
94
- | { op: "insert"; before: string; after: string; content: string[] };
95
-
96
91
  /** Extended context for edit tool rendering */
97
92
  export interface EditRenderContext {
98
93
  /** Pre-computed diff preview (computed before tool executes) */
@@ -123,7 +118,7 @@ function formatStreamingDiff(diff: string, rawPath: string, uiTheme: Theme, labe
123
118
  return text;
124
119
  }
125
120
 
126
- function formatStreamingHashlineEdits(edits: unknown[], uiTheme: Theme): string {
121
+ function formatStreamingHashlineEdits(edits: Partial<HashlineToolEdit>[], uiTheme: Theme): string {
127
122
  const MAX_EDITS = 4;
128
123
  const MAX_DST_LINES = 8;
129
124
  let text = "\n\n";
@@ -158,34 +153,25 @@ function formatStreamingHashlineEdits(edits: unknown[], uiTheme: Theme): string
158
153
  }
159
154
 
160
155
  return text.trimEnd();
161
- function formatHashlineEdit(edit: unknown): { srcLabel: string; dst: string } {
162
- const editRecord = typeof edit === "object" && edit !== null ? (edit as Record<string, unknown>) : undefined;
163
- if (!editRecord) {
156
+ function formatHashlineEdit(edit: Partial<HashlineToolEdit>): { srcLabel: string; dst: string } {
157
+ if (typeof edit !== "object" || !edit) {
164
158
  return { srcLabel: "• (incomplete edit)", dst: "" };
165
159
  }
166
160
 
167
- const contentLines = Array.isArray(editRecord.content) ? (editRecord.content as string[]).join("\n") : "";
161
+ const contentLines = Array.isArray(edit.lines) ? (edit.lines as string[]).join("\n") : "";
168
162
 
169
- // replace with tag (single line)
170
- if ("tag" in editRecord && !("first" in editRecord)) {
171
- const tag = typeof editRecord.tag === "string" ? editRecord.tag : "…";
172
- return { srcLabel: `• line ${tag}`, dst: contentLines };
173
- }
174
- // replace with first..last (range)
175
- if ("first" in editRecord || "last" in editRecord) {
176
- const first = typeof editRecord.first === "string" ? editRecord.first : "…";
177
- const last = typeof editRecord.last === "string" ? editRecord.last : "…";
178
- return { srcLabel: `• range ${first}..${last}`, dst: contentLines };
163
+ const op = typeof edit.op === "string" ? edit.op : "?";
164
+ const pos = typeof edit.pos === "string" ? edit.pos : undefined;
165
+ const end = typeof edit.end === "string" ? edit.end : undefined;
166
+
167
+ if (pos && end && pos !== end) {
168
+ return { srcLabel: `• ${op} ${pos}…${end}`, dst: contentLines };
179
169
  }
180
- // append/prepend/insert
181
- if ("before" in editRecord || "after" in editRecord) {
182
- const after = typeof editRecord.after === "string" ? editRecord.after : undefined;
183
- const before = typeof editRecord.before === "string" ? editRecord.before : undefined;
184
- const refs = [after, before].filter(Boolean).join("..") || "…";
185
- return { srcLabel: `• insert ${refs}`, dst: contentLines };
170
+ const anchor = pos ?? end;
171
+ if (anchor) {
172
+ return { srcLabel: `\u2022 ${op} ${anchor}`, dst: contentLines };
186
173
  }
187
-
188
- return { srcLabel: "• (incomplete edit)", dst: "" };
174
+ return { srcLabel: `\u2022 ${op} (file-level)`, dst: contentLines };
189
175
  }
190
176
  }
191
177
  function formatMetadataLine(lineCount: number | null, language: string | undefined, uiTheme: Theme): string {
@@ -234,7 +220,7 @@ export const editToolRenderer = {
234
220
  mergeCallAndResult: true,
235
221
 
236
222
  renderCall(args: EditRenderArgs, options: RenderResultOptions, uiTheme: Theme): Component {
237
- const rawPath = args.file_path || args.path || "";
223
+ const rawPath = args.file_path || args.path || args.file || "";
238
224
  const filePath = shortenPath(rawPath);
239
225
  const editLanguage = getLanguageFromPath(rawPath) ?? "text";
240
226
  const editIcon = uiTheme.fg("muted", uiTheme.getLangIcon(editLanguage));
@@ -289,13 +275,13 @@ export const editToolRenderer = {
289
275
  uiTheme: Theme,
290
276
  args?: EditRenderArgs,
291
277
  ): Component {
292
- const rawPath = args?.file_path || args?.path || "";
278
+ const rawPath = args?.file_path || args?.path || args?.file || "";
293
279
  const filePath = shortenPath(rawPath);
294
280
  const editLanguage = getLanguageFromPath(rawPath) ?? "text";
295
281
  const editIcon = uiTheme.fg("muted", uiTheme.getLangIcon(editLanguage));
296
282
 
297
283
  const op = args?.op || result.details?.op;
298
- const rename = args?.rename || result.details?.rename;
284
+ const rename = args?.rename || result.details?.move;
299
285
  const opTitle = op === "create" ? "Create" : op === "delete" ? "Delete" : "Edit";
300
286
 
301
287
  // Pre-compute metadata line (static across renders)
@@ -5,5 +5,5 @@ Rules:
5
5
  - MUST describe the changes made, not the process
6
6
  - MUST NOT mention running tests, builds, or other validation steps
7
7
  - MUST NOT explain what the user asked for
8
- - MUST write in first person (I added..., I fixed...)
8
+ - MUST write in first person (I added…, I fixed)
9
9
  - MUST NOT ask questions
@@ -33,25 +33,25 @@ When a user describes what they want an agent to do, you will:
33
33
  <function call omitted for brevity only for this example>
34
34
  <commentary>
35
35
  Since a significant piece of code was written, use the {{TASK_TOOL_NAME}} tool to launch the test-runner agent to run the tests.
36
- </commentary>
36
+ </commentary>
37
37
  assistant: "Now let me use the test-runner agent to run the tests"
38
- </example>
38
+ </example>
39
39
  - <example>
40
40
  Context: User is creating an agent to respond to the word "hello" with a friendly jok.
41
41
  user: "Hello"
42
42
  assistant: "I'm going to use the {{TASK_TOOL_NAME}} tool to launch the greeting-responder agent to respond with a friendly joke"
43
43
  <commentary>
44
44
  Since the user is greeting, use the greeting-responder agent to respond with a friendly joke.
45
- </commentary>
46
- </example>
45
+ </commentary>
46
+ </example>
47
47
  - If the user mentioned or implied that the agent should be used proactively, you SHOULD include examples of this.
48
48
  - NOTE: You MUST ensure that in the examples, you are making the assistant use the Agent tool and MUST NOT simply respond directly to the task.
49
49
 
50
50
  Your output MUST be a valid JSON object with exactly these fields:
51
51
  {
52
52
  "identifier": "A unique, descriptive identifier using lowercase letters, numbers, and hyphens (e.g., 'test-runner', 'api-docs-writer', 'code-formatter')",
53
- "whenToUse": "A precise, actionable description starting with 'Use this agent when...' that clearly defines the triggering conditions and use cases. Ensure you include examples as described above.",
54
- "systemPrompt": "The complete system prompt that will govern the agent's behavior, written in second person ('You are...', 'You will...') and structured for maximum clarity and effectiveness"
53
+ "whenToUse": "A precise, actionable description starting with 'Use this agent when' that clearly defines the triggering conditions and use cases. Ensure you include examples as described above.",
54
+ "systemPrompt": "The complete system prompt that will govern the agent's behavior, written in second person ('You are', 'You will') and structured for maximum clarity and effectiveness"
55
55
  }
56
56
 
57
57
  Key principles for your system prompts:
@@ -90,7 +90,7 @@ Semantic questions MUST be answered with semantic tools.
90
90
  {{#has tools "ssh"}}
91
91
  ### SSH: match commands to host shell
92
92
  Commands MUST match the host shell. linux/bash, macos/zsh: Unix. windows/cmd: dir, type, findstr. windows/powershell: Get-ChildItem, Get-Content.
93
- Remote filesystems: `~/.omp/remote/<hostname>/`. Windows paths need colons: `C:/Users/...`
93
+ Remote filesystems: `~/.omp/remote/<hostname>/`. Windows paths need colons: `C:/Users/…`
94
94
  {{/has}}
95
95
 
96
96
  {{#ifAny (includes tools "grep") (includes tools "find")}}
@@ -3,7 +3,7 @@
3
3
  Executes bash command in shell session for terminal operations like git, bun, cargo, python.
4
4
 
5
5
  <instruction>
6
- - You MUST use `cwd` parameter to set working directory instead of `cd dir && ...`
6
+ - You MUST use `cwd` parameter to set working directory instead of `cd dir && …`
7
7
  - PTY mode is opt-in: set `pty: true` only when command expects a real terminal (for example `sudo`, `ssh` where you need input from the user); default is `false`
8
8
  - You MUST use `;` only when later commands should run regardless of earlier failures
9
9
  - `skill://` URIs are auto-resolved to filesystem paths before execution
@@ -13,6 +13,15 @@ Fast file pattern matching that works with any codebase size.
13
13
  Matching file paths sorted by modification time (most recent first). Results truncated at 1000 entries or 50KB (configurable via `limit`).
14
14
  </output>
15
15
 
16
+ <example name="find files">
17
+ ```
18
+ {
19
+ "pattern": "src/**/*.ts",
20
+ "limit": 1000
21
+ }
22
+ ```
23
+ </example>
24
+
16
25
  <avoid>
17
26
  For open-ended searches requiring multiple rounds of globbing and grepping, you MUST use Task tool instead.
18
27
  </avoid>
@@ -1,96 +1,115 @@
1
1
  # Edit
2
2
 
3
- Apply precise file edits using `LINE#ID` tags, anchoring to the file content.
3
+ Apply precise file edits using `LINE#ID` tags from `read` output.
4
4
 
5
5
  <workflow>
6
- 1. You MUST `read` the target range to capture current `LINE#ID` tags.
7
- 2. You MUST pick the smallest operation per change site (line/range/insert/content-replace).
8
- 3. You MUST direction-lock every edit: exact current text intended text.
9
- 4. You MUST submit one `edit` call per file containing all operations.
10
- 5. If another edit is needed in that file, you MUST re-read first (hashes changed).
11
- 6. You MUST output tool calls only; no prose.
6
+ 1. You SHOULD issue a `read` call before editing if you have no tagged context for a file.
7
+ 2. You MUST pick the smallest operation per change site.
8
+ 3. You MUST submit one `edit` call per file with all operations, think your changes through before submitting.
12
9
  </workflow>
13
10
 
14
11
  <operations>
15
- - **Line or range replace/delete**
16
- - `{ op: "replace", tag: "N#ID", content: […] }`
17
- - `{ op: "replace", first: "N#ID", last: "N#ID", content: […] }`
18
- - Use for swaps, block rewrites, or deleting a full span (`content: null`).
19
- - **Insert** (new content)
20
- - `{ op: "prepend", before: "N#ID", content: […] }` or `{ op: "prepend", content: […] }` (no `before` = insert at beginning of file)
21
- - `{ op: "append", after: "N#ID", content: […] }` or `{ op: "append", content: […] }` (no `after` = insert at end of file)
22
- - `{ op: "insert", after: "N#ID", before: "N#ID", content: […] }` (between adjacent anchors; safest for blocks)
23
- - **File-level controls**
24
- - `{ delete: true, edits: [] }` deletes the file (cannot be combined with `rename`).
25
- - `{ rename: "new/path.ts", edits: [] }` writes result to new path and removes old path.
26
- **Atomicity:** all ops validate against the same pre-edit file snapshot; refs are interpreted against last `read`; applicator applies bottom-up.
12
+ Every edit has `op`, `pos`, and `lines`. Range replaces also have `end`. Both `pos` and `end` use `"N#ID"` format (e.g. `"23#XY"`).
13
+ **`pos`** the anchor line. Meaning depends on `op`:
14
+ - `replace`: start of range (or the single line to replace)
15
+ - `prepend`: insert new lines **before** this line; omit for beginning of file
16
+ - `append`: insert new lines **after** this line; omit for end of file
17
+ **`end`** range replace only. The last line of the range (inclusive). Omit for single-line replace.
18
+ **`lines`** the replacement content:
19
+ - `["line1", "line2"]` replace with these lines (array of strings)
20
+ - `"line1"` — shorthand for `["line1"]` (single-line replace)
21
+ - `[""]` replace content with a blank line (line preserved, content cleared)
22
+ - `null` or `[]` **delete** the line(s) entirely
23
+
24
+ ### Line or range replace/delete
25
+ - `{ file: "…", edits: [{ op: "replace", pos: "N#ID", lines: null }] }` — delete one line
26
+ - `{ file: "…", edits: [{ op: "replace", pos: "N#ID", end: "M#ID", lines: null }] }` — delete a range
27
+ - `{ file: "…", edits: [{ op: "replace", pos: "N#ID", lines: […] }] }` — replace one line
28
+ - `{ file: "…", edits: [{ op: "replace", pos: "N#ID", end: "M#ID", lines: […] }] }` — replace a range
29
+
30
+ ### Insert new lines
31
+ - `{ file: "…", edits: [{ op: "prepend", pos: "N#ID", lines: […] }] }` — insert before tagged line
32
+ - `{ file: "…", edits: [{ op: "prepend", lines: […] }] }` — insert at beginning of file (no tag)
33
+ - `{ file: "…", edits: [{ op: "append", pos: "N#ID", lines: […] }] }` — insert after tagged line
34
+ - `{ file: "…", edits: [{ op: "append", lines: […] }] }` — insert at end of file (no tag)
35
+
36
+ ### File-level controls
37
+ - `{ file: "…", delete: true, edits: [] }` — delete the file
38
+ - `{ file: "…", move: "new/path.ts", edits: […] }` — move file to new path (edits applied first)
39
+ **Atomicity:** all ops in one call validate against the same pre-edit snapshot; tags reference the last `read`. Edits are applied bottom-up, so earlier tags stay valid even when later ops add or remove lines.
27
40
  </operations>
28
41
 
29
42
  <rules>
30
- 1. **Minimize scope:** You MUST use one logical mutation site per operation.
31
- 2. **Preserve formatting:** You MUST keep indentation, punctuation, line breaks, trailing commas, brace style.
32
- 3. **Prefer insertion over neighbor rewrites:** You SHOULD anchor on structural boundaries (`}`, `]`, `},`) not interior property lines.
33
- 4. **No no-ops:** replacement content MUST differ from current content.
34
- 5. **Touch only requested code:** You MUST NOT make incidental edits.
35
- 6. **Use exact current tokens:** You MUST NOT rewrite approximately; mutate the token that exists now.
36
- 7. **For swaps/moves:** You SHOULD prefer one range operation over multiple single-line operations.
43
+ 1. **Minimize scope:** You MUST use one logical mutation per operation.
44
+ 2. **No no-ops:** replacement MUST differ from current.
45
+ 3. **Prefer insertion over neighbor rewrites:** You SHOULD anchor on structural boundaries (`}`, `]`, `},`), not interior lines.
46
+ 4. **For swaps/moves:** You SHOULD prefer one range op over multiple single-line ops.
37
47
  </rules>
38
48
 
39
- <op-choice>
40
- - One wrong line → MUST use `set`
41
- - Adjacent block changed → MUST use `insert`
42
- - Missing line/block → MUST use `append`/`prepend`
43
- </op-choice>
44
-
45
- <tag-choice>
46
- - You MUST copy tags exactly from the prefix of the `read` or error output.
47
- - You MUST NOT guess tags.
48
- - For inserts, you SHOULD prefer `insert` > `append`/`prepend` when both boundaries are known.
49
- - You MUST re-read after each successful edit call before issuing another on same file.
50
- </tag-choice>
51
-
52
49
  <recovery>
53
- **Tag mismatch (`>>>`)**
54
- - You MUST retry with the updated tags shown in error output.
55
- - You MUST re-read only if required tags are missing from error snippet.
56
- - If mismatch repeats, you MUST stop and re-read the exact block.
50
+ **Tag mismatch (`>>>`):** You MUST retry using fresh tags from the error snippet. Re-read only if snippet lacks context.
51
+ **No-op (`identical`):** You MUST NOT resubmit. Re-read target lines and adjust the edit.
57
52
  </recovery>
58
53
 
59
- <example name="fix a value or type">
54
+ <example name="single-line replace">
60
55
  ```ts
61
56
  {{hlinefull 23 " const timeout: number = 5000;"}}
62
57
  ```
63
58
  ```
64
- op: "replace"
65
- tag: "{{hlineref 23 " const timeout: number = 5000;"}}"
66
- content: [" const timeout: number = 30_000;"]
59
+ {
60
+ file: "",
61
+ edits: [{
62
+ op: "replace",
63
+ pos: "{{hlineref 23 " const timeout: number = 5000;"}}",
64
+ lines: [" const timeout: number = 30_000;"]
65
+ }]
66
+ }
67
67
  ```
68
68
  </example>
69
69
 
70
- <example name="remove a line entirely">
71
- ```ts
72
- {{hlinefull 7 "// @ts-ignore"}}
73
- {{hlinefull 8 "const data = fetchSync(url);"}}
74
- ```
75
- ```
76
- op: "replace"
77
- tag: "{{hlineref 7 "// @ts-ignore"}}"
78
- content: null
70
+ <example name="delete lines">
71
+ Single line — `lines: null` deletes entirely:
72
+ ```
73
+ {
74
+ file: "…",
75
+ edits: [{
76
+ op: "replace",
77
+ pos: "{{hlineref 7 "// @ts-ignore"}}",
78
+ lines: null
79
+ }]
80
+ }
81
+ ```
82
+ Range — add `end`:
83
+ ```
84
+ {
85
+ file: "…",
86
+ edits: [{
87
+ op: "replace",
88
+ pos: "{{hlineref 80 " // TODO: remove after migration"}}",
89
+ end: "{{hlineref 83 " }"}}",
90
+ lines: null
91
+ }]
92
+ }
79
93
  ```
80
94
  </example>
81
95
 
82
- <example name="clear content but keep the line break">
96
+ <example name="clear text but keep the line break">
83
97
  ```ts
84
98
  {{hlinefull 14 " placeholder: \"DO NOT SHIP\","}}
85
99
  ```
86
100
  ```
87
- op: "replace"
88
- tag: "{{hlineref 14 " placeholder: \"DO NOT SHIP\","}}"
89
- content: [""]
101
+ {
102
+ file: "",
103
+ edits: [{
104
+ op: "replace",
105
+ pos: "{{hlineref 14 " placeholder: \"DO NOT SHIP\","}}",
106
+ lines: [""]
107
+ }]
108
+ }
90
109
  ```
91
110
  </example>
92
111
 
93
- <example name="rewrite a block of logic">
112
+ <example name="rewrite a block">
94
113
  ```ts
95
114
  {{hlinefull 60 " } catch (err) {"}}
96
115
  {{hlinefull 61 " console.error(err);"}}
@@ -98,117 +117,74 @@ content: [""]
98
117
  {{hlinefull 63 " }"}}
99
118
  ```
100
119
  ```
101
- op: "replace"
102
- first: "{{hlineref 60 " } catch (err) {"}}"
103
- last: "{{hlineref 63 " }"}}"
104
- content: [" } catch (err) {", " if (isEnoent(err)) return null;", " throw err;", " }"]
105
- ```
106
- </example>
107
-
108
- <example name="remove a full block">
109
- ```ts
110
- {{hlinefull 80 " // TODO: remove after migration"}}
111
- {{hlinefull 81 " if (legacy) {"}}
112
- {{hlinefull 82 " legacyHandler(req);"}}
113
- {{hlinefull 83 " }"}}
114
- ```
115
- ```
116
- op: "replace"
117
- first: "{{hlineref 80 " // TODO: remove after migration"}}"
118
- last: "{{hlineref 83 " }"}}"
119
- content: null
120
+ {
121
+ file: "",
122
+ edits: [{
123
+ op: "replace",
124
+ pos: "{{hlineref 60 " } catch (err) {"}}",
125
+ end: "{{hlineref 63 " }"}}",
126
+ lines: [
127
+ " } catch (err) {",
128
+ " if (isEnoent(err)) return null;",
129
+ " throw err;",
130
+ " }"
131
+ ]
132
+ }]
133
+ }
120
134
  ```
121
135
  </example>
122
136
 
123
- <example name="add an import above the first import">
137
+ <example name="insert between siblings">
124
138
  ```ts
125
- {{hlinefull 1 "import * as fs from \"node:fs/promises\";"}}
126
- {{hlinefull 2 "import * as path from \"node:path\";"}}
127
- ```
128
- ```
129
- op: "prepend"
130
- before: "{{hlineref 1 "import * as fs from \"node:fs/promises\";"}}"
131
- content: ["import * as os from \"node:os\";"]
132
- ```
133
- Use `before` for anchored insertion before a specific line. Omit `before` to prepend at BOF.
134
- </example>
135
-
136
- <example name="append at end of file">
137
- ```ts
138
- {{hlinefull 260 "export { serialize, deserialize };"}}
139
+ {{hlinefull 44 " \"build\": \"bun run compile\","}}
140
+ {{hlinefull 45 " \"test\": \"bun test\""}}
139
141
  ```
140
142
  ```
141
- op: "append"
142
- after: "{{hlineref 260 "export { serialize, deserialize };"}}"
143
- content: ["export { validate };"]
143
+ {
144
+ file: "",
145
+ edits: [{
146
+ op: "prepend",
147
+ pos: "{{hlineref 45 " \"test\": \"bun test\""}}",
148
+ lines: [" \"lint\": \"biome check\","]
149
+ }]
150
+ }
144
151
  ```
145
- Use `after` for anchored insertion after a specific line. Omit `after` to append at EOF.
146
- </example>
147
-
148
- <example name="add an entry between known siblings">
152
+ Result:
149
153
  ```ts
150
154
  {{hlinefull 44 " \"build\": \"bun run compile\","}}
151
- {{hlinefull 45 " \"test\": \"bun test\""}}
152
- ```
153
- ```
154
- op: "insert"
155
- after: "{{hlineref 44 " \"build\": \"bun run compile\","}}"
156
- before: "{{hlineref 45 " \"test\": \"bun test\""}}"
157
- content: [" \"lint\": \"biome check\","]
155
+ {{hlinefull 45 " \"lint\": \"biome check\","}}
156
+ {{hlinefull 46 " \"test\": \"bun test\""}}
158
157
  ```
159
- Dual anchors pin the insert to exactly one gap, preventing drift from edits elsewhere in the file. **Always prefer dual anchors when both boundaries are content lines.**
160
158
  </example>
161
159
 
162
- <example name="insert a function before another function">
160
+ <example name="anchor to structure, not whitespace">
161
+ Trailing `""` in `lines` preserves blank-line separators. Anchor to the structural line, not the blank line above — blank lines are ambiguous and shift.
163
162
  ```ts
164
- {{hlinefull 100 " return buf.toString(\"hex\");"}}
165
163
  {{hlinefull 101 "}"}}
166
164
  {{hlinefull 102 ""}}
167
165
  {{hlinefull 103 "export function serialize(data: unknown): string {"}}
168
166
  ```
169
- ```
170
- op: "insert"
171
- before: "{{hlineref 103 "export function serialize(data: unknown): string {"}}"
172
- content: ["function validate(data: unknown): boolean {", " return data != null && typeof data === \"object\";", "}", ""]
173
- ```
174
- The trailing `""` in `content` preserves the blank-line separator. **Anchor to the structural line (`export function ...`), not the blank line above it** — blank lines are ambiguous and may be added or removed by other edits.
175
- </example>
176
-
177
- <example name="file delete">
178
- ```
179
- path: "src/deprecated/legacy.ts"
180
- delete: true
181
- ```
182
- </example>
183
-
184
- <example name="file rename with edits">
185
- ```
186
- path: "src/utils.ts"
187
- rename: "src/helpers/utils.ts"
188
- edits: […]
189
- ```
190
- </example>
191
-
192
- <example name="anti-pattern: anchoring to whitespace">
193
- Bad — tags to a blank line; fragile if blank lines shift:
194
- ```
195
- after: "{{hlineref 102 ""}}"
196
- content: ["function validate() {", …, "}"]
197
- ```
198
-
199
- Good — anchors to the structural target:
200
-
201
- ```
202
- before: "{{hlineref 103 "export function serialize(data: unknown): string {"}}"
203
- content: ["function validate() {", …, "}"]
167
+ Bad — append after "}"
168
+ Good — anchors to structural line:
169
+ ```
170
+ {
171
+ file: "…",
172
+ edits: [{
173
+ op: "prepend",
174
+ pos: "{{hlineref 103 "export function serialize(data: unknown): string {"}}",
175
+ lines: [
176
+ "function validate(data: unknown): boolean {",
177
+ " return data != null && typeof data === \"object\";",
178
+ "}",
179
+ ""
180
+ ]
181
+ }]
182
+ }
204
183
  ```
205
184
  </example>
206
185
 
207
186
  <critical>
208
- You MUST ensure:
209
- - Payload shape is `{ "path": string, "edits": [operation, …], "delete"?: boolean, "rename"?: string }`
210
- - Every edit MUST match exactly one variant
211
- - Every tag MUST be copied EXACTLY from a tool result as `N#ID`
212
- - Scope MUST be minimal and formatting MUST be preserved except targeted token changes
213
- </critical>
214
- **Final reminder:** tags are immutable references to the last read snapshot. You MUST re-read when state changes, then edit.
187
+ - Edit payload: `{ file, edits[] }`. Each entry: `op`, `lines`, optional `pos`/`end`. No extra keys.
188
+ - Every tag MUST be copied exactly from fresh tool result as `N#ID`.
189
+ - You MUST re-read after each edit call before issuing another on same file.
190
+ </critical>
@@ -60,7 +60,7 @@ edit {"path":"src/app.py","op":"update","diff":"@@ def greet():\n def greet():\n
60
60
  </example>
61
61
 
62
62
  <example name="rename">
63
- edit {"path":"src/app.py","op":"update","rename":"src/main.py","diff":"@@\n ...\n"}
63
+ edit {"path":"src/app.py","op":"update","rename":"src/main.py","diff":"@@\n …\n"}
64
64
  </example>
65
65
 
66
66
  <example name="delete">
@@ -36,8 +36,8 @@ All helpers auto-print results and return values for chaining.
36
36
  <output>
37
37
  User sees output like Jupyter notebook; rich displays render fully:
38
38
  - `display(JSON(data))` → interactive JSON tree
39
- - `display(HTML(...))` → rendered HTML
40
- - `display(Markdown(...))` → formatted markdown
39
+ - `display(HTML())` → rendered HTML
40
+ - `display(Markdown())` → formatted markdown
41
41
  - `plt.show()` → inline figures
42
42
  **You will see object repr** (e.g., `<IPython.core.display.JSON object>`). Trust `display()`; you MUST NOT assume user sees only repr.
43
43
  </output>
@@ -24,7 +24,7 @@ For position-addressed or pattern-addressed changes, bash more efficient:
24
24
 
25
25
  |Operation|Command|
26
26
  |---|---|
27
- |Append to file|`cat >> file <<'EOF'`...`EOF`|
27
+ |Append to file|`cat >> file <<'EOF'`…`EOF`|
28
28
  |Prepend to file|`{ cat - file; } <<'EOF' > tmp && mv tmp file`|
29
29
  |Delete lines N-M|`sed -i 'N,Md' file`|
30
30
  |Insert after line N|`sed -i 'Na\text' file`|