@oh-my-pi/pi-coding-agent 13.0.1 → 13.1.0
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.
- package/CHANGELOG.md +16 -0
- package/package.json +7 -7
- package/scripts/format-prompts.ts +33 -3
- package/src/commit/prompts/analysis-system.md +3 -3
- package/src/commit/prompts/changelog-system.md +3 -3
- package/src/commit/prompts/summary-system.md +5 -5
- package/src/extensibility/custom-tools/wrapper.ts +1 -0
- package/src/extensibility/extensions/wrapper.ts +2 -0
- package/src/extensibility/hooks/tool-wrapper.ts +1 -0
- package/src/lsp/index.ts +1 -0
- package/src/patch/diff.ts +2 -2
- package/src/patch/hashline.ts +88 -119
- package/src/patch/index.ts +88 -124
- package/src/patch/shared.ts +14 -21
- package/src/prompts/system/agent-creation-architect.md +2 -2
- package/src/prompts/system/system-prompt.md +9 -9
- package/src/prompts/tools/ask.md +3 -18
- package/src/prompts/tools/{poll-jobs.md → await.md} +1 -1
- package/src/prompts/tools/bash.md +4 -3
- package/src/prompts/tools/browser.md +11 -18
- package/src/prompts/tools/fetch.md +2 -5
- package/src/prompts/tools/find.md +10 -1
- package/src/prompts/tools/grep.md +1 -4
- package/src/prompts/tools/hashline.md +131 -155
- package/src/prompts/tools/lsp.md +6 -16
- package/src/prompts/tools/read.md +3 -6
- package/src/prompts/tools/task.md +93 -275
- package/src/prompts/tools/web-search.md +0 -7
- package/src/prompts/tools/write.md +0 -5
- package/src/sdk.ts +12 -2
- package/src/system-prompt.ts +5 -0
- package/src/task/index.ts +1 -0
- package/src/tools/ask.ts +1 -0
- package/src/tools/{poll-jobs.ts → await-tool.ts} +20 -19
- package/src/tools/bash.ts +1 -0
- package/src/tools/browser.ts +19 -4
- package/src/tools/calculator.ts +1 -0
- package/src/tools/cancel-job.ts +1 -0
- package/src/tools/exit-plan-mode.ts +1 -0
- package/src/tools/fetch.ts +1 -0
- package/src/tools/find.ts +1 -0
- package/src/tools/grep.ts +1 -0
- package/src/tools/index.ts +3 -3
- package/src/tools/notebook.ts +2 -1
- package/src/tools/plan-mode-guard.ts +2 -2
- package/src/tools/python.ts +1 -0
- package/src/tools/read.ts +1 -0
- package/src/tools/ssh.ts +1 -0
- package/src/tools/submit-result.ts +1 -0
- package/src/tools/todo-write.ts +1 -0
- package/src/tools/write.ts +1 -0
- package/src/web/search/index.ts +1 -0
package/src/patch/index.ts
CHANGED
|
@@ -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,
|
|
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";
|
|
@@ -168,7 +167,7 @@ export function stripNewLinePrefixes(lines: string[]): string[] {
|
|
|
168
167
|
});
|
|
169
168
|
}
|
|
170
169
|
|
|
171
|
-
export function
|
|
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
|
|
179
|
+
const hashlineEditSchema = Type.Object(
|
|
181
180
|
{
|
|
182
|
-
op: StringEnum(["replace", "append", "prepend"
|
|
183
|
-
|
|
184
|
-
}),
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
|
193
|
+
const hashlineEditParamsSchema = Type.Object(
|
|
197
194
|
{
|
|
198
|
-
path: Type.String({ description: "
|
|
199
|
-
edits: Type.Array(
|
|
200
|
-
|
|
201
|
-
|
|
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
|
+
path: Type.String({ description: "path" }),
|
|
196
|
+
edits: Type.Array(hashlineEditSchema, { description: "edits over $path" }),
|
|
197
|
+
delete: Type.Optional(Type.Boolean({ description: "If true, delete $path" })),
|
|
198
|
+
move: Type.Optional(Type.String({ description: "If set, move $path to $move" })),
|
|
205
199
|
},
|
|
206
200
|
{ additionalProperties: false },
|
|
207
201
|
);
|
|
208
202
|
|
|
209
|
-
export type HashlineToolEdit = Static<typeof
|
|
210
|
-
export type HashlineParams = Static<typeof
|
|
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 (
|
|
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 +
|
|
221
|
-
* - replace +
|
|
222
|
-
* - append +
|
|
223
|
-
* - prepend +
|
|
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
|
|
234
|
-
const
|
|
235
|
-
const
|
|
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"
|
|
239
|
-
|
|
230
|
+
const op = edit.op === "append" || edit.op === "prepend" ? edit.op : "replace";
|
|
240
231
|
switch (op) {
|
|
241
232
|
case "replace": {
|
|
242
|
-
if (
|
|
243
|
-
result.push({ op: "replace",
|
|
244
|
-
} else if (
|
|
245
|
-
result.push({ op: "replace",
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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):
|
|
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
|
|
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 = "
|
|
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
|
|
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,
|
|
492
|
+
const { path, edits, delete: deleteFile, move } = params as HashlineParams;
|
|
522
493
|
|
|
523
|
-
enforcePlanModeWrite(this.session, path, { op: deleteFile ? "delete" : "update",
|
|
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
|
|
531
|
-
const file = Bun.file(absolutePath);
|
|
501
|
+
const resolvedMove = move ? resolvePlanPath(this.session, move) : undefined;
|
|
532
502
|
|
|
533
503
|
if (deleteFile) {
|
|
534
|
-
if (await
|
|
535
|
-
await
|
|
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
|
|
549
|
-
const
|
|
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.
|
|
522
|
+
if ((edit.op === "append" || edit.op === "prepend") && !edit.pos && !edit.end) {
|
|
553
523
|
if (edit.op === "prepend") {
|
|
554
|
-
|
|
524
|
+
lines.unshift(...hashlineParseText(edit.lines));
|
|
555
525
|
} else {
|
|
556
|
-
|
|
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
|
|
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
|
|
576
|
-
const { bom, text
|
|
577
|
-
const originalEnding = detectLineEnding(
|
|
578
|
-
const originalNormalized = normalizeToLF(
|
|
579
|
-
let
|
|
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 (
|
|
582
|
-
const anchorResult = applyHashlineEdits(
|
|
583
|
-
|
|
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
|
-
|
|
556
|
+
text: normalizedText,
|
|
587
557
|
firstChangedLine: anchorResult.firstChangedLine,
|
|
588
558
|
warnings: anchorResult.warnings,
|
|
589
559
|
noopEdits: anchorResult.noopEdits,
|
|
590
560
|
};
|
|
591
|
-
if (originalNormalized === result.
|
|
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.
|
|
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.
|
|
575
|
+
const lines = result.text.split("\n");
|
|
606
576
|
const targetLines: string[] = [];
|
|
607
|
-
const refs:
|
|
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 (
|
|
613
|
-
refs.push(edit.
|
|
582
|
+
if (edit.end) {
|
|
583
|
+
refs.push(edit.end, edit.pos);
|
|
614
584
|
} else {
|
|
615
|
-
refs.push(edit.
|
|
585
|
+
refs.push(edit.pos);
|
|
616
586
|
}
|
|
617
587
|
break;
|
|
618
588
|
case "append":
|
|
619
|
-
if (edit.
|
|
589
|
+
if (edit.pos) refs.push(edit.pos);
|
|
620
590
|
break;
|
|
621
591
|
case "prepend":
|
|
622
|
-
if (edit.
|
|
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
|
|
635
|
-
const hash = computeLineHash(ref.line,
|
|
636
|
-
targetLines.push(`${ref.line}#${hash}:${
|
|
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.
|
|
652
|
-
const writePath =
|
|
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 (
|
|
661
|
-
await
|
|
662
|
-
invalidateFsScanAfterRename(absolutePath,
|
|
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.
|
|
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 =
|
|
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
|
-
|
|
652
|
+
move,
|
|
693
653
|
meta,
|
|
694
654
|
},
|
|
695
|
-
$normative: normative,
|
|
696
655
|
};
|
|
697
656
|
}
|
|
698
657
|
|
|
@@ -705,7 +664,7 @@ export class EditTool implements AgentTool<TInput> {
|
|
|
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
|
-
|
|
735
|
+
move: effRename,
|
|
777
736
|
meta,
|
|
778
737
|
},
|
|
779
738
|
};
|
|
@@ -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
|
|
758
|
+
if (!(await fs.exists(absolutePath))) {
|
|
801
759
|
throw new Error(`File not found: ${path}`);
|
|
802
760
|
}
|
|
803
761
|
|
|
804
|
-
const rawContent = await
|
|
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(
|
|
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
|
|
package/src/patch/shared.ts
CHANGED
|
@@ -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
|
-
|
|
62
|
+
move?: string;
|
|
62
63
|
/** Structured output metadata */
|
|
63
64
|
meta?: OutputMeta;
|
|
64
65
|
}
|
|
@@ -83,16 +84,9 @@ interface EditRenderArgs {
|
|
|
83
84
|
*/
|
|
84
85
|
previewDiff?: string;
|
|
85
86
|
// Hashline mode fields
|
|
86
|
-
edits?:
|
|
87
|
+
edits?: Partial<HashlineToolEdit>[];
|
|
87
88
|
}
|
|
88
89
|
|
|
89
|
-
type HashlineEditPreview = {
|
|
90
|
-
op: string;
|
|
91
|
-
first?: string;
|
|
92
|
-
last?: string;
|
|
93
|
-
content: string | string[] | null;
|
|
94
|
-
};
|
|
95
|
-
|
|
96
90
|
/** Extended context for edit tool rendering */
|
|
97
91
|
export interface EditRenderContext {
|
|
98
92
|
/** Pre-computed diff preview (computed before tool executes) */
|
|
@@ -123,7 +117,7 @@ function formatStreamingDiff(diff: string, rawPath: string, uiTheme: Theme, labe
|
|
|
123
117
|
return text;
|
|
124
118
|
}
|
|
125
119
|
|
|
126
|
-
function formatStreamingHashlineEdits(edits:
|
|
120
|
+
function formatStreamingHashlineEdits(edits: Partial<HashlineToolEdit>[], uiTheme: Theme): string {
|
|
127
121
|
const MAX_EDITS = 4;
|
|
128
122
|
const MAX_DST_LINES = 8;
|
|
129
123
|
let text = "\n\n";
|
|
@@ -158,22 +152,21 @@ function formatStreamingHashlineEdits(edits: unknown[], uiTheme: Theme): string
|
|
|
158
152
|
}
|
|
159
153
|
|
|
160
154
|
return text.trimEnd();
|
|
161
|
-
function formatHashlineEdit(edit:
|
|
162
|
-
|
|
163
|
-
if (!editRecord) {
|
|
155
|
+
function formatHashlineEdit(edit: Partial<HashlineToolEdit>): { srcLabel: string; dst: string } {
|
|
156
|
+
if (typeof edit !== "object" || !edit) {
|
|
164
157
|
return { srcLabel: "• (incomplete edit)", dst: "" };
|
|
165
158
|
}
|
|
166
159
|
|
|
167
|
-
const contentLines = Array.isArray(
|
|
160
|
+
const contentLines = Array.isArray(edit.lines) ? (edit.lines as string[]).join("\n") : "";
|
|
168
161
|
|
|
169
|
-
const op = typeof
|
|
170
|
-
const
|
|
171
|
-
const
|
|
162
|
+
const op = typeof edit.op === "string" ? edit.op : "?";
|
|
163
|
+
const pos = typeof edit.pos === "string" ? edit.pos : undefined;
|
|
164
|
+
const end = typeof edit.end === "string" ? edit.end : undefined;
|
|
172
165
|
|
|
173
|
-
if (
|
|
174
|
-
return { srcLabel:
|
|
166
|
+
if (pos && end && pos !== end) {
|
|
167
|
+
return { srcLabel: `• ${op} ${pos}…${end}`, dst: contentLines };
|
|
175
168
|
}
|
|
176
|
-
const anchor =
|
|
169
|
+
const anchor = pos ?? end;
|
|
177
170
|
if (anchor) {
|
|
178
171
|
return { srcLabel: `\u2022 ${op} ${anchor}`, dst: contentLines };
|
|
179
172
|
}
|
|
@@ -287,7 +280,7 @@ export const editToolRenderer = {
|
|
|
287
280
|
const editIcon = uiTheme.fg("muted", uiTheme.getLangIcon(editLanguage));
|
|
288
281
|
|
|
289
282
|
const op = args?.op || result.details?.op;
|
|
290
|
-
const rename = args?.rename || result.details?.
|
|
283
|
+
const rename = args?.rename || result.details?.move;
|
|
291
284
|
const opTitle = op === "create" ? "Create" : op === "delete" ? "Delete" : "Edit";
|
|
292
285
|
|
|
293
286
|
// 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
|
|
54
|
-
"systemPrompt": "The complete system prompt that will govern the agent's behavior, written in second person ('You are
|
|
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")}}
|
|
@@ -100,6 +100,10 @@ You MUST NOT open a file hoping. Hope is not a strategy.
|
|
|
100
100
|
{{#has tools "grep"}}- Known territory → `grep` to locate target{{/has}}
|
|
101
101
|
{{#has tools "read"}}- Known location → `read` with offset/limit, not whole file{{/has}}
|
|
102
102
|
{{/ifAny}}
|
|
103
|
+
|
|
104
|
+
{{#if intentTracing}}
|
|
105
|
+
Every tool has a required `{{intentField}}` parameter. Describe intent as one sentence in present participle form (e.g., Inserting comment before the function) with no trailing period.
|
|
106
|
+
{{/if}}
|
|
103
107
|
</tools>
|
|
104
108
|
|
|
105
109
|
<procedure>
|
|
@@ -215,30 +219,26 @@ Match skill descriptions to the task domain. If a skill is relevant, you MUST re
|
|
|
215
219
|
Relative paths in skill files resolve against the skill directory.
|
|
216
220
|
|
|
217
221
|
{{#list skills join="\n"}}
|
|
218
|
-
|
|
222
|
+
### {{name}}
|
|
219
223
|
{{description}}
|
|
220
|
-
</skill>
|
|
221
224
|
{{/list}}
|
|
222
225
|
</skills>
|
|
223
226
|
{{/if}}
|
|
224
227
|
{{#if preloadedSkills.length}}
|
|
225
|
-
<
|
|
228
|
+
<skills>
|
|
226
229
|
{{#list preloadedSkills join="\n"}}
|
|
227
230
|
<skill name="{{name}}">
|
|
228
231
|
{{content}}
|
|
229
232
|
</skill>
|
|
230
233
|
{{/list}}
|
|
231
|
-
</
|
|
234
|
+
</skills>
|
|
232
235
|
{{/if}}
|
|
233
236
|
{{#if rules.length}}
|
|
234
237
|
<rules>
|
|
235
238
|
Read `rule://<name>` when working in matching domain.
|
|
236
|
-
|
|
237
239
|
{{#list rules join="\n"}}
|
|
238
|
-
|
|
240
|
+
### {{name}} (Glob: {{#list globs join=", "}}{{this}}{{/list}})
|
|
239
241
|
{{description}}
|
|
240
|
-
{{#list globs join="\n"}}<glob>{{this}}</glob>{{/list}}
|
|
241
|
-
</rule>
|
|
242
242
|
{{/list}}
|
|
243
243
|
</rules>
|
|
244
244
|
{{/if}}
|
package/src/prompts/tools/ask.md
CHANGED
|
@@ -12,33 +12,18 @@ Ask user when you need clarification or input during task execution.
|
|
|
12
12
|
- Set `multi: true` on question to allow multiple selections
|
|
13
13
|
</instruction>
|
|
14
14
|
|
|
15
|
-
<output>
|
|
16
|
-
Returns selected option(s) as text. For multi-part questions, returns map of question IDs to selected values.
|
|
17
|
-
</output>
|
|
18
|
-
|
|
19
15
|
<caution>
|
|
20
16
|
- Provide 2-5 concise, distinct options
|
|
21
17
|
</caution>
|
|
22
18
|
|
|
23
19
|
<critical>
|
|
24
|
-
**Default to action.
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
3. **If multiple choices are acceptable**, you MUST pick the most conservative/standard option and proceed; state the choice.
|
|
28
|
-
4. You MUST **only ask when options have materially different tradeoffs and the user must decide.**
|
|
29
|
-
**You MUST NOT include "Other" option in your options array.** UI automatically adds "Other (type your own)" to every question; adding your own creates duplicates.
|
|
20
|
+
- **Default to action.** Resolve ambiguity yourself using repo conventions, existing patterns, and reasonable defaults. Exhaust existing sources (code, configs, docs, history) before asking. Only ask when options have materially different tradeoffs the user must decide.
|
|
21
|
+
- **If multiple choices are acceptable**, pick the most conservative/standard option and proceed; state the choice.
|
|
22
|
+
- **Do NOT include "Other" option** — UI automatically adds "Other (type your own)" to every question.
|
|
30
23
|
</critical>
|
|
31
24
|
|
|
32
25
|
<example name="single">
|
|
33
26
|
question: "Which authentication method should this API use?"
|
|
34
27
|
options: [{"label": "JWT"}, {"label": "OAuth2"}, {"label": "Session cookies"}]
|
|
35
28
|
recommended: 0
|
|
36
|
-
</example>
|
|
37
|
-
|
|
38
|
-
<example name="multi-part">
|
|
39
|
-
questions: [
|
|
40
|
-
{"id": "auth", "question": "Which auth method?", "options": [{"label": "JWT"}, {"label": "OAuth2"}], "recommended": 0},
|
|
41
|
-
{"id": "cache", "question": "Enable caching?", "options": [{"label": "Yes"}, {"label": "No"}]},
|
|
42
|
-
{"id": "features", "question": "Which features to include?", "options": [{"label": "Logging"}, {"label": "Metrics"}, {"label": "Tracing"}], "multi": true}
|
|
43
|
-
]
|
|
44
29
|
</example>
|