@agi-cli/sdk 0.1.77 → 0.1.79

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agi-cli/sdk",
3
- "version": "0.1.77",
3
+ "version": "0.1.79",
4
4
  "description": "AI agent SDK for building intelligent assistants - tree-shakable and comprehensive",
5
5
  "author": "ntishxyz",
6
6
  "license": "MIT",
@@ -4,5 +4,7 @@
4
4
  - Returns a compact patch artifact summarizing the change
5
5
 
6
6
  Usage tips:
7
- - Prefer idempotent writes by providing the full intended content
8
- - For localized edits, consider the Edit tool with structured operations
7
+ - Only use for creating new files or completely replacing file content
8
+ - NEVER use for partial/targeted edits - use apply_patch or edit instead
9
+ - Using write for partial edits wastes output tokens and risks hallucinating unchanged parts
10
+ - Prefer idempotent writes by providing the full intended content when you do use write
@@ -1,184 +1,688 @@
1
1
  import { tool, type Tool } from 'ai';
2
2
  import { z } from 'zod';
3
- import { exec } from 'node:child_process';
4
- import { promisify } from 'node:util';
5
- import { writeFile, readFile, mkdir } from 'node:fs/promises';
3
+ import { readFile, writeFile, unlink, mkdir } from 'node:fs/promises';
4
+ import { dirname, resolve, relative, isAbsolute } from 'node:path';
6
5
  import DESCRIPTION from './patch.txt' with { type: 'text' };
7
6
 
8
- const execAsync = promisify(exec);
7
+ interface PatchAddOperation {
8
+ kind: 'add';
9
+ filePath: string;
10
+ lines: string[];
11
+ }
9
12
 
