@oh-my-pi/pi-coding-agent 13.0.1 → 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 (39) hide show
  1. package/package.json +7 -7
  2. package/scripts/format-prompts.ts +33 -3
  3. package/src/commit/prompts/analysis-system.md +3 -3
  4. package/src/commit/prompts/changelog-system.md +3 -3
  5. package/src/commit/prompts/summary-system.md +5 -5
  6. package/src/extensibility/custom-tools/wrapper.ts +1 -0
  7. package/src/extensibility/extensions/wrapper.ts +2 -0
  8. package/src/extensibility/hooks/tool-wrapper.ts +1 -0
  9. package/src/lsp/index.ts +1 -0
  10. package/src/patch/diff.ts +2 -2
  11. package/src/patch/hashline.ts +88 -119
  12. package/src/patch/index.ts +92 -128
  13. package/src/patch/shared.ts +17 -23
  14. package/src/prompts/system/agent-creation-architect.md +2 -2
  15. package/src/prompts/system/system-prompt.md +1 -1
  16. package/src/prompts/tools/bash.md +1 -1
  17. package/src/prompts/tools/find.md +9 -0
  18. package/src/prompts/tools/hashline.md +130 -155
  19. package/src/prompts/tools/task.md +3 -3
  20. package/src/task/index.ts +1 -0
  21. package/src/tools/ask.ts +1 -0
  22. package/src/tools/bash.ts +1 -0
  23. package/src/tools/browser.ts +1 -0
  24. package/src/tools/calculator.ts +1 -0
  25. package/src/tools/cancel-job.ts +1 -0
  26. package/src/tools/exit-plan-mode.ts +1 -0
  27. package/src/tools/fetch.ts +1 -0
  28. package/src/tools/find.ts +1 -0
  29. package/src/tools/grep.ts +1 -0
  30. package/src/tools/notebook.ts +1 -0
  31. package/src/tools/plan-mode-guard.ts +2 -2
  32. package/src/tools/poll-jobs.ts +1 -0
  33. package/src/tools/python.ts +1 -0
  34. package/src/tools/read.ts +1 -0
  35. package/src/tools/ssh.ts +1 -0
  36. package/src/tools/submit-result.ts +1 -0
  37. package/src/tools/todo-write.ts +1 -0
  38. package/src/tools/write.ts +1 -0
  39. 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,7 +167,7 @@ export function stripNewLinePrefixes(lines: string[]): string[] {
168
167
  });
169
168
  }
170
169
 
