@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
@@ -34,9 +34,8 @@ import { enforcePlanModeWrite, resolvePlanPath } from "../tools/plan-mode-guard"
34
34
  import { applyPatch } from "./applicator";
35
35
  import { generateDiffString, generateUnifiedDiffString, replaceText } from "./diff";
36
36
  import { findMatch } from "./fuzzy";
37
- import { applyHashlineEdits, computeLineHash, type HashlineEdit, type LineTag, parseTag } from "./hashline";
37
+ import { type Anchor, applyHashlineEdits, computeLineHash, type HashlineEdit, parseTag } from "./hashline";
38
38
  import { detectLineEnding, normalizeToLF, restoreLineEndings, stripBom } from "./normalize";
39
- import { buildNormativeUpdateInput } from "./normative";
40
39
  import { type EditToolDetails, getLspBatchRequest } from "./shared";
41
40
  // Internal imports
42
41
  import type { FileSystem, Operation, PatchInput } from "./types";
@@ -109,14 +108,14 @@ export { ApplyPatchError, EditMatchError, ParseError } from "./types";
109
108
  // ═══════════════════════════════════════════════════════════════════════════
110
109
 
111
110
  const replaceEditSchema = Type.Object({
112
- path: Type.String({ description: "File path (relative or absolute)" }),
111
+ file: Type.String({ description: "File path (relative or absolute)" }),
113
112
  old_text: Type.String({ description: "Text to find (fuzzy whitespace matching enabled)" }),
114
113
  new_text: Type.String({ description: "Replacement text" }),
115
114
  all: Type.Optional(Type.Boolean({ description: "Replace all occurrences (default: unique match required)" })),
116
115
  });
117
116
 