10
- /**
11
- * Apply enveloped patch by directly modifying files
12
- */
13
- async function applyEnvelopedPatch(projectRoot: string, patch: string) {
14
- const lines = patch.split('\n');
15
- let currentFile: string | null = null;
16
- let operation: 'add' | 'update' | 'delete' | null = null;
17
- let fileContent: string[] = [];
18
-
19
- async function applyCurrentFile() {
20
- if (!currentFile || !operation) return { ok: true };
21
-
22
- const fullPath = `${projectRoot}/${currentFile}`;
23
-
24
- if (operation === 'delete') {
25
- try {
26
- await writeFile(fullPath, '');
27
- } catch (e) {
28
- return {
29
- ok: false,
30
- error: `Failed to delete ${currentFile}: ${e instanceof Error ? e.message : String(e)}`,
31
- };
13
+ interface PatchDeleteOperation {
14
+ kind: 'delete';
15
+ filePath: string;
16
+ }
17
+
18
+ interface PatchUpdateOperation {
19
+ kind: 'update';
20
+ filePath: string;
21
+ hunks: PatchHunk[];
22
+ }
23
+
24
+ type ParsedPatchOperation =
25
+ | PatchAddOperation
26
+ | PatchDeleteOperation
27
+ | PatchUpdateOperation;
28
+
29
+ type PatchLineKind = 'context' | 'add' | 'remove';
30
+
31
+ interface PatchHunkLine {
32
+ kind: PatchLineKind;
33
+ content: string;
34
+ }
35
+
36
+ interface PatchHunkHeader {
37
+ oldStart?: number;
38
+ oldLines?: number;
39
+ newStart?: number;
40
+ newLines?: number;
41
+ context?: string;
42
+ }
43
+
44
+ interface PatchHunk {
45
+ header: PatchHunkHeader;
46
+ lines: PatchHunkLine[];
47
+ }
48
+
49
+ interface PatchStats {
50
+ additions: number;
51
+ deletions: number;
52
+ }
53
+
54
+ interface AppliedHunkResult {
55
+ header: PatchHunkHeader;
56
+ lines: PatchHunkLine[];
57
+ oldStart: number;
58
+ oldLines: number;
59
+ newStart: number;
60
+ newLines: number;
61
+ additions: number;
62
+ deletions: number;
63
+ }
64
+
65
+ interface AppliedOperationRecord {
66
+ kind: 'add' | 'delete' | 'update';
67
+ filePath: string;
68
+ operation: ParsedPatchOperation;
69
+ stats: PatchStats;
70
+ hunks: AppliedHunkResult[];
71
+ }
72
+
73
+ const PATCH_BEGIN_MARKER = '*** Begin Patch';
74
+ const PATCH_END_MARKER = '*** End Patch';
75
+ const PATCH_ADD_PREFIX = '*** Add File:';
76
+ const PATCH_UPDATE_PREFIX = '*** Update File:';
77
+ const PATCH_DELETE_PREFIX = '*** Delete File:';
78
+
79
+ function isErrnoException(value: unknown): value is NodeJS.ErrnoException {
80
+ return (
81
+ value instanceof Error &&
82
+ typeof (value as NodeJS.ErrnoException).code === 'string'
83
+ );
84
+ }
85
+
86
+ function splitLines(value: string): { lines: string[]; newline: string } {
87
+ const newline = value.includes('\r\n') ? '\r\n' : '\n';
88
+ const normalized = newline === '\n' ? value : value.replace(/\r\n/g, '\n');
89
+ const parts = normalized.split('\n');
90
+
91
+ if (parts.length > 0 && parts[parts.length - 1] === '') {
92
+ parts.pop();
93
+ }
94
+
95
+ return { lines: parts, newline };
96
+ }
97
+
98
+ function joinLines(lines: string[], newline: string): string {
99
+ const base = lines.join('\n');
100
+ return newline === '\n' ? base : base.replace(/\n/g, newline);
101
+ }
102
+
103
+ function ensureTrailingNewline(lines: string[]) {
104
+ if (lines.length === 0 || lines[lines.length - 1] !== '') {
105
+ lines.push('');
106
+ }
107
+ }
108
+
109
+ function findSubsequence(
110
+ lines: string[],
111
+ pattern: string[],
112
+ startIndex: number,
113
+ ): number {
114
+ if (pattern.length === 0) return -1;
115
+ const start = Math.max(0, startIndex);
116
+ for (let i = start; i <= lines.length - pattern.length; i++) {
117
+ let matches = true;
118
+ for (let j = 0; j < pattern.length; j++) {
119
+ if (lines[i + j] !== pattern[j]) {
120
+ matches = false;
121
+ break;
32
122
  }
33
- } else if (operation === 'add') {
34
- // For add, only use lines starting with +
35
- const newContent = fileContent
36
- .filter((l) => l.startsWith('+'))
37
- .map((l) => l.substring(1))
38
- .join('\n');
39
- try {
40
- await writeFile(fullPath, newContent);
41
- } catch (e) {
42
- return {
43
- ok: false,
44
- error: `Failed to create ${currentFile}: ${e instanceof Error ? e.message : String(e)}`,
45
- };
123
+ }
124
+ if (matches) return i;
125
+ }
126
+ return -1;
127
+ }
128
+
129
+ function parseDirectivePath(line: string, prefix: string): string {
130
+ const filePath = line.slice(prefix.length).trim();
131
+ if (!filePath) {
132
+ throw new Error(`Missing file path for directive: ${line}`);
133
+ }
134
+ if (filePath.startsWith('/') || isAbsolute(filePath)) {
135
+ throw new Error('Patch file paths must be relative to the project root.');
136
+ }
137
+ return filePath;
138
+ }
139
+
140
+ function parseHunkHeader(raw: string): PatchHunkHeader {
141
+ const numericMatch = raw.match(
142
+ /^@@\s*-(\d+)(?:,(\d+))?\s+\+(\d+)(?:,(\d+))?\s*@@(?:\s*(.*))?$/,
143
+ );
144
+ if (numericMatch) {
145
+ const [, oldStart, oldCount, newStart, newCount, context] = numericMatch;
146
+ return {
147
+ oldStart: Number.parseInt(oldStart, 10),
148
+ oldLines: oldCount ? Number.parseInt(oldCount, 10) : undefined,
149
+ newStart: Number.parseInt(newStart, 10),
150
+ newLines: newCount ? Number.parseInt(newCount, 10) : undefined,
151
+ context: context?.trim() || undefined,
152
+ };
153
+ }
154
+
155
+ const context = raw.replace(/^@@/, '').trim();
156
+ return context ? { context } : {};
157
+ }
158
+
159
+ function parseEnvelopedPatch(patch: string): ParsedPatchOperation[] {
160
+ const normalized = patch.replace(/\r\n/g, '\n');
161
+ const lines = normalized.split('\n');
162
+ const operations: ParsedPatchOperation[] = [];
163
+
164
+ type Builder =
165
+ | (PatchAddOperation & { kind: 'add' })
166
+ | (PatchDeleteOperation & { kind: 'delete' })
167
+ | (PatchUpdateOperation & {
168
+ kind: 'update';
169
+ currentHunk: PatchHunk | null;
170
+ });
171
+
172
+ let builder: Builder | null = null;
173
+ let insidePatch = false;
174
+ let encounteredEnd = false;
175
+
176
+ const flushBuilder = () => {
177
+ if (!builder) return;
178
+ if (builder.kind === 'update') {
179
+ if (builder.currentHunk && builder.currentHunk.lines.length === 0) {
180
+ builder.hunks.pop();
46
181
  }
47
- } else if (operation === 'update') {
48
- try {
49
- // Read existing file
50
- let existingContent = '';
51
- try {
52
- existingContent = await readFile(fullPath, 'utf-8');
53
- } catch {
54
- // File doesn't exist yet
55
- }
182
+ if (builder.hunks.length === 0) {
183
+ throw new Error(
184
+ `Update for ${builder.filePath} does not contain any diff hunks.`,
185
+ );
186
+ }
187
+ operations.push({
188
+ kind: 'update',
189
+ filePath: builder.filePath,
190
+ hunks: builder.hunks.map((hunk) => ({
191
+ header: { ...hunk.header },
192
+ lines: hunk.lines.map((line) => ({ ...line })),
193
+ })),
194
+ });
195
+ } else if (builder.kind === 'add') {
196
+ operations.push({
197
+ kind: 'add',
198
+ filePath: builder.filePath,
199
+ lines: [...builder.lines],
200
+ });
201
+ } else {
202
+ operations.push({ kind: 'delete', filePath: builder.filePath });
203
+ }
204
+ builder = null;
205
+ };
56
206
 
57
- // Get the old content (lines starting with -)
58
- const oldLines = fileContent
59
- .filter((l) => l.startsWith('-'))
60
- .map((l) => l.substring(1));
61
-
62
- // Get the new content (lines starting with +)
63
- const newLines = fileContent
64
- .filter((l) => l.startsWith('+'))
65
- .map((l) => l.substring(1));
66
-
67
- // Simple replacement: if old content is empty, append
68
- // Otherwise try to replace old with new
69
- let newContent = existingContent;
70
- if (oldLines.length > 0) {
71
- const oldText = oldLines.join('\n');
72
- const newText = newLines.join('\n');
73
-
74
- // Try exact match first
75
- if (existingContent.includes(oldText)) {
76
- newContent = existingContent.replace(oldText, newText);
77
- } else {
78
- // Try normalizing whitespace for more flexible matching
79
- const normalizeWhitespace = (s: string) =>
80
- s.replace(/\s+/g, ' ').trim();
81
- const normalizedOld = normalizeWhitespace(oldText);
82
- const normalizedExisting = normalizeWhitespace(existingContent);
83
-
84
- if (normalizedExisting.includes(normalizedOld)) {
85
- // Find the actual text in the file by matching normalized version
86
- const lines = existingContent.split('\n');
87
- let found = false;
88
-
89
- for (let i = 0; i < lines.length; i++) {
90
- const candidate = lines
91
- .slice(i, i + oldLines.length)
92
- .join('\n');
93
- if (normalizeWhitespace(candidate) === normalizedOld) {
94
- // Replace this section
95
- const before = lines.slice(0, i).join('\n');
96
- const after = lines.slice(i + oldLines.length).join('\n');
97
- newContent =
98
- before +
99
- (before ? '\n' : '') +
100
- newText +
101
- (after ? '\n' : '') +
102
- after;
103
- found = true;
104
- break;
105
- }
106
- }
107
-
108
- if (!found) {
109
- const preview = oldText.substring(0, 100);
110
- return {
111
- ok: false,
112
- error: `Cannot find exact location to replace in ${currentFile}. Looking for: "${preview}${oldText.length > 100 ? '...' : ''}"`,
113
- };
114
- }
115
- } else {
116
- // Can't find even with normalized whitespace
117
- const preview = oldText.substring(0, 100);
118
- const filePreview = existingContent.substring(0, 200);
119
- return {
120
- ok: false,
121
- error: `Cannot find content to replace in ${currentFile}.\nLooking for: "${preview}${oldText.length > 100 ? '...' : ''}"\nFile contains: "${filePreview}${existingContent.length > 200 ? '...' : ''}"`,
122
- };
123
- }
124
- }
125
- } else if (newLines.length > 0) {
126
- // Just appending new lines
127
- newContent =
128
- existingContent +
129
- (existingContent.endsWith('\n') ? '' : '\n') +
130
- newLines.join('\n');
131
- }
207
+ for (let i = 0; i < lines.length; i++) {
208
+ const line = lines[i];
209
+ if (!insidePatch) {
210
+ if (line.trim() === '') continue;
211
+ if (line.startsWith(PATCH_BEGIN_MARKER)) {
212
+ insidePatch = true;
213
+ continue;
214
+ }
215
+ throw new Error(
216
+ 'Patch must start with "*** Begin Patch" and use the enveloped patch format.',
217
+ );
218
+ }
219
+
220
+ if (line.startsWith(PATCH_BEGIN_MARKER)) {
221
+ throw new Error('Nested "*** Begin Patch" markers are not supported.');
222
+ }
132
223
 
133
- await writeFile(fullPath, newContent);
134
- } catch (e) {
135
- return {
136
- ok: false,
137
- error: `Failed to update ${currentFile}: ${e instanceof Error ? e.message : String(e)}`,
138
- };
224
+ if (line.startsWith(PATCH_END_MARKER)) {
225
+ flushBuilder();
226
+ encounteredEnd = true;
227
+ const remaining = lines.slice(i + 1).find((rest) => rest.trim() !== '');
228
+ if (remaining) {
229
+ throw new Error(
230
+ 'Unexpected content found after "*** End Patch" marker.',
231
+ );
139
232
  }
233
+ break;
234
+ }
235
+
236
+ if (line.startsWith(PATCH_ADD_PREFIX)) {
237
+ flushBuilder();
238
+ builder = {
239
+ kind: 'add',
240
+ filePath: parseDirectivePath(line, PATCH_ADD_PREFIX),
241
+ lines: [],
242
+ };
243
+ continue;
140
244
  }
141
- return { ok: true };
142
- }
143
245
 
144
- for (const line of lines) {
145
- if (line === '*** Begin Patch' || line === '*** End Patch') {
246
+ if (line.startsWith(PATCH_UPDATE_PREFIX)) {
247
+ flushBuilder();
248
+ builder = {
249
+ kind: 'update',
250
+ filePath: parseDirectivePath(line, PATCH_UPDATE_PREFIX),
251
+ hunks: [],
252
+ currentHunk: null,
253
+ };
146
254
  continue;
147
255
  }
148
256
 
149
- if (
150
- line.startsWith('*** Add File:') ||
151
- line.startsWith('*** Update File:') ||
152
- line.startsWith('*** Delete File:')
153
- ) {
154
- // Apply previous file if any
155
- const result = await applyCurrentFile();
156
- if (!result.ok) return result;
157
-
158
- // Start new file
159
- if (line.startsWith('*** Add File:')) {
160
- currentFile = line.replace('*** Add File:', '').trim();
161
- operation = 'add';
162
- } else if (line.startsWith('*** Update File:')) {
163
- currentFile = line.replace('*** Update File:', '').trim();
164
- operation = 'update';
165
- } else if (line.startsWith('*** Delete File:')) {
166
- currentFile = line.replace('*** Delete File:', '').trim();
167
- operation = 'delete';
257
+ if (line.startsWith(PATCH_DELETE_PREFIX)) {
258
+ flushBuilder();
259
+ builder = {
260
+ kind: 'delete',
261
+ filePath: parseDirectivePath(line, PATCH_DELETE_PREFIX),
262
+ };
263
+ continue;
264
+ }
265
+
266
+ if (!builder) {
267
+ if (line.trim() === '') {
268
+ continue;
168
269
  }
169
- fileContent = [];
170
- } else if (
171
- currentFile &&
172
- (line.startsWith('+') || line.startsWith('-') || line.startsWith(' '))
173
- ) {
174
- // Collect patch content lines
175
- fileContent.push(line);
270
+ throw new Error(`Unexpected content in patch: "${line}"`);
271
+ }
272
+
273
+ if (builder.kind === 'add') {
274
+ const content = line.startsWith('+') ? line.slice(1) : line;
275
+ builder.lines.push(content);
276
+ continue;
176
277
  }
278
+
279
+ if (builder.kind === 'delete') {
280
+ if (line.trim() !== '') {
281
+ throw new Error(
282
+ `Delete directive for ${builder.filePath} should not contain additional lines.`,
283
+ );
284
+ }
285
+ continue;
286
+ }
287
+
288
+ if (line.startsWith('@@')) {
289
+ const hunk: PatchHunk = { header: parseHunkHeader(line), lines: [] };
290
+ builder.hunks.push(hunk);
291
+ builder.currentHunk = hunk;
292
+ continue;
293
+ }
294
+
295
+ if (!builder.currentHunk) {
296
+ const fallbackHunk: PatchHunk = { header: {}, lines: [] };
297
+ builder.hunks.push(fallbackHunk);
298
+ builder.currentHunk = fallbackHunk;
299
+ }
300
+
301
+ const currentHunk = builder.currentHunk;
302
+ const prefix = line[0];
303
+ if (prefix === '+') {
304
+ currentHunk.lines.push({ kind: 'add', content: line.slice(1) });
305
+ } else if (prefix === '-') {
306
+ currentHunk.lines.push({ kind: 'remove', content: line.slice(1) });
307
+ } else if (prefix === ' ') {
308
+ currentHunk.lines.push({ kind: 'context', content: line.slice(1) });
309
+ } else {
310
+ throw new Error(`Unrecognized patch line: "${line}"`);
311
+ }
312
+ }
313
+
314
+ if (!encounteredEnd) {
315
+ throw new Error('Missing "*** End Patch" marker.');
177
316
  }
178
317
 
179
- // Apply the last file
180
- const result = await applyCurrentFile();
181
- return result;
318
+ return operations;
319
+ }
320
+
321
+ function resolveProjectPath(projectRoot: string, filePath: string): string {
322
+ const fullPath = resolve(projectRoot, filePath);
323
+ const relativePath = relative(projectRoot, fullPath);
324
+ if (relativePath.startsWith('..') || isAbsolute(relativePath)) {
325
+ throw new Error(`Patch path escapes project root: ${filePath}`);
326
+ }
327
+ return fullPath;
328
+ }
329
+
330
+ async function applyAddOperation(
331
+ projectRoot: string,
332
+ operation: PatchAddOperation,
333
+ ): Promise<AppliedOperationRecord> {
334
+ const targetPath = resolveProjectPath(projectRoot, operation.filePath);
335
+ await mkdir(dirname(targetPath), { recursive: true });
336
+ const linesForWrite = [...operation.lines];
337
+ ensureTrailingNewline(linesForWrite);
338
+ await writeFile(targetPath, joinLines(linesForWrite, '\n'), 'utf-8');
339
+
340
+ const hunkLines: PatchHunkLine[] = operation.lines.map((line) => ({
341
+ kind: 'add',
342
+ content: line,
343
+ }));
344
+
345
+ return {
346
+ kind: 'add',
347
+ filePath: operation.filePath,
348
+ operation,
349
+ stats: {
350
+ additions: hunkLines.length,
351
+ deletions: 0,
352
+ },
353
+ hunks: [
354
+ {
355
+ header: {},
356
+ lines: hunkLines,
357
+ oldStart: 0,
358
+ oldLines: 0,
359
+ newStart: 1,
360
+ newLines: hunkLines.length,
361
+ additions: hunkLines.length,
362
+ deletions: 0,
363
+ },
364
+ ],
365
+ };
366
+ }
367
+
368
+ async function applyDeleteOperation(
369
+ projectRoot: string,
370
+ operation: PatchDeleteOperation,
371
+ ): Promise<AppliedOperationRecord> {
372
+ const targetPath = resolveProjectPath(projectRoot, operation.filePath);
373
+ let existingContent = '';
374
+ try {
375
+ existingContent = await readFile(targetPath, 'utf-8');
376
+ } catch (error) {
377
+ if (isErrnoException(error) && error.code === 'ENOENT') {
378
+ throw new Error(`File not found for deletion: ${operation.filePath}`);
379
+ }
380
+ throw error;
381
+ }
382
+
383
+ const { lines } = splitLines(existingContent);
384
+ await unlink(targetPath);
385
+
386
+ const hunkLines: PatchHunkLine[] = lines.map((line) => ({
387
+ kind: 'remove',
388
+ content: line,
389
+ }));
390
+
391
+ return {
392
+ kind: 'delete',
393
+ filePath: operation.filePath,
394
+ operation,
395
+ stats: {
396
+ additions: 0,
397
+ deletions: hunkLines.length,
398
+ },
399
+ hunks: [
400
+ {
401
+ header: {},
402
+ lines: hunkLines,
403
+ oldStart: 1,
404
+ oldLines: hunkLines.length,
405
+ newStart: 0,
406
+ newLines: 0,
407
+ additions: 0,
408
+ deletions: hunkLines.length,
409
+ },
410
+ ],
411
+ };
412
+ }
413
+
414
+ function applyHunksToLines(
415
+ originalLines: string[],
416
+ hunks: PatchHunk[],
417
+ filePath: string,
418
+ ): { lines: string[]; applied: AppliedHunkResult[] } {
419
+ const lines = [...originalLines];
420
+ let searchIndex = 0;
421
+ let lineOffset = 0;
422
+ const applied: AppliedHunkResult[] = [];
423
+
424
+ for (const hunk of hunks) {
425
+ const expected: string[] = [];
426
+ const replacement: string[] = [];
427
+ let additions = 0;
428
+ let deletions = 0;
429
+
430
+ for (const line of hunk.lines) {
431
+ if (line.kind !== 'add') {
432
+ expected.push(line.content);
433
+ }
434
+ if (line.kind !== 'remove') {
435
+ replacement.push(line.content);
436
+ }
437
+ if (line.kind === 'add') additions += 1;
438
+ if (line.kind === 'remove') deletions += 1;
439
+ }
440
+
441
+ const hasExpected = expected.length > 0;
442
+ const hint =
443
+ typeof hunk.header.oldStart === 'number'
444
+ ? Math.max(0, hunk.header.oldStart - 1 + lineOffset)
445
+ : searchIndex;
446
+
447
+ let matchIndex = hasExpected
448
+ ? findSubsequence(lines, expected, Math.max(0, hint - 3))
449
+ : -1;
450
+
451
+ if (hasExpected && matchIndex === -1) {
452
+ matchIndex = findSubsequence(lines, expected, 0);
453
+ }
454
+
455
+ if (matchIndex === -1 && hasExpected && hunk.header.context) {
456
+ const contextIndex = findSubsequence(lines, [hunk.header.context], 0);
457
+ if (contextIndex !== -1) {
458
+ const positionInExpected = expected.indexOf(hunk.header.context);
459
+ matchIndex =
460
+ positionInExpected >= 0
461
+ ? Math.max(0, contextIndex - positionInExpected)
462
+ : contextIndex;
463
+ }
464
+ }
465
+
466
+ if (!hasExpected) {
467
+ matchIndex = computeInsertionIndex(lines, hint, hunk.header);
468
+ }
469
+
470
+ if (matchIndex === -1) {
471
+ const contextInfo = hunk.header.context
472
+ ? ` near context '${hunk.header.context}'`
473
+ : '';
474
+ throw new Error(
475
+ `Failed to apply patch hunk in ${filePath}${contextInfo}.`,
476
+ );
477
+ }
478
+
479
+ const deleteCount = hasExpected ? expected.length : 0;
480
+ const originalIndex = matchIndex - lineOffset;
481
+ const normalizedOriginalIndex = Math.min(
482
+ Math.max(0, originalIndex),
483
+ originalLines.length,
484
+ );
485
+ const oldStart = normalizedOriginalIndex + 1;
486
+ const newStart = matchIndex + 1;
487
+
488
+ lines.splice(matchIndex, deleteCount, ...replacement);
489
+ searchIndex = matchIndex + replacement.length;
490
+ lineOffset += replacement.length - deleteCount;
491
+
492
+ applied.push({
493
+ header: { ...hunk.header },
494
+ lines: hunk.lines.map((line) => ({ ...line })),
495
+ oldStart,
496
+ oldLines: deleteCount,
497
+ newStart,
498
+ newLines: replacement.length,
499
+ additions,
500
+ deletions,
501
+ });
502
+ }
503
+
504
+ return { lines, applied };
505
+ }
506
+
507
+ function computeInsertionIndex(
508
+ lines: string[],
509
+ hint: number,
510
+ header: PatchHunkHeader,
511
+ ): number {
512
+ if (header.context) {
513
+ const contextIndex = findSubsequence(lines, [header.context], 0);
514
+ if (contextIndex !== -1) {
515
+ return contextIndex + 1;
516
+ }
517
+ }
518
+
519
+ if (typeof header.oldStart === 'number') {
520
+ const zeroBased = Math.max(0, header.oldStart - 1);
521
+ return Math.min(lines.length, zeroBased);
522
+ }
523
+
524
+ if (typeof header.newStart === 'number') {
525
+ const zeroBased = Math.max(0, header.newStart - 1);
526
+ return Math.min(lines.length, zeroBased);
527
+ }
528
+
529
+ return Math.min(lines.length, Math.max(0, hint));
530
+ }
531
+
532
+ async function applyUpdateOperation(
533
+ projectRoot: string,
534
+ operation: PatchUpdateOperation,
535
+ ): Promise<AppliedOperationRecord> {
536
+ const targetPath = resolveProjectPath(projectRoot, operation.filePath);
537
+ let originalContent: string;
538
+ try {
539
+ originalContent = await readFile(targetPath, 'utf-8');
540
+ } catch (error) {
541
+ if (isErrnoException(error) && error.code === 'ENOENT') {
542
+ throw new Error(`File not found: ${operation.filePath}`);
543
+ }
544
+ throw error;
545
+ }
546
+
547
+ const { lines: originalLines, newline } = splitLines(originalContent);
548
+ const { lines: updatedLines, applied } = applyHunksToLines(
549
+ originalLines,
550
+ operation.hunks,
551
+ operation.filePath,
552
+ );
553
+ ensureTrailingNewline(updatedLines);
554
+ await writeFile(targetPath, joinLines(updatedLines, newline), 'utf-8');
555
+
556
+ const stats = applied.reduce<PatchStats>(
557
+ (acc, hunk) => ({
558
+ additions: acc.additions + hunk.additions,
559
+ deletions: acc.deletions + hunk.deletions,
560
+ }),
561
+ { additions: 0, deletions: 0 },
562
+ );
563
+
564
+ return {
565
+ kind: 'update',
566
+ filePath: operation.filePath,
567
+ operation,
568
+ stats,
569
+ hunks: applied,
570
+ };
571
+ }
572
+
573
+ function summarizeOperations(operations: AppliedOperationRecord[]) {
574
+ const summary = operations.reduce(
575
+ (acc, op) => ({
576
+ files: acc.files + 1,
577
+ additions: acc.additions + op.stats.additions,
578
+ deletions: acc.deletions + op.stats.deletions,
579
+ }),
580
+ { files: 0, additions: 0, deletions: 0 },
581
+ );
582
+ return {
583
+ files: Math.max(summary.files, operations.length > 0 ? 1 : 0),
584
+ additions: summary.additions,
585
+ deletions: summary.deletions,
586
+ };
587
+ }
588
+
589
+ function formatRange(start: number, count: number) {
590
+ const normalizedStart = Math.max(0, start);
591
+ if (count === 0) return `${normalizedStart},0`;
592
+ if (count === 1) return `${normalizedStart}`;
593
+ return `${normalizedStart},${count}`;
594
+ }
595
+
596
+ function formatHunkHeader(applied: AppliedHunkResult) {
597
+ const oldRange = formatRange(applied.oldStart, applied.oldLines);
598
+ const newRange = formatRange(applied.newStart, applied.newLines);
599
+ const context = applied.header.context?.trim();
600
+ return context
601
+ ? `@@ -${oldRange} +${newRange} @@ ${context}`
602
+ : `@@ -${oldRange} +${newRange} @@`;
603
+ }
604
+
605
+ function serializePatchLine(line: PatchHunkLine): string {
606
+ switch (line.kind) {
607
+ case 'add':
608
+ return `+${line.content}`;
609
+ case 'remove':
610
+ return `-${line.content}`;
611
+ default:
612
+ return ` ${line.content}`;
613
+ }
614
+ }
615
+
616
+ function formatNormalizedPatch(operations: AppliedOperationRecord[]): string {
617
+ const lines: string[] = [PATCH_BEGIN_MARKER];
618
+
619
+ for (const op of operations) {
620
+ switch (op.kind) {
621
+ case 'add': {
622
+ lines.push(`${PATCH_ADD_PREFIX} ${op.filePath}`);
623
+ for (const hunk of op.hunks) {
624
+ lines.push(formatHunkHeader(hunk));
625
+ for (const line of hunk.lines) {
626
+ lines.push(serializePatchLine(line));
627
+ }
628
+ }
629
+ break;
630
+ }
631
+ case 'delete': {
632
+ lines.push(`${PATCH_DELETE_PREFIX} ${op.filePath}`);
633
+ for (const hunk of op.hunks) {
634
+ lines.push(formatHunkHeader(hunk));
635
+ for (const line of hunk.lines) {
636
+ lines.push(serializePatchLine(line));
637
+ }
638
+ }
639
+ break;
640
+ }
641
+ case 'update': {
642
+ lines.push(`${PATCH_UPDATE_PREFIX} ${op.filePath}`);
643
+ const updateOp = op.operation as PatchUpdateOperation;
644
+ for (let i = 0; i < updateOp.hunks.length; i++) {
645
+ const originalHunk = updateOp.hunks[i];
646
+ const appliedHunk = op.hunks[i];
647
+ const header: AppliedHunkResult = {
648
+ ...appliedHunk,
649
+ header: {
650
+ ...appliedHunk.header,
651
+ context: originalHunk.header.context,
652
+ },
653
+ };
654
+ lines.push(formatHunkHeader(header));
655
+ for (const line of originalHunk.lines) {
656
+ lines.push(serializePatchLine(line));
657
+ }
658
+ }
659
+ break;
660
+ }
661
+ }
662
+ }
663
+
664
+ lines.push(PATCH_END_MARKER);
665
+ return lines.join('\n');
666
+ }
667
+
668
+ async function applyEnvelopedPatch(projectRoot: string, patch: string) {
669
+ const operations = parseEnvelopedPatch(patch);
670
+ const applied: AppliedOperationRecord[] = [];
671
+
672
+ for (const operation of operations) {
673
+ if (operation.kind === 'add') {
674
+ applied.push(await applyAddOperation(projectRoot, operation));
675
+ } else if (operation.kind === 'delete') {
676
+ applied.push(await applyDeleteOperation(projectRoot, operation));
677
+ } else {
678
+ applied.push(await applyUpdateOperation(projectRoot, operation));
679
+ }
680
+ }
681
+
682
+ return {
683
+ operations: applied,
684
+ normalizedPatch: formatNormalizedPatch(applied),
685
+ };
182
686
  }
183
687
 
184
688
  export function buildApplyPatchTool(projectRoot: string): {
@@ -197,114 +701,46 @@ export function buildApplyPatchTool(projectRoot: string): {
197
701
  'Allow hunks to be rejected without failing the whole operation',
198
702
  ),
199
703
  }),
200
- async execute({
201
- patch,
202
- allowRejects,
203
- }: {
204
- patch: string;
205
- allowRejects?: boolean;
206
- }) {
207
- // Check if this is an enveloped patch format
208
- const isEnveloped =
209
- patch.includes('*** Begin Patch') ||
210
- patch.includes('*** Add File:') ||
211
- patch.includes('*** Update File:');
212
-
213
- if (isEnveloped) {
214
- // Handle enveloped patches directly
215
- const result = await applyEnvelopedPatch(projectRoot, patch);
216
- const summary = summarizePatch(patch);
217
- if (result.ok) {
218
- return {
219
- ok: true,
220
- output: 'Applied enveloped patch',
221
- artifact: { kind: 'file_diff', patch, summary },
222
- } as const;
223
- } else {
224
- return {
225
- ok: false,
226
- error: result.error || 'Failed to apply enveloped patch',
227
- artifact: { kind: 'file_diff', patch, summary },
228
- } as const;
229
- }
704
+ async execute({ patch }: { patch: string; allowRejects?: boolean }) {
705
+ if (
706
+ !patch.includes(PATCH_BEGIN_MARKER) ||
707
+ !patch.includes(PATCH_END_MARKER)
708
+ ) {
709
+ throw new Error(
710
+ 'Only enveloped patch format is supported. Patch must start with "*** Begin Patch" and contain "*** Add File:", "*** Update File:", or "*** Delete File:" directives.',
711
+ );
230
712
  }
231
713
 
232
- // For unified diffs, use git apply as before
233
- const dir = `${projectRoot}/.agi/tmp`;
234
- await mkdir(dir, { recursive: true }).catch(() => {});
235
- const file = `${dir}/patch-${Date.now()}.diff`;
236
- await writeFile(file, patch);
237
- const summary = summarizePatch(patch);
238
- // Try -p1 first for canonical git-style patches (a/ b/ prefixes), then fall back to -p0.
239
- const baseArgs = ['apply', '--whitespace=nowarn'];
240
- const rejectArg = allowRejects ? '--reject' : '';
241
- const tries = [
242
- `git -C "${projectRoot}" ${baseArgs.join(' ')} ${rejectArg} -p1 "${file}"`,
243
- `git -C "${projectRoot}" ${baseArgs.join(' ')} ${rejectArg} -p0 "${file}"`,
244
- ];
245
- let lastError = '';
246
- for (const cmd of tries) {
247
- try {
248
- const { stdout } = await execAsync(cmd, {
249
- maxBuffer: 10 * 1024 * 1024,
250
- });
251
- // Check if any files were actually modified
252
- try {
253
- const { stdout: statusOut } = await execAsync(
254
- `git -C "${projectRoot}" status --porcelain`,
255
- );
256
- if (statusOut && statusOut.trim().length > 0) {
257
- return {
258
- ok: true,
259
- output: stdout.trim(),
260
- artifact: { kind: 'file_diff', patch, summary },
261
- } as const;
262
- }
263
- } catch {}
264
- } catch (error: unknown) {
265
- const err = error as { stderr?: string; message?: string };
266
- lastError = err.stderr || err.message || 'git apply failed';
267
- }
268
- }
714
+ const { operations, normalizedPatch } = await applyEnvelopedPatch(
715
+ projectRoot,
716
+ patch,
717
+ );
718
+ const summary = summarizeOperations(operations);
719
+ const changes = operations.map((operation) => ({
720
+ filePath: operation.filePath,
721
+ kind: operation.kind,
722
+ hunks: operation.hunks.map((hunk) => ({
723
+ oldStart: hunk.oldStart,
724
+ oldLines: hunk.oldLines,
725
+ newStart: hunk.newStart,
726
+ newLines: hunk.newLines,
727
+ additions: hunk.additions,
728
+ deletions: hunk.deletions,
729
+ context: hunk.header.context,
730
+ })),
731
+ }));
269
732
 
270
- // Final check if files were modified anyway
271
- try {
272
- const { stdout: statusOut } = await execAsync(
273
- `git -C "${projectRoot}" status --porcelain`,
274
- );
275
- if (statusOut && statusOut.trim().length > 0) {
276
- return {
277
- ok: true,
278
- output: 'Patch applied with warnings',
279
- artifact: { kind: 'file_diff', patch, summary },
280
- } as const;
281
- }
282
- } catch {}
283
-
284
- // If both attempts fail and no files changed, return error with more context
285
- const errorDetails = lastError.includes('patch does not apply')
286
- ? 'The patch cannot be applied because the target content has changed or does not match. The file may have been modified since the patch was created.'
287
- : lastError ||
288
- 'git apply failed (tried -p1 and -p0) — ensure paths match project root';
289
733
  return {
290
- ok: false,
291
- error: errorDetails,
292
- artifact: { kind: 'file_diff', patch, summary },
734
+ ok: true,
735
+ output: 'Applied enveloped patch',
736
+ changes,
737
+ artifact: {
738
+ kind: 'file_diff',
739
+ patch: normalizedPatch,
740
+ summary,
741
+ },
293
742
  } as const;
294
743
  },
295
744
  });
296
745
  return { name: 'apply_patch', tool: applyPatch };
297
746
  }
298
-
299
- function summarizePatch(patch: string) {
300
- const lines = String(patch || '').split('\n');
301
- let files = 0;
302
- let additions = 0;
303
- let deletions = 0;
304
- for (const l of lines) {
305
- if (/^\*\*\*\s+(Add|Update|Delete) File:/.test(l)) files += 1;
306
- else if (l.startsWith('+') && !l.startsWith('+++')) additions += 1;
307
- else if (l.startsWith('-') && !l.startsWith('---')) deletions += 1;
308
- }
309
- return { files, additions, deletions };
310
- }
@@ -1,7 +1,104 @@
1
- - Apply a unified diff patch (`*** Begin Patch`/`*** Update File`/`---`/`+++`/`@@`)
2
- - Uses `git apply` under the hood; tries `-p1` then `-p0`
3
- - Returns an artifact summary and any output from `git apply`
1
+ Apply a patch to modify one or more files using the enveloped patch format.
4
2
 
5
- Usage tips:
6
- - Ensure paths in the patch match the project root (prefer `a/` and `b/` prefixes)
7
- - For small edits, consider the Edit or Write tools instead
3
+ **RECOMMENDED: Use apply_patch for targeted file edits to avoid rewriting entire files and wasting tokens.**
4
+
5
+ Use `apply_patch` only when:
6
+ - You want to make targeted edits to specific lines (primary use case)
7
+ - You want to make multiple related changes across different files in a single operation
8
+ - You need to add/delete entire files along with modifications
9
+ - You have JUST read the file and are confident the content hasn't changed
10
+
11
+ **IMPORTANT: Patches require EXACT line matches. If the file content has changed since you last read it, the patch will fail.**
12
+
13
+ **Alternative: Use the `edit` tool if you need fuzzy matching or structured operations.**
14
+
15
+ ## Patch Format
16
+
17
+ All patches must be wrapped in markers and use explicit file directives:
18
+
19
+ ```
20
+ *** Begin Patch
21
+ *** Add File: path/to/file.txt
22
+ +line 1
23
+ +line 2
24
+ *** Update File: path/to/other.txt
25
+ -old line
26
+ +new line
27
+ *** Delete File: path/to/delete.txt
28
+ *** End Patch
29
+ ```
30
+
31
+ ## File Operations
32
+
33
+ ### Add a new file:
34
+ ```
35
+ *** Begin Patch
36
+ *** Add File: src/hello.ts
37
+ +export function hello() {
38
+ + console.log("Hello!");
39
+ +}
40
+ *** End Patch
41
+ ```
42
+
43
+ ### Update an existing file (simple replacement):
44
+ ```
45
+ *** Begin Patch
46
+ *** Update File: src/config.ts
47
+ -const PORT = 3000;
48
+ +const PORT = 8080;
49
+ *** End Patch
50
+ ```
51
+
52
+ **CRITICAL**: The `-` lines must match EXACTLY what's in the file, character-for-character. If you're not 100% certain, use the `edit` tool instead.
53
+
54
+ ### Update with context (recommended for precision):
55
+ ```
56
+ *** Begin Patch
57
+ *** Update File: src/app.ts
58
+ @@ function main()
59
+ function main() {
60
+ - console.log("old");
61
+ + console.log("new");
62
+ }
63
+ *** End Patch
64
+ ```
65
+
66
+ The `@@ context line` helps locate the exact position, but the `-` lines must still match exactly.
67
+
68
+ ### Delete a file:
69
+ ```
70
+ *** Begin Patch
71
+ *** Delete File: old/unused.ts
72
+ *** End Patch
73
+ ```
74
+
75
+ ### Multiple operations in one patch:
76
+ ```
77
+ *** Begin Patch
78
+ *** Add File: new.txt
79
+ +New content
80
+ *** Update File: existing.txt
81
+ -old
82
+ +new
83
+ *** Delete File: obsolete.txt
84
+ *** End Patch
85
+ ```
86
+
87
+ ## Line Prefixes
88
+
89
+ - Lines starting with `+` are added
90
+ - Lines starting with `-` are removed
91
+ - Lines starting with ` ` (space) are context (kept unchanged)
92
+ - Lines starting with `@@` provide context for finding the location
93
+
94
+ ## Common Errors
95
+
96
+ **"Failed to find expected lines"**: The file content doesn't match your patch. The file may have changed, or you may have mistyped the lines. Solution: Use the `edit` tool instead.
97
+
98
+ ## Important Notes
99
+
100
+ - **Patches are fragile**: Any mismatch in whitespace, indentation, or content will cause failure
101
+ - **Use `edit` for reliability**: The `edit` tool can make targeted changes without requiring exact matches
102
+ - All file paths are relative to the project root
103
+ - The patch format does NOT support standard unified diff format (no `---`/`+++` headers)
104
+ - Always wrap patches with `*** Begin Patch` and `*** End Patch`
@@ -74,7 +74,7 @@ When making changes to files, first understand the file's code conventions. Mimi
74
74
  - IMPORTANT: DO NOT ADD ***ANY*** COMMENTS unless asked
75
75
 
76
76
  ## Tool Selection Guidelines
77
- - For file editing, prefer `edit` with structured operations over `apply_patch` for better reliability
77
+ - For file editing, prefer `apply_patch` for targeted edits to avoid rewriting entire files and wasting tokens
78
78
  - When you need to make multiple edits to the same file, combine them in a single `edit` call with multiple ops
79
79
  - Use `ripgrep` over `grep` for faster searching across large codebases
80
80
  - Always use `glob` first to discover files before reading them, unless you know exact paths
@@ -82,19 +82,28 @@ When making changes to files, first understand the file's code conventions. Mimi
82
82
 
83
83
  ## File Editing Best Practices
84
84
 
85
- **Using the `edit` Tool** (Recommended):
85
+ **Using the `apply_patch` Tool** (Recommended):
86
+ - Primary choice for targeted file edits - avoids rewriting entire files
87
+ - Only requires the specific lines you want to change
88
+ - Format: `*** Begin Patch` ... `*** Update File: path` ... `-old` / `+new` ... `*** End Patch`
89
+ - Only use when you have a complete unified diff ready
90
+ - Ensure patch is based on current file content (not stale)
91
+ - If patch fails, read the file first to see current state before retrying
92
+
93
+ **Using the `edit` Tool** (Alternative):
86
94
  - Specify the file path and a list of operations
87
95
  - Operations are applied sequentially to the latest file state
88
96
  - Operation types: `replace`, `insert-before`, `insert-after`, `delete`
89
97
  - Each operation includes: `old` (text to match/find) and `new` (replacement/insertion)
90
98
  - When making multiple changes to a file, use ONE `edit` call with multiple ops
91
99
 
92
- **Using the `apply_patch` Tool** (Alternative):
93
- - Only use when you have a complete unified diff ready
94
- - Ensure patch is based on current file content (not stale)
95
- - If patch fails, read the file first to see current state before retrying
100
+ **Using the `write` Tool** (Last Resort):
101
+ - Only use when creating new files or completely replacing file content
102
+ - NEVER use for targeted edits - it rewrites the entire file
103
+ - Wastes output tokens and risks hallucinating unchanged parts
96
104
 
97
105
  **Never**:
106
+ - Use `write` for partial file edits (use `apply_patch` or `edit` instead)
98
107
  - Make multiple separate `edit` or `apply_patch` calls for the same file
99
108
  - Assume file content remains unchanged between operations
100
109
  - Use `bash` with `sed`/`awk` for programmatic file editing (use `edit` instead)
@@ -22,8 +22,8 @@ You have access to a rich set of specialized tools optimized for coding tasks:
22
22
  **File Reading & Editing**:
23
23
  - `read`: Read file contents (supports line ranges)
24
24
  - `write`: Write complete file contents
25
- - `edit`: Structured file editing with operations (PREFERRED for modifications)
26
- - `apply_patch`: Apply unified diff patches (alternative editing method)
25
+ - `apply_patch`: Apply unified diff patches (RECOMMENDED for targeted edits)
26
+ - `edit`: Structured file editing with operations (alternative editing method)
27
27
 
28
28
  **Version Control**:
29
29
  - `git_status`, `git_diff`, `git_log`, `git_show`, `git_commit`
@@ -37,7 +37,7 @@ You have access to a rich set of specialized tools optimized for coding tasks:
37
37
  ### Tool Usage Best Practices:
38
38
 
39
39
  1. **Batch Independent Operations**: Make all independent tool calls in one turn
40
- 2. **File Editing**: Prefer `edit` over `apply_patch` for reliability
40
+ 2. **File Editing**: Prefer `apply_patch` for targeted edits to avoid rewriting entire files
41
41
  3. **Combine Edits**: When editing the same file multiple times, use ONE `edit` call with multiple ops
42
42
  4. **Search First**: Use `glob` to find files before reading them
43
43
  5. **Progress Updates**: Call `progress_update` at major milestones (planning, discovering, writing, verifying)
@@ -46,19 +46,28 @@ You have access to a rich set of specialized tools optimized for coding tasks:
46
46
 
47
47
  ## File Editing Best Practices
48
48
 
49
- **Using the `edit` Tool** (Recommended):
49
+ **Using the `apply_patch` Tool** (Recommended):
50
+ - Primary choice for targeted file edits - avoids rewriting entire files
51
+ - Only requires the specific lines you want to change
52
+ - Format: `*** Begin Patch` ... `*** Update File: path` ... `-old` / `+new` ... `*** End Patch`
53
+ - Only use when you have a complete unified diff ready
54
+ - Ensure patch is based on current file content (not stale)
55
+ - If patch fails, read the file first to see current state before retrying
56
+
57
+ **Using the `edit` Tool** (Alternative):
50
58
  - Specify the file path and a list of operations
51
59
  - Operations are applied sequentially to the latest file state
52
60
  - Operation types: `replace`, `insert-before`, `insert-after`, `delete`
53
61
  - Each operation includes: `old` (text to match/find) and `new` (replacement/insertion)
54
62
  - When making multiple changes to a file, use ONE `edit` call with multiple ops
55
63
 
56
- **Using the `apply_patch` Tool** (Alternative):
57
- - Only use when you have a complete unified diff ready
58
- - Ensure patch is based on current file content (not stale)
59
- - If patch fails, read the file first to see current state before retrying
64
+ **Using the `write` Tool** (Last Resort):
65
+ - Only use when creating new files or completely replacing file content
66
+ - NEVER use for targeted edits - it rewrites the entire file
67
+ - Wastes output tokens and risks hallucinating unchanged parts
60
68
 
61
69
  **Never**:
70
+ - Use `write` for partial file edits (use `apply_patch` or `edit` instead)
62
71
  - Make multiple separate `edit` or `apply_patch` calls for the same file
63
72
  - Assume file content remains unchanged between operations
64
73
  - Use `bash` with `sed`/`awk` for programmatic file editing (use `edit` instead)
@@ -18,7 +18,7 @@ You are opencode, an interactive CLI agent specializing in software engineering
18
18
  Your primary tools for coding tasks are:
19
19
  - **Discovery**: `glob` (find files), `ripgrep` (search content), `tree` (directory structure)
20
20
  - **Reading**: `read` (individual files), `ls` (directory listing)
21
- - **Editing**: `edit` (preferred - structured ops), `apply_patch` (alternative - unified diffs)
21
+ - **Editing**: `apply_patch` (recommended - targeted edits), `edit` (alternative - structured ops)
22
22
  - **Execution**: `bash` (run commands), `git_*` tools (version control)
23
23
  - **Planning**: `update_plan` (create and track task plans)
24
24
  - **Progress**: `progress_update` (inform user of current phase)
@@ -28,19 +28,28 @@ call with multiple ops. Each separate `edit` operation re-reads the file fresh.
28
28
 
29
29
  ## File Editing Best Practices
30
30
 
31
- **Using the `edit` Tool** (Recommended):
31
+ **Using the `apply_patch` Tool** (Recommended):
32
+ - Primary choice for targeted file edits - avoids rewriting entire files
33
+ - Only requires the specific lines you want to change
34
+ - Format: `*** Begin Patch` ... `*** Update File: path` ... `-old` / `+new` ... `*** End Patch`
35
+ - Only use when you have a complete unified diff ready
36
+ - Ensure patch is based on current file content (not stale)
37
+ - If patch fails, read the file first to see current state before retrying
38
+
39
+ **Using the `edit` Tool** (Alternative):
32
40
  - Specify the file path and a list of operations
33
41
  - Operations are applied sequentially to the latest file state
34
42
  - Operation types: `replace`, `insert-before`, `insert-after`, `delete`
35
43
  - Each operation includes: `old` (text to match/find) and `new` (replacement/insertion)
36
44
  - When making multiple changes to a file, use ONE `edit` call with multiple ops
37
45
 
38
- **Using the `apply_patch` Tool** (Alternative):
39
- - Only use when you have a complete unified diff ready
40
- - Ensure patch is based on current file content (not stale)
41
- - If patch fails, read the file first to see current state before retrying
46
+ **Using the `write` Tool** (Last Resort):
47
+ - Only use when creating new files or completely replacing file content
48
+ - NEVER use for targeted edits - it rewrites the entire file
49
+ - Wastes output tokens and risks hallucinating unchanged parts
42
50
 
43
51
  **Never**:
52
+ - Use `write` for partial file edits (use `apply_patch` or `edit` instead)
44
53
  - Make multiple separate `edit` or `apply_patch` calls for the same file
45
54
  - Assume file content remains unchanged between operations
46
55
  - Use `bash` with `sed`/`awk` for programmatic file editing (use `edit` instead)
@@ -54,8 +54,9 @@ Before making tool calls, send a brief preamble to the user explaining what you'
54
54
  Your toolset includes specialized file editing and search tools. Follow these guidelines:
55
55
 
56
56
  **File Editing**:
57
- - Prefer `edit` tool with structured operations for most file modifications
58
- - Use `apply_patch` only when you have a complete unified diff ready
57
+ - Prefer `apply_patch` tool for targeted edits - avoids rewriting entire files
58
+ - Use `edit` tool as alternative when you need structured operations
59
+ - NEVER use `write` for partial edits - it rewrites the entire file and wastes tokens
59
60
  - When editing the same file multiple times, batch operations in a single `edit` call
60
61
  - Each separate `edit` operation re-reads the file, so ops within one call are sequential
61
62
 
@@ -71,19 +72,28 @@ Your toolset includes specialized file editing and search tools. Follow these gu
71
72
 
72
73
  ## File Editing Best Practices
73
74
 
74
- **Using the `edit` Tool** (Recommended):
75
+ **Using the `apply_patch` Tool** (Recommended):
76
+ - Primary choice for targeted file edits - avoids rewriting entire files
77
+ - Only requires the specific lines you want to change
78
+ - Format: `*** Begin Patch` ... `*** Update File: path` ... `-old` / `+new` ... `*** End Patch`
79
+ - Only use when you have a complete unified diff ready
80
+ - Ensure patch is based on current file content (not stale)
81
+ - If patch fails, read the file first to see current state before retrying
82
+
83
+ **Using the `edit` Tool** (Alternative):
75
84
  - Specify the file path and a list of operations
76
85
  - Operations are applied sequentially to the latest file state
77
86
  - Operation types: `replace`, `insert-before`, `insert-after`, `delete`
78
87
  - Each operation includes: `old` (text to match/find) and `new` (replacement/insertion)
79
88
  - When making multiple changes to a file, use ONE `edit` call with multiple ops
80
89
 
81
- **Using the `apply_patch` Tool** (Alternative):
82
- - Only use when you have a complete unified diff ready
83
- - Ensure patch is based on current file content (not stale)
84
- - If patch fails, read the file first to see current state before retrying
90
+ **Using the `write` Tool** (Last Resort):
91
+ - Only use when creating new files or completely replacing file content
92
+ - NEVER use for targeted edits - it rewrites the entire file
93
+ - Wastes output tokens and risks hallucinating unchanged parts
85
94
 
86
95
  **Never**:
96
+ - Use `write` for partial file edits (use `apply_patch` or `edit` instead)
87
97
  - Make multiple separate `edit` or `apply_patch` calls for the same file
88
98
  - Assume file content remains unchanged between operations
89
99
  - Use `bash` with `sed`/`awk` for programmatic file editing (use `edit` instead)