171
- export function hashlineParseContent(edit: string | string[] | null): string[] {
170
+ export function hashlineParseText(edit: string[] | string | null): string[] {
172
171
  if (edit === null) return [];
173
172
  if (Array.isArray(edit)) return edit;
174
173
  const lines = stripNewLinePrefixes(edit.split("\n"));
@@ -177,52 +176,45 @@ export function hashlineParseContent(edit: string | string[] | null): string[] {
177
176
  return lines;
178
177
  }
179
178
 
180
- const hashlineEditSpecSchema = Type.Object(
179
+ const hashlineEditSchema = Type.Object(
181
180
  {
182
- op: StringEnum(["replace", "append", "prepend", "insert"], {
183
- description: "Operation type",
184
- }),
185
- first: Type.Optional(Type.String({ description: 'First/start anchor tag in "LINE#ID" format' })),
186
- last: Type.Optional(Type.String({ description: 'Last/end anchor tag in "LINE#ID" format' })),
187
- content: Type.Union([
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(),
188
187
  Type.Null(),
189
- Type.Array(Type.String(), { description: "Content lines" }),
190
- Type.String({ description: "Content line" }),
191
188
  ]),
192
189
  },
193
190
  { additionalProperties: false },
194
191
  );
195
192
 
196
- const hashlineEditSchema = Type.Object(
193
+ const hashlineEditParamsSchema = Type.Object(
197
194
  {
198
- path: Type.String({ description: "File path (relative or absolute)" }),
199
- edits: Type.Array(hashlineEditSpecSchema, {
200
- description: "Changes to apply to the file at `path`",
201
- minItems: 0,
202
- }),
203
- delete: Type.Optional(Type.Boolean({ description: "Delete the file when true" })),
204
- rename: Type.Optional(Type.String({ description: "New path if moving" })),
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" })),
205
199
  },
206
200
  { additionalProperties: false },
207
201
  );
208
202
 
209
- export type HashlineToolEdit = Static<typeof hashlineEditSpecSchema>;
210
- export type HashlineParams = Static<typeof hashlineEditSchema>;
203
+ export type HashlineToolEdit = Static<typeof hashlineEditSchema>;
204
+ export type HashlineParams = Static<typeof hashlineEditParamsSchema>;
211
205
 
212
206
  // ═══════════════════════════════════════════════════════════════════════════
213
207
  // Resilient anchor resolution
214
208
  // ═══════════════════════════════════════════════════════════════════════════
215
209
 
216
210
  /**
217
- * Map flat tool-schema edits (first/last) into typed HashlineEdit objects.
211
+ * Map flat tool-schema edits (tag/end) into typed HashlineEdit objects.
218
212
  *
219
213
  * Resilient: as long as at least one anchor exists, we execute.
220
- * - replace + first only → single-line replace (tag = first)
221
- * - replace + first + last → range replace
222
- * - append + first or last → append after that anchor
223
- * - prepend + first or last → prepend before that anchor
224
- * - insert + first + last → insert between them
225
- * - insert + one anchor → degrade to append/prepend
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
226
218
  * - no anchors → file-level append/prepend (only for those ops)
227
219
  *
228
220
  * Unknown ops default to "replace".
@@ -230,51 +222,29 @@ export type HashlineParams = Static<typeof hashlineEditSchema>;
230
222
  function resolveEditAnchors(edits: HashlineToolEdit[]): HashlineEdit[] {
231
223
  const result: HashlineEdit[] = [];
232
224
  for (const edit of edits) {
233
- const content = hashlineParseContent(edit.content);
234
- const first = edit.first ? tryParseTag(edit.first) : undefined;
235
- const last = edit.last ? tryParseTag(edit.last) : undefined;
225
+ const lines = hashlineParseText(edit.lines);
226
+ const tag = edit.pos ? tryParseTag(edit.pos) : undefined;
227
+ const end = edit.end ? tryParseTag(edit.end) : undefined;
236
228
 
237
229
  // Normalize op — default unknown values to "replace"
238
- const op = edit.op === "append" || edit.op === "prepend" || edit.op === "insert" ? edit.op : "replace";
239
-
230
+ const op = edit.op === "append" || edit.op === "prepend" ? edit.op : "replace";
240
231
  switch (op) {
241
232
  case "replace": {
242
- if (first && last) {
243
- result.push({ op: "replace", first, last, content });
244
- } else if (first) {
245
- result.push({ op: "replace", tag: first, content });
246
- } else if (last) {
247
- result.push({ op: "replace", tag: last, content });
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 });
248
237
  } else {
249
- throw new Error("Replace requires at least one anchor (first or last).");
238
+ throw new Error("Replace requires at least one anchor (tag or end).");
250
239
  }
251
240
  break;
252
241
  }
253
242
  case "append": {
254
- // Prefer first as the "after" anchor; fall back to last
255
- const anchor = first ?? last;
256
- result.push({ op: "append", ...(anchor ? { after: anchor } : {}), content });
243
+ result.push({ op: "append", pos: tag ?? end, lines });
257
244
  break;
258
245
  }
259
246
  case "prepend": {
260
- // Prefer last as the "before" anchor; fall back to first
261
- const anchor = last ?? first;
262
- result.push({ op: "prepend", ...(anchor ? { before: anchor } : {}), content });
263
- break;
264
- }
265
- case "insert": {
266
- if (first && last) {
267
- result.push({ op: "insert", after: first, before: last, content });
268
- } else if (first) {
269
- // Degrade: insert after first
270
- result.push({ op: "append", after: first, content });
271
- } else if (last) {
272
- // Degrade: insert before last
273
- result.push({ op: "prepend", before: last, content });
274
- } else {
275
- // No anchors — append to end
276
- result.push({ op: "append", content });
277
- }
247
+ result.push({ op: "prepend", pos: end ?? tag, lines });
278
248
  break;
279
249
  }
280
250
  }
@@ -283,7 +253,7 @@ function resolveEditAnchors(edits: HashlineToolEdit[]): HashlineEdit[] {
283
253
  }
284
254
 
285
255
  /** Parse a tag, returning undefined instead of throwing on garbage. */
286
- function tryParseTag(raw: string): LineTag | undefined {
256
+ function tryParseTag(raw: string): Anchor | undefined {
287
257
  try {
288
258
  return parseTag(raw);
289
259
  } catch {
@@ -379,11 +349,11 @@ function mergeDiagnosticsWithWarnings(
379
349
  // Tool Class
380
350
  // ═══════════════════════════════════════════════════════════════════════════
381
351
 
382
- type TInput = typeof replaceEditSchema | typeof patchEditSchema | typeof hashlineEditSchema;
352
+ type TInput = typeof replaceEditSchema | typeof patchEditSchema | typeof hashlineEditParamsSchema;
383
353
 
384
354
  export type EditMode = "replace" | "patch" | "hashline";
385
355
 
386
- export const DEFAULT_EDIT_MODE: EditMode = "patch";
356
+ export const DEFAULT_EDIT_MODE: EditMode = "hashline";
387
357
 
388
358
  export function normalizeEditMode(mode?: string | null): EditMode | null {
389
359
  switch (mode) {
@@ -408,6 +378,7 @@ export class EditTool implements AgentTool<TInput> {
408
378
  readonly label = "Edit";
409
379
  readonly nonAbortable = true;
410
380
  readonly concurrency = "exclusive";
381
+ readonly strict = true;
411
382
 
412
383
  readonly #allowFuzzy: boolean;
413
384
  readonly #fuzzyThreshold: number;
@@ -499,7 +470,7 @@ export class EditTool implements AgentTool<TInput> {
499
470
  case "patch":
500
471
  return patchEditSchema;
501
472
  case "hashline":
502
- return hashlineEditSchema;
473
+ return hashlineEditParamsSchema;
503
474
  default:
504
475
  return replaceEditSchema;
505
476
  }
@@ -518,21 +489,20 @@ export class EditTool implements AgentTool<TInput> {
518
489
  // Hashline mode execution
519
490
  // ─────────────────────────────────────────────────────────────────
520
491
  if (this.mode === "hashline") {
521
- const { path, edits, delete: deleteFile, rename } = params as HashlineParams;
492
+ const { file: path, edits, delete: deleteFile, move } = params as HashlineParams;
522
493
 
523
- enforcePlanModeWrite(this.session, path, { op: deleteFile ? "delete" : "update", rename });
494
+ enforcePlanModeWrite(this.session, path, { op: deleteFile ? "delete" : "update", move });
524
495
 
525
496
  if (path.endsWith(".ipynb") && edits?.length > 0) {
526
497
  throw new Error("Cannot edit Jupyter notebooks with the Edit tool. Use the NotebookEdit tool instead.");
527
498
  }
528
499
 
529
500
  const absolutePath = resolvePlanPath(this.session, path);
530
- const resolvedRename = rename ? resolvePlanPath(this.session, rename) : undefined;
531
- const file = Bun.file(absolutePath);
501
+ const resolvedMove = move ? resolvePlanPath(this.session, move) : undefined;
532
502
 
533
503
  if (deleteFile) {
534
- if (await file.exists()) {
535
- await file.unlink();
504
+ if (await fs.exists(absolutePath)) {
505
+ await fs.unlink(absolutePath);
536
506
  }
537
507
  invalidateFsScanAfterDelete(absolutePath);
538
508
  return {
@@ -545,21 +515,21 @@ export class EditTool implements AgentTool<TInput> {
545
515
  };
546
516
  }
547
517
 
548
- if (!(await file.exists())) {
549
- const content: string[] = [];
518
+ if (!(await fs.exists(absolutePath))) {
519
+ const lines: string[] = [];
550
520
  for (const edit of edits) {
551
521
  // For file creation, only anchorless appends/prepends are valid
552
- if ((edit.op === "append" || edit.op === "prepend") && !edit.first && !edit.last) {
522
+ if ((edit.op === "append" || edit.op === "prepend") && !edit.pos && !edit.end) {
553
523
  if (edit.op === "prepend") {
554
- content.unshift(...hashlineParseContent(edit.content));
524
+ lines.unshift(...hashlineParseText(edit.lines));
555
525
  } else {
556
- content.push(...hashlineParseContent(edit.content));
526
+ lines.push(...hashlineParseText(edit.lines));
557
527
  }
558
528
  } else {
559
529
  throw new Error(`File not found: ${path}`);
560
530
  }
561
531
  }
562
- await file.write(content.join("\n"));
532
+ await fs.writeFile(absolutePath, lines.join("\n"));
563
533
  return {
564
534
  content: [{ type: "text", text: `Created ${path}` }],
565
535
  details: {
@@ -572,29 +542,29 @@ export class EditTool implements AgentTool<TInput> {
572
542
 
573
543
  const anchorEdits = resolveEditAnchors(edits);
574
544
 
575
- const rawContent = await file.text();
576
- const { bom, text: content } = stripBom(rawContent);
577
- const originalEnding = detectLineEnding(content);
578
- const originalNormalized = normalizeToLF(content);
579
- 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;
580
550
 
581
- // Apply anchor-based edits first (set, set_range, insert)
582
- const anchorResult = applyHashlineEdits(normalizedContent, anchorEdits);
583
- normalizedContent = anchorResult.content;
551
+ // Apply anchor-based edits first (replace, append, prepend)
552
+ const anchorResult = applyHashlineEdits(normalizedText, anchorEdits);
553
+ normalizedText = anchorResult.lines;
584
554
 
585
555
  const result = {
586
- content: normalizedContent,
556
+ text: normalizedText,
587
557
  firstChangedLine: anchorResult.firstChangedLine,
588
558
  warnings: anchorResult.warnings,
589
559
  noopEdits: anchorResult.noopEdits,
590
560
  };
591
- if (originalNormalized === result.content && !rename) {
561
+ if (originalNormalized === result.text && !move) {
592
562
  let diagnostic = `No changes made to ${path}. The edits produced identical content.`;
593
563
  if (result.noopEdits && result.noopEdits.length > 0) {
594
564
  const details = result.noopEdits
595
565
  .map(
596
566
  e =>
597
- `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}`,
598
568
  )
599
569
  .join("\n");
600
570
  diagnostic += `\n${details}`;
@@ -602,27 +572,24 @@ export class EditTool implements AgentTool<TInput> {
602
572
  "\nYour content must differ from what the file already contains. Re-read the file to see the current state.";
603
573
  } else {
604
574
  // Edits were not literally identical but heuristics normalized them back
605
- const lines = result.content.split("\n");
575
+ const lines = result.text.split("\n");
606
576
  const targetLines: string[] = [];
607
- const refs: LineTag[] = [];
577
+ const refs: Anchor[] = [];
608
578
  for (const edit of anchorEdits) {
609
579
  refs.length = 0;
610
580
  switch (edit.op) {
611
581
  case "replace":
612
- if ("tag" in edit) {
613
- refs.push(edit.tag);
582
+ if (edit.end) {
583
+ refs.push(edit.end, edit.pos);
614
584
  } else {
615
- refs.push(edit.first, edit.last);
585
+ refs.push(edit.pos);
616
586
  }
617
587
  break;
618
588
  case "append":
619
- if (edit.after) refs.push(edit.after);
589
+ if (edit.pos) refs.push(edit.pos);
620
590
  break;
621
591
  case "prepend":
622
- if (edit.before) refs.push(edit.before);
623
- break;
624
- case "insert":
625
- refs.push(edit.after, edit.before);
592
+ if (edit.pos) refs.push(edit.pos);
626
593
  break;
627
594
  default:
628
595
  break;
@@ -631,9 +598,9 @@ export class EditTool implements AgentTool<TInput> {
631
598
  for (const ref of refs) {
632
599
  try {
633
600
  if (ref.line >= 1 && ref.line <= lines.length) {
634
- const lineContent = lines[ref.line - 1];
635
- const hash = computeLineHash(ref.line, lineContent);
636
- 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}`);
637
604
  }
638
605
  } catch {
639
606
  /* skip malformed refs */
@@ -648,8 +615,8 @@ export class EditTool implements AgentTool<TInput> {
648
615
  throw new Error(diagnostic);
649
616
  }
650
617
 
651
- const finalContent = bom + restoreLineEndings(result.content, originalEnding);
652
- const writePath = resolvedRename ?? absolutePath;
618
+ const finalContent = bom + restoreLineEndings(result.text, originalEnding);
619
+ const writePath = resolvedMove ?? absolutePath;
653
620
  const diagnostics = await this.#writethrough(
654
621
  writePath,
655
622
  finalContent,
@@ -657,26 +624,19 @@ export class EditTool implements AgentTool<TInput> {
657
624
  Bun.file(writePath),
658
625
  batchRequest,
659
626
  );
660
- if (resolvedRename && resolvedRename !== absolutePath) {
661
- await file.unlink();
662
- invalidateFsScanAfterRename(absolutePath, resolvedRename);
627
+ if (resolvedMove && resolvedMove !== absolutePath) {
628
+ await fs.unlink(absolutePath);
629
+ invalidateFsScanAfterRename(absolutePath, resolvedMove);
663
630
  } else {
664
631
  invalidateFsScanAfterWrite(absolutePath);
665
632
  }
666
- const diffResult = generateDiffString(originalNormalized, result.content);
667
-
668
- const normative = buildNormativeUpdateInput({
669
- path,
670
- ...(rename ? { rename } : {}),
671
- oldContent: rawContent,
672
- newContent: finalContent,
673
- });
633
+ const diffResult = generateDiffString(originalNormalized, result.text);
674
634
 
675
635
  const meta = outputMeta()
676
636
  .diagnostics(diagnostics?.summary ?? "", diagnostics?.messages ?? [])
677
637
  .get();
678
638
 
679
- const resultText = rename ? `Updated and moved ${path} to ${rename}` : `Updated ${path}`;
639
+ const resultText = move ? `Moved ${path} to ${move}` : `Updated ${path}`;
680
640
  return {
681
641
  content: [
682
642
  {
@@ -689,10 +649,9 @@ export class EditTool implements AgentTool<TInput> {
689
649
  firstChangedLine: result.firstChangedLine ?? diffResult.firstChangedLine,
690
650
  diagnostics,
691
651
  op: "update",
692
- rename,
652
+ move,
693
653
  meta,
694
654
  },
695
- $normative: normative,
696
655
  };
697
656
  }
698
657
 
@@ -700,12 +659,12 @@ export class EditTool implements AgentTool<TInput> {
700
659
  // Patch mode execution
701
660
  // ─────────────────────────────────────────────────────────────────
702
661
  if (this.mode === "patch") {
703
- const { path, op: rawOp, rename, diff } = params as PatchParams;
662
+ const { file: path, op: rawOp, rename, diff } = params as PatchParams;
704
663
 
705
664
  // Normalize unrecognized operations to "update"
706
665
  const op: Operation = rawOp === "create" || rawOp === "delete" ? rawOp : "update";
707
666
 
708
- enforcePlanModeWrite(this.session, path, { op, rename });
667
+ enforcePlanModeWrite(this.session, path, { op, move: rename });
709
668
  const resolvedPath = resolvePlanPath(this.session, path);
710
669
  const resolvedRename = rename ? resolvePlanPath(this.session, rename) : undefined;
711
670
 
@@ -773,7 +732,7 @@ export class EditTool implements AgentTool<TInput> {
773
732
  firstChangedLine: diffResult.firstChangedLine,
774
733
  diagnostics: mergedDiagnostics,
775
734
  op,
776
- rename: effRename,
735
+ move: effRename,
777
736
  meta,
778
737
  },
779
738
  };
@@ -782,7 +741,7 @@ export class EditTool implements AgentTool<TInput> {
782
741
  // ─────────────────────────────────────────────────────────────────
783
742
  // Replace mode execution
784
743
  // ─────────────────────────────────────────────────────────────────
785
- const { path, old_text, new_text, all } = params as ReplaceParams;
744
+ const { file: path, old_text, new_text, all } = params as ReplaceParams;
786
745
 
787
746
  enforcePlanModeWrite(this.session, path);
788
747
 
@@ -795,13 +754,12 @@ export class EditTool implements AgentTool<TInput> {
795
754
  }
796
755
 
797
756
  const absolutePath = resolvePlanPath(this.session, path);
798
- const file = Bun.file(absolutePath);
799
757
 
800
- if (!(await file.exists())) {
758
+ if (!(await fs.exists(absolutePath))) {
801
759
  throw new Error(`File not found: ${path}`);
802
760
  }
803
761
 
804
- const rawContent = await file.text();
762
+ const rawContent = await fs.readFile(absolutePath, "utf-8");
805
763
  const { bom, text: content } = stripBom(rawContent);
806
764
  const originalEnding = detectLineEnding(content);
807
765
  const normalizedContent = normalizeToLF(content);
@@ -844,7 +802,13 @@ export class EditTool implements AgentTool<TInput> {
844
802
  }
845
803
 
846
804
  const finalContent = bom + restoreLineEndings(result.content, originalEnding);
847
- 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
+ );
848
812
  invalidateFsScanAfterWrite(absolutePath);
849
813
  const diffResult = generateDiffString(normalizedContent, result.content);
850
814
 
@@ -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: string;
91
- first?: string;
92
- last?: string;
93
- content: string | string[] | null;
94
- };
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,22 +153,21 @@ 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
- const op = typeof editRecord.op === "string" ? editRecord.op : "?";
170
- const first = typeof editRecord.first === "string" ? editRecord.first : undefined;
171
- const last = typeof editRecord.last === "string" ? editRecord.last : undefined;
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;
172
166
 
173
- if (first && last && first !== last) {
174
- return { srcLabel: `\u2022 ${op} ${first}..${last}`, dst: contentLines };
167
+ if (pos && end && pos !== end) {
168
+ return { srcLabel: `• ${op} ${pos}…${end}`, dst: contentLines };
175
169
  }
176
- const anchor = first ?? last;
170
+ const anchor = pos ?? end;
177
171
  if (anchor) {
178
172
  return { srcLabel: `\u2022 ${op} ${anchor}`, dst: contentLines };
179
173
  }
@@ -226,7 +220,7 @@ export const editToolRenderer = {
226
220
  mergeCallAndResult: true,
227
221
 
228
222
  renderCall(args: EditRenderArgs, options: RenderResultOptions, uiTheme: Theme): Component {
229
- const rawPath = args.file_path || args.path || "";
223
+ const rawPath = args.file_path || args.path || args.file || "";
230
224
  const filePath = shortenPath(rawPath);
231
225
  const editLanguage = getLanguageFromPath(rawPath) ?? "text";
232
226
  const editIcon = uiTheme.fg("muted", uiTheme.getLangIcon(editLanguage));
@@ -281,13 +275,13 @@ export const editToolRenderer = {
281
275
  uiTheme: Theme,
282
276
  args?: EditRenderArgs,
283
277
  ): Component {
284
- const rawPath = args?.file_path || args?.path || "";
278
+ const rawPath = args?.file_path || args?.path || args?.file || "";
285
279
  const filePath = shortenPath(rawPath);
286
280
  const editLanguage = getLanguageFromPath(rawPath) ?? "text";
287
281
  const editIcon = uiTheme.fg("muted", uiTheme.getLangIcon(editLanguage));
288
282
 
289
283
  const op = args?.op || result.details?.op;
290
- const rename = args?.rename || result.details?.rename;
284
+ const rename = args?.rename || result.details?.move;
291
285
  const opTitle = op === "create" ? "Create" : op === "delete" ? "Delete" : "Edit";
292
286
 
293
287
  // Pre-compute metadata line (static across renders)
@@ -50,8 +50,8 @@ When a user describes what they want an agent to do, you will:
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>