118
117
  const patchEditSchema = Type.Object({
119
- path: Type.String({ description: "File path" }),
118
+ file: Type.String({ description: "File path" }),
120
119
  op: Type.Optional(
121
120
  StringEnum(["create", "delete", "update"], {
122
121
  description: "Operation (default: update)",
@@ -168,25 +167,7 @@ export function stripNewLinePrefixes(lines: string[]): string[] {
168
167
  });
169
168
  }
170
169
 
171
- const hashlineReplaceContentFormat = (kind: string) =>
172
- Type.Union([
173
- Type.Null(),
174
- Type.Array(Type.String(), { description: `${kind} lines` }),
175
- Type.String({ description: `${kind} line` }),
176
- ]);
177
-
178
- const hashlineInsertContentFormat = (kind: string) =>
179
- Type.Union([
180
- Type.Array(Type.String(), { description: `${kind} lines`, minItems: 1 }),
181
- Type.String({ description: `${kind} line`, minLength: 1 }),
182
- ]);
183
-
184
- const hashlineTagFormat = (what: string) =>
185
- Type.String({
186
- description: `Tag identifying the ${what} in "LINE#ID" format`,
187
- });
188
-
189
- export function hashlineParseContent(edit: string | string[] | null): string[] {
170
+ export function hashlineParseText(edit: string[] | string | null): string[] {
190
171
  if (edit === null) return [];
191
172
  if (Array.isArray(edit)) return edit;
192
173
  const lines = stripNewLinePrefixes(edit.split("\n"));
@@ -194,76 +175,91 @@ export function hashlineParseContent(edit: string | string[] | null): string[] {
194
175
  if (lines[lines.length - 1].trim() === "") return lines.slice(0, -1);
195
176
  return lines;
196
177
  }
197
- const hashlineReplaceTagEditSchema = Type.Object(
198
- {
199
- op: Type.Literal("replace"),
200
- tag: hashlineTagFormat("line being replaced"),
201
- content: hashlineReplaceContentFormat("Replacement"),
202
- },
203
- { additionalProperties: false },
204
- );
205
178
 
206
- const hashlineAppendEditSchema = Type.Object(
207
- {
208
- op: Type.Literal("append"),
209
- after: Type.Optional(hashlineTagFormat("line after which to append")),
210
- content: hashlineInsertContentFormat("Appended"),
211
- },
212
- { additionalProperties: false },
213
- );
214
-
215
- const hashlinePrependEditSchema = Type.Object(
179
+ const hashlineEditSchema = Type.Object(
216
180
  {
217
- op: Type.Literal("prepend"),
218
- before: Type.Optional(hashlineTagFormat("line before which to prepend")),
219
- content: hashlineInsertContentFormat("Prepended"),
181
+ op: StringEnum(["replace", "append", "prepend"]),
182
+ pos: Type.Optional(Type.String({ description: "anchor" })),
183
+ end: Type.Optional(Type.String({ description: "limit position" })),
184
+ lines: Type.Union([
185
+ Type.Array(Type.String(), { description: "content (preferred format)" }),
186
+ Type.String(),
187
+ Type.Null(),
188
+ ]),
220
189
  },
221
190
  { additionalProperties: false },
222
191
  );
223
192
 
224
- const hashlineReplaceRangeEditSchema = Type.Object(
193
+ const hashlineEditParamsSchema = Type.Object(
225
194
  {
226
- op: Type.Literal("replace"),
227
- first: hashlineTagFormat("first line"),
228
- last: hashlineTagFormat("last line"),
229
- content: hashlineReplaceContentFormat("Replacement"),
195
+ file: Type.String({ description: "path" }),
196
+ edits: Type.Array(hashlineEditSchema, { description: "edits over $file" }),
197
+ delete: Type.Optional(Type.Boolean({ description: "If true, delete $file" })),
198
+ move: Type.Optional(Type.String({ description: "If set, move $file to $move" })),
230
199
  },
231
200
  { additionalProperties: false },
232
201
  );
233
202
 
234
- const hashlineInsertEditSchema = Type.Object(
235
- {
236
- op: Type.Literal("insert"),
237
- before: Type.Optional(hashlineTagFormat("line before which to insert")),
238
- after: Type.Optional(hashlineTagFormat("line after which to insert")),
239
- content: hashlineInsertContentFormat("Inserted"),
240
- },
241
- { additionalProperties: false },
242
- );
203
+ export type HashlineToolEdit = Static<typeof hashlineEditSchema>;
204
+ export type HashlineParams = Static<typeof hashlineEditParamsSchema>;
243
205
 
244
- const hashlineEditSpecSchema = Type.Union([
245
- hashlineReplaceTagEditSchema,
246
- hashlineReplaceRangeEditSchema,
247
- hashlineAppendEditSchema,
248
- hashlinePrependEditSchema,
249
- hashlineInsertEditSchema,
250
- ]);
206
+ // ═══════════════════════════════════════════════════════════════════════════
207
+ // Resilient anchor resolution
208
+ // ═══════════════════════════════════════════════════════════════════════════
251
209
 
252
- const hashlineEditSchema = Type.Object(
253
- {
254
- path: Type.String({ description: "File path (relative or absolute)" }),
255
- edits: Type.Array(hashlineEditSpecSchema, {
256
- description: "Changes to apply to the file at `path`",
257
- minItems: 0,
258
- }),
259
- delete: Type.Optional(Type.Boolean({ description: "Delete the file when true" })),
260
- rename: Type.Optional(Type.String({ description: "New path if moving" })),
261
- },
262
- { additionalProperties: false },
263
- );
210
+ /**
211
+ * Map flat tool-schema edits (tag/end) into typed HashlineEdit objects.
212
+ *
213
+ * Resilient: as long as at least one anchor exists, we execute.
214
+ * - replace + tag only single-line replace
215
+ * - replace + tag + end → range replace
216
+ * - append + tag or end → append after that anchor
217
+ * - prepend + tag or end → prepend before that anchor
218
+ * - no anchors file-level append/prepend (only for those ops)
219
+ *
220
+ * Unknown ops default to "replace".
221
+ */
222
+ function resolveEditAnchors(edits: HashlineToolEdit[]): HashlineEdit[] {
223
+ const result: HashlineEdit[] = [];
224
+ for (const edit of edits) {
225
+ const lines = hashlineParseText(edit.lines);
226
+ const tag = edit.pos ? tryParseTag(edit.pos) : undefined;
227
+ const end = edit.end ? tryParseTag(edit.end) : undefined;
228
+
229
+ // Normalize op — default unknown values to "replace"
230
+ const op = edit.op === "append" || edit.op === "prepend" ? edit.op : "replace";
231
+ switch (op) {
232
+ case "replace": {
233
+ if (tag && end) {
234
+ result.push({ op: "replace", pos: tag, end, lines });
235
+ } else if (tag || end) {
236
+ result.push({ op: "replace", pos: tag || end!, lines });
237
+ } else {
238
+ throw new Error("Replace requires at least one anchor (tag or end).");
239
+ }
240
+ break;
241
+ }
242
+ case "append": {
243
+ result.push({ op: "append", pos: tag ?? end, lines });
244
+ break;
245
+ }
246
+ case "prepend": {
247
+ result.push({ op: "prepend", pos: end ?? tag, lines });
248
+ break;
249
+ }
250
+ }
251
+ }
252
+ return result;
253
+ }
264
254
 
265
- export type HashlineToolEdit = Static<typeof hashlineEditSpecSchema>;
266
- export type HashlineParams = Static<typeof hashlineEditSchema>;
255
+ /** Parse a tag, returning undefined instead of throwing on garbage. */
256
+ function tryParseTag(raw: string): Anchor | undefined {
257
+ try {
258
+ return parseTag(raw);
259
+ } catch {
260
+ return undefined;
261
+ }
262
+ }
267
263
 
268
264
  // ═══════════════════════════════════════════════════════════════════════════
269
265
  // LSP FileSystem for patch mode
@@ -353,11 +349,11 @@ function mergeDiagnosticsWithWarnings(
353
349
  // Tool Class
354
350
  // ═══════════════════════════════════════════════════════════════════════════
355
351
 
356
- type TInput = typeof replaceEditSchema | typeof patchEditSchema | typeof hashlineEditSchema;
352
+ type TInput = typeof replaceEditSchema | typeof patchEditSchema | typeof hashlineEditParamsSchema;
357
353
 
358
354
  export type EditMode = "replace" | "patch" | "hashline";
359
355
 
360
- export const DEFAULT_EDIT_MODE: EditMode = "patch";
356
+ export const DEFAULT_EDIT_MODE: EditMode = "hashline";
361
357
 
362
358
  export function normalizeEditMode(mode?: string | null): EditMode | null {
363
359
  switch (mode) {
@@ -382,6 +378,7 @@ export class EditTool implements AgentTool<TInput> {
382
378
  readonly label = "Edit";
383
379
  readonly nonAbortable = true;
384
380
  readonly concurrency = "exclusive";
381
+ readonly strict = true;
385
382
 
386
383
  readonly #allowFuzzy: boolean;
387
384
  readonly #fuzzyThreshold: number;
@@ -473,7 +470,7 @@ export class EditTool implements AgentTool<TInput> {
473
470
  case "patch":
474
471
  return patchEditSchema;
475
472
  case "hashline":
476
- return hashlineEditSchema;
473
+ return hashlineEditParamsSchema;
477
474
  default:
478
475
  return replaceEditSchema;
479
476
  }
@@ -492,21 +489,20 @@ export class EditTool implements AgentTool<TInput> {
492
489
  // Hashline mode execution
493
490
  // ─────────────────────────────────────────────────────────────────
494
491
  if (this.mode === "hashline") {
495
- const { path, edits, delete: deleteFile, rename } = params as HashlineParams;
492
+ const { file: path, edits, delete: deleteFile, move } = params as HashlineParams;
496
493
 
497
- enforcePlanModeWrite(this.session, path, { op: deleteFile ? "delete" : "update", rename });
494
+ enforcePlanModeWrite(this.session, path, { op: deleteFile ? "delete" : "update", move });
498
495
 
499
496
  if (path.endsWith(".ipynb") && edits?.length > 0) {
500
497
  throw new Error("Cannot edit Jupyter notebooks with the Edit tool. Use the NotebookEdit tool instead.");
501
498
  }
502
499
 
503
500
  const absolutePath = resolvePlanPath(this.session, path);
504
- const resolvedRename = rename ? resolvePlanPath(this.session, rename) : undefined;
505
- const file = Bun.file(absolutePath);
501
+ const resolvedMove = move ? resolvePlanPath(this.session, move) : undefined;
506
502
 
507
503
  if (deleteFile) {
508
- if (await file.exists()) {
509
- await file.unlink();
504
+ if (await fs.exists(absolutePath)) {
505
+ await fs.unlink(absolutePath);
510
506
  }
511
507
  invalidateFsScanAfterDelete(absolutePath);
512
508
  return {
@@ -519,30 +515,21 @@ export class EditTool implements AgentTool<TInput> {
519
515
  };
520
516
  }
521
517
 
522
- if (!(await file.exists())) {
523
- const content: string[] = [];
518
+ if (!(await fs.exists(absolutePath))) {
519
+ const lines: string[] = [];
524
520
  for (const edit of edits) {
525
- switch (edit.op) {
526
- case "append": {
527
- if (edit.after) {
528
- throw new Error(`File not found: ${path}`);
529
- }
530
- content.push(...hashlineParseContent(edit.content));
531
- break;
532
- }
533
- case "prepend": {
534
- if (edit.before) {
535
- throw new Error(`File not found: ${path}`);
536
- }
537
- content.unshift(...hashlineParseContent(edit.content));
538
- break;
539
- }
540
- default: {
541
- throw new Error(`File not found: ${path}`);
521
+ // For file creation, only anchorless appends/prepends are valid
522
+ if ((edit.op === "append" || edit.op === "prepend") && !edit.pos && !edit.end) {
523
+ if (edit.op === "prepend") {
524
+ lines.unshift(...hashlineParseText(edit.lines));
525
+ } else {
526
+ lines.push(...hashlineParseText(edit.lines));
542
527
  }
528
+ } else {
529
+ throw new Error(`File not found: ${path}`);
543
530
  }
544
531
  }
545
- await file.write(content.join("\n"));
532
+ await fs.writeFile(absolutePath, lines.join("\n"));
546
533
  return {
547
534
  content: [{ type: "text", text: `Created ${path}` }],
548
535
  details: {
@@ -553,98 +540,31 @@ export class EditTool implements AgentTool<TInput> {
553
540
  };
554
541
  }
555
542
 
556
- const anchorEdits: HashlineEdit[] = [];
557
- for (const edit of edits) {
558
- switch (edit.op) {
559
- case "replace": {
560
- if ("tag" in edit) {
561
- anchorEdits.push({
562
- op: "replace",
563
- tag: parseTag(edit.tag),
564
- content: hashlineParseContent(edit.content),
565
- });
566
- } else {
567
- anchorEdits.push({
568
- op: "replace",
569
- first: parseTag(edit.first),
570
- last: parseTag(edit.last),
571
- content: hashlineParseContent(edit.content),
572
- });
573
- }
574
- break;
575
- }
576
- case "append": {
577
- const { after, content } = edit;
578
- anchorEdits.push({
579
- op: "append",
580
- ...(after ? { after: parseTag(after) } : {}),
581
- content: hashlineParseContent(content),
582
- });
583
- break;
584
- }
585
- case "prepend": {
586
- const { before, content } = edit;
587
- anchorEdits.push({
588
- op: "prepend",
589
- ...(before ? { before: parseTag(before) } : {}),
590
- content: hashlineParseContent(content),
591
- });
592
- break;
593
- }
594
- case "insert": {
595
- const { before, after, content } = edit;
596
- if (before && !after) {
597
- anchorEdits.push({
598
- op: "prepend",
599
- before: parseTag(before),
600
- content: hashlineParseContent(content),
601
- });
602
- } else if (after && !before) {
603
- anchorEdits.push({
604
- op: "append",
605
- after: parseTag(after),
606
- content: hashlineParseContent(content),
607
- });
608
- } else if (before && after) {
609
- anchorEdits.push({
610
- op: "insert",
611
- before: parseTag(before),
612
- after: parseTag(after),
613
- content: hashlineParseContent(content),
614
- });
615
- } else {
616
- throw new Error(`Insert must have both before and after tags.`);
617
- }
618
- break;
619
- }
620
- default:
621
- throw new Error(`Invalid edit operation: ${JSON.stringify(edit)}`);
622
- }
623
- }
543
+ const anchorEdits = resolveEditAnchors(edits);
624
544
 
625
- const rawContent = await file.text();
626
- const { bom, text: content } = stripBom(rawContent);
627
- const originalEnding = detectLineEnding(content);
628
- const originalNormalized = normalizeToLF(content);
629
- let normalizedContent = originalNormalized;
545
+ const rawContent = await fs.readFile(absolutePath, "utf-8");
546
+ const { bom, text } = stripBom(rawContent);
547
+ const originalEnding = detectLineEnding(text);
548
+ const originalNormalized = normalizeToLF(text);
549
+ let normalizedText = originalNormalized;
630
550
 
631
- // Apply anchor-based edits first (set, set_range, insert)
632
- const anchorResult = applyHashlineEdits(normalizedContent, anchorEdits);
633
- normalizedContent = anchorResult.content;
551
+ // Apply anchor-based edits first (replace, append, prepend)
552
+ const anchorResult = applyHashlineEdits(normalizedText, anchorEdits);
553
+ normalizedText = anchorResult.lines;
634
554
 
635
555
  const result = {
636
- content: normalizedContent,
556
+ text: normalizedText,
637
557
  firstChangedLine: anchorResult.firstChangedLine,
638
558
  warnings: anchorResult.warnings,
639
559
  noopEdits: anchorResult.noopEdits,
640
560
  };
641
- if (originalNormalized === result.content && !rename) {
561
+ if (originalNormalized === result.text && !move) {
642
562
  let diagnostic = `No changes made to ${path}. The edits produced identical content.`;
643
563
  if (result.noopEdits && result.noopEdits.length > 0) {
644
564
  const details = result.noopEdits
645
565
  .map(
646
566
  e =>
647
- `Edit ${e.editIndex}: replacement for ${e.loc} is identical to current content:\n ${e.loc}| ${e.currentContent}`,
567
+ `Edit ${e.editIndex}: replacement for ${e.loc} is identical to current content:\n ${e.loc}| ${e.current}`,
648
568
  )
649
569
  .join("\n");
650
570
  diagnostic += `\n${details}`;
@@ -652,27 +572,24 @@ export class EditTool implements AgentTool<TInput> {
652
572
  "\nYour content must differ from what the file already contains. Re-read the file to see the current state.";
653
573
  } else {
654
574
  // Edits were not literally identical but heuristics normalized them back
655
- const lines = result.content.split("\n");
575
+ const lines = result.text.split("\n");
656
576
  const targetLines: string[] = [];
657
- const refs: LineTag[] = [];
577
+ const refs: Anchor[] = [];
658
578
  for (const edit of anchorEdits) {
659
579
  refs.length = 0;
660
580
  switch (edit.op) {
661
581
  case "replace":
662
- if ("tag" in edit) {
663
- refs.push(edit.tag);
582
+ if (edit.end) {
583
+ refs.push(edit.end, edit.pos);
664
584
  } else {
665
- refs.push(edit.first, edit.last);
585
+ refs.push(edit.pos);
666
586
  }
667
587
  break;
668
588
  case "append":
669
- if (edit.after) refs.push(edit.after);
589
+ if (edit.pos) refs.push(edit.pos);
670
590
  break;
671
591
  case "prepend":
672
- if (edit.before) refs.push(edit.before);
673
- break;
674
- case "insert":
675
- refs.push(edit.after, edit.before);
592
+ if (edit.pos) refs.push(edit.pos);
676
593
  break;
677
594
  default:
678
595
  break;
@@ -681,9 +598,9 @@ export class EditTool implements AgentTool<TInput> {
681
598
  for (const ref of refs) {
682
599
  try {
683
600
  if (ref.line >= 1 && ref.line <= lines.length) {
684
- const lineContent = lines[ref.line - 1];
685
- const hash = computeLineHash(ref.line, lineContent);
686
- targetLines.push(`${ref.line}#${hash}:${lineContent}`);
601
+ const text = lines[ref.line - 1];
602
+ const hash = computeLineHash(ref.line, text);
603
+ targetLines.push(`${ref.line}#${hash}:${text}`);
687
604
  }
688
605
  } catch {
689
606
  /* skip malformed refs */
@@ -698,8 +615,8 @@ export class EditTool implements AgentTool<TInput> {
698
615
  throw new Error(diagnostic);
699
616
  }
700
617
 
701
- const finalContent = bom + restoreLineEndings(result.content, originalEnding);
702
- const writePath = resolvedRename ?? absolutePath;
618
+ const finalContent = bom + restoreLineEndings(result.text, originalEnding);
619
+ const writePath = resolvedMove ?? absolutePath;
703
620
  const diagnostics = await this.#writethrough(
704
621
  writePath,
705
622
  finalContent,
@@ -707,26 +624,19 @@ export class EditTool implements AgentTool<TInput> {
707
624
  Bun.file(writePath),
708
625
  batchRequest,
709
626
  );
710
- if (resolvedRename && resolvedRename !== absolutePath) {
711
- await file.unlink();
712
- invalidateFsScanAfterRename(absolutePath, resolvedRename);
627
+ if (resolvedMove && resolvedMove !== absolutePath) {
628
+ await fs.unlink(absolutePath);
629
+ invalidateFsScanAfterRename(absolutePath, resolvedMove);
713
630
  } else {
714
631
  invalidateFsScanAfterWrite(absolutePath);
715
632
  }
716
- const diffResult = generateDiffString(originalNormalized, result.content);
717
-
718
- const normative = buildNormativeUpdateInput({
719
- path,
720
- ...(rename ? { rename } : {}),
721
- oldContent: rawContent,
722
- newContent: finalContent,
723
- });
633
+ const diffResult = generateDiffString(originalNormalized, result.text);
724
634
 
725
635
  const meta = outputMeta()
726
636
  .diagnostics(diagnostics?.summary ?? "", diagnostics?.messages ?? [])
727
637
  .get();
728
638
 
729
- const resultText = rename ? `Updated and moved ${path} to ${rename}` : `Updated ${path}`;
639
+ const resultText = move ? `Moved ${path} to ${move}` : `Updated ${path}`;
730
640
  return {
731
641
  content: [
732
642
  {
@@ -739,10 +649,9 @@ export class EditTool implements AgentTool<TInput> {
739
649
  firstChangedLine: result.firstChangedLine ?? diffResult.firstChangedLine,
740
650
  diagnostics,
741
651
  op: "update",
742
- rename,
652
+ move,
743
653
  meta,
744
654
  },
745
- $normative: normative,
746
655
  };
747
656
  }
748
657
 
@@ -750,12 +659,12 @@ export class EditTool implements AgentTool<TInput> {
750
659
  // Patch mode execution
751
660
  // ─────────────────────────────────────────────────────────────────
752
661
  if (this.mode === "patch") {
753
- const { path, op: rawOp, rename, diff } = params as PatchParams;
662
+ const { file: path, op: rawOp, rename, diff } = params as PatchParams;
754
663
 
755
664
  // Normalize unrecognized operations to "update"
756
665
  const op: Operation = rawOp === "create" || rawOp === "delete" ? rawOp : "update";
757
666
 
758
- enforcePlanModeWrite(this.session, path, { op, rename });
667
+ enforcePlanModeWrite(this.session, path, { op, move: rename });
759
668
  const resolvedPath = resolvePlanPath(this.session, path);
760
669
  const resolvedRename = rename ? resolvePlanPath(this.session, rename) : undefined;
761
670
 
@@ -823,7 +732,7 @@ export class EditTool implements AgentTool<TInput> {
823
732
  firstChangedLine: diffResult.firstChangedLine,
824
733
  diagnostics: mergedDiagnostics,
825
734
  op,
826
- rename: effRename,
735
+ move: effRename,
827
736
  meta,
828
737
  },
829
738
  };
@@ -832,7 +741,7 @@ export class EditTool implements AgentTool<TInput> {
832
741
  // ─────────────────────────────────────────────────────────────────
833
742
  // Replace mode execution
834
743
  // ─────────────────────────────────────────────────────────────────
835
- const { path, old_text, new_text, all } = params as ReplaceParams;
744
+ const { file: path, old_text, new_text, all } = params as ReplaceParams;
836
745
 
837
746
  enforcePlanModeWrite(this.session, path);
838
747
 
@@ -845,13 +754,12 @@ export class EditTool implements AgentTool<TInput> {
845
754
  }
846
755
 
847
756
  const absolutePath = resolvePlanPath(this.session, path);
848
- const file = Bun.file(absolutePath);
849
757
 
850
- if (!(await file.exists())) {
758
+ if (!(await fs.exists(absolutePath))) {
851
759
  throw new Error(`File not found: ${path}`);
852
760
  }
853
761
 
854
- const rawContent = await file.text();
762
+ const rawContent = await fs.readFile(absolutePath, "utf-8");
855
763
  const { bom, text: content } = stripBom(rawContent);
856
764
  const originalEnding = detectLineEnding(content);
857
765
  const normalizedContent = normalizeToLF(content);
@@ -894,7 +802,13 @@ export class EditTool implements AgentTool<TInput> {
894
802
  }
895
803
 
896
804
  const finalContent = bom + restoreLineEndings(result.content, originalEnding);
897
- const diagnostics = await this.#writethrough(absolutePath, finalContent, signal, file, batchRequest);
805
+ const diagnostics = await this.#writethrough(
806
+ absolutePath,
807
+ finalContent,
808
+ signal,
809
+ Bun.file(absolutePath),
810
+ batchRequest,
811
+ );
898
812
  invalidateFsScanAfterWrite(absolutePath);
899
813
  const diffResult = generateDiffString(normalizedContent, result.content);
900
814