pi-agent-flow 2.0.0 → 2.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 (195) hide show
  1. package/README.md +126 -489
  2. package/agents/audit.md +7 -2
  3. package/agents/build.md +6 -2
  4. package/agents/craft.md +6 -3
  5. package/agents/debug.md +7 -3
  6. package/agents/ideas.md +8 -4
  7. package/agents/scout.md +8 -1
  8. package/dist/batch/apply-patch.d.ts +60 -0
  9. package/dist/batch/apply-patch.d.ts.map +1 -0
  10. package/dist/batch/apply-patch.js +477 -0
  11. package/dist/batch/apply-patch.js.map +1 -0
  12. package/dist/batch/batch-bash.d.ts +0 -6
  13. package/dist/batch/batch-bash.d.ts.map +1 -1
  14. package/dist/batch/batch-bash.js +52 -14
  15. package/dist/batch/batch-bash.js.map +1 -1
  16. package/dist/batch/constants.d.ts +45 -4
  17. package/dist/batch/constants.d.ts.map +1 -1
  18. package/dist/batch/constants.js +72 -4
  19. package/dist/batch/constants.js.map +1 -1
  20. package/dist/batch/execute.d.ts +8 -2
  21. package/dist/batch/execute.d.ts.map +1 -1
  22. package/dist/batch/execute.js +338 -70
  23. package/dist/batch/execute.js.map +1 -1
  24. package/dist/batch/fuzzy-edit.d.ts +4 -1
  25. package/dist/batch/fuzzy-edit.d.ts.map +1 -1
  26. package/dist/batch/fuzzy-edit.js +7 -2
  27. package/dist/batch/fuzzy-edit.js.map +1 -1
  28. package/dist/batch/index.d.ts +3 -15
  29. package/dist/batch/index.d.ts.map +1 -1
  30. package/dist/batch/index.js +48 -78
  31. package/dist/batch/index.js.map +1 -1
  32. package/dist/batch/render.d.ts.map +1 -1
  33. package/dist/batch/render.js +30 -7
  34. package/dist/batch/render.js.map +1 -1
  35. package/dist/batch/shell-compress.d.ts +25 -0
  36. package/dist/batch/shell-compress.d.ts.map +1 -0
  37. package/dist/batch/shell-compress.js +602 -0
  38. package/dist/batch/shell-compress.js.map +1 -0
  39. package/dist/batch/summary.d.ts.map +1 -1
  40. package/dist/batch/summary.js +4 -0
  41. package/dist/batch/summary.js.map +1 -1
  42. package/dist/batch/symbols.d.ts.map +1 -1
  43. package/dist/batch/symbols.js +12 -7
  44. package/dist/batch/symbols.js.map +1 -1
  45. package/dist/config/config.d.ts +5 -0
  46. package/dist/config/config.d.ts.map +1 -1
  47. package/dist/config/config.js +63 -0
  48. package/dist/config/config.js.map +1 -1
  49. package/dist/config/models.d.ts +2 -0
  50. package/dist/config/models.d.ts.map +1 -0
  51. package/dist/config/models.js +49 -0
  52. package/dist/config/models.js.map +1 -0
  53. package/dist/config/settings-resolver.js +2 -2
  54. package/dist/config/settings-resolver.js.map +1 -1
  55. package/dist/core/agents.js +2 -2
  56. package/dist/core/agents.js.map +1 -1
  57. package/dist/core/depth.d.ts +3 -3
  58. package/dist/core/depth.d.ts.map +1 -1
  59. package/dist/core/depth.js +5 -5
  60. package/dist/core/depth.js.map +1 -1
  61. package/dist/core/executor.d.ts +10 -3
  62. package/dist/core/executor.d.ts.map +1 -1
  63. package/dist/core/executor.js +7 -3
  64. package/dist/core/executor.js.map +1 -1
  65. package/dist/core/flow.d.ts +19 -3
  66. package/dist/core/flow.d.ts.map +1 -1
  67. package/dist/core/flow.js +97 -58
  68. package/dist/core/flow.js.map +1 -1
  69. package/dist/core/session-mode.d.ts +1 -1
  70. package/dist/core/session-mode.d.ts.map +1 -1
  71. package/dist/core/session-mode.js +2 -1
  72. package/dist/core/session-mode.js.map +1 -1
  73. package/dist/core/{delegation.d.ts → transition.d.ts} +9 -9
  74. package/dist/core/transition.d.ts.map +1 -0
  75. package/dist/core/{delegation.js → transition.js} +17 -24
  76. package/dist/core/transition.js.map +1 -0
  77. package/dist/flow/auto-warp.d.ts +12 -0
  78. package/dist/flow/auto-warp.d.ts.map +1 -0
  79. package/dist/flow/auto-warp.js +29 -0
  80. package/dist/flow/auto-warp.js.map +1 -0
  81. package/dist/flow/command.d.ts.map +1 -1
  82. package/dist/flow/command.js +7 -2
  83. package/dist/flow/command.js.map +1 -1
  84. package/dist/flow/continuation.d.ts.map +1 -1
  85. package/dist/flow/continuation.js +52 -15
  86. package/dist/flow/continuation.js.map +1 -1
  87. package/dist/flow/index.d.ts +5 -2
  88. package/dist/flow/index.d.ts.map +1 -1
  89. package/dist/flow/index.js +6 -3
  90. package/dist/flow/index.js.map +1 -1
  91. package/dist/flow/loop-command.d.ts +8 -0
  92. package/dist/flow/loop-command.d.ts.map +1 -0
  93. package/dist/flow/loop-command.js +102 -0
  94. package/dist/flow/loop-command.js.map +1 -0
  95. package/dist/flow/loop-templates.d.ts +7 -0
  96. package/dist/flow/loop-templates.d.ts.map +1 -0
  97. package/dist/flow/loop-templates.js +38 -0
  98. package/dist/flow/loop-templates.js.map +1 -0
  99. package/dist/flow/loop.d.ts +19 -0
  100. package/dist/flow/loop.d.ts.map +1 -0
  101. package/dist/flow/loop.js +95 -0
  102. package/dist/flow/loop.js.map +1 -0
  103. package/dist/flow/settings-command.d.ts.map +1 -1
  104. package/dist/flow/settings-command.js +93 -4
  105. package/dist/flow/settings-command.js.map +1 -1
  106. package/dist/flow/store.d.ts +3 -3
  107. package/dist/flow/store.d.ts.map +1 -1
  108. package/dist/flow/store.js +24 -16
  109. package/dist/flow/store.js.map +1 -1
  110. package/dist/flow/template-shared.d.ts +9 -0
  111. package/dist/flow/template-shared.d.ts.map +1 -0
  112. package/dist/flow/template-shared.js +13 -0
  113. package/dist/flow/template-shared.js.map +1 -0
  114. package/dist/flow/template-strings.d.ts.map +1 -1
  115. package/dist/flow/template-strings.js +2 -5
  116. package/dist/flow/template-strings.js.map +1 -1
  117. package/dist/flow/types.d.ts +15 -9
  118. package/dist/flow/types.d.ts.map +1 -1
  119. package/dist/flow/warp.d.ts +15 -0
  120. package/dist/flow/warp.d.ts.map +1 -0
  121. package/dist/flow/warp.js +207 -0
  122. package/dist/flow/warp.js.map +1 -0
  123. package/dist/index.d.ts +3 -2
  124. package/dist/index.d.ts.map +1 -1
  125. package/dist/index.js +185 -28
  126. package/dist/index.js.map +1 -1
  127. package/dist/snapshot/cli-args.d.ts +1 -0
  128. package/dist/snapshot/cli-args.d.ts.map +1 -1
  129. package/dist/snapshot/cli-args.js +12 -0
  130. package/dist/snapshot/cli-args.js.map +1 -1
  131. package/dist/snapshot/runner-events.d.ts +5 -0
  132. package/dist/snapshot/runner-events.d.ts.map +1 -1
  133. package/dist/snapshot/runner-events.js +89 -15
  134. package/dist/snapshot/runner-events.js.map +1 -1
  135. package/dist/snapshot/snapshot.d.ts +22 -19
  136. package/dist/snapshot/snapshot.d.ts.map +1 -1
  137. package/dist/snapshot/snapshot.js +1063 -167
  138. package/dist/snapshot/snapshot.js.map +1 -1
  139. package/dist/steering/flow-prompt.d.ts +2 -2
  140. package/dist/steering/flow-prompt.d.ts.map +1 -1
  141. package/dist/steering/flow-prompt.js +4 -4
  142. package/dist/steering/flow-prompt.js.map +1 -1
  143. package/dist/steering/sliding-prompt.d.ts.map +1 -1
  144. package/dist/steering/sliding-prompt.js +9 -6
  145. package/dist/steering/sliding-prompt.js.map +1 -1
  146. package/dist/steering/tool-utils.d.ts +31 -8
  147. package/dist/steering/tool-utils.d.ts.map +1 -1
  148. package/dist/steering/tool-utils.js +63 -30
  149. package/dist/steering/tool-utils.js.map +1 -1
  150. package/dist/tools/ask-user.d.ts +0 -17
  151. package/dist/tools/ask-user.d.ts.map +1 -1
  152. package/dist/tools/ask-user.js +13 -37
  153. package/dist/tools/ask-user.js.map +1 -1
  154. package/dist/tools/timed-bash.d.ts +1 -1
  155. package/dist/tools/timed-bash.d.ts.map +1 -1
  156. package/dist/tools/timed-bash.js +19 -8
  157. package/dist/tools/timed-bash.js.map +1 -1
  158. package/dist/tools/web-tool.d.ts.map +1 -1
  159. package/dist/tools/web-tool.js +11 -13
  160. package/dist/tools/web-tool.js.map +1 -1
  161. package/dist/tui/render-utils.d.ts +5 -1
  162. package/dist/tui/render-utils.d.ts.map +1 -1
  163. package/dist/tui/render-utils.js +36 -10
  164. package/dist/tui/render-utils.js.map +1 -1
  165. package/dist/tui/render.d.ts +9 -0
  166. package/dist/tui/render.d.ts.map +1 -1
  167. package/dist/tui/render.js +112 -100
  168. package/dist/tui/render.js.map +1 -1
  169. package/dist/tui/scramble/constants.d.ts +8 -8
  170. package/dist/tui/scramble/constants.d.ts.map +1 -1
  171. package/dist/tui/scramble/constants.js +7 -7
  172. package/dist/tui/scramble/constants.js.map +1 -1
  173. package/dist/tui/scramble/index.d.ts +1 -1
  174. package/dist/tui/scramble/index.d.ts.map +1 -1
  175. package/dist/tui/scramble/index.js +1 -1
  176. package/dist/tui/scramble/index.js.map +1 -1
  177. package/dist/tui/scramble/manager.d.ts +1 -5
  178. package/dist/tui/scramble/manager.d.ts.map +1 -1
  179. package/dist/tui/scramble/manager.js +16 -64
  180. package/dist/tui/scramble/manager.js.map +1 -1
  181. package/dist/tui/scramble/utils.js +1 -1
  182. package/dist/tui/scramble/utils.js.map +1 -1
  183. package/dist/types/flow.d.ts +2 -0
  184. package/dist/types/flow.d.ts.map +1 -1
  185. package/dist/types/flow.js +11 -2
  186. package/dist/types/flow.js.map +1 -1
  187. package/dist/types/output.d.ts +6 -0
  188. package/dist/types/output.d.ts.map +1 -1
  189. package/package.json +2 -2
  190. package/dist/core/delegation.d.ts.map +0 -1
  191. package/dist/core/delegation.js.map +0 -1
  192. package/dist/flow/warp-command.d.ts +0 -9
  193. package/dist/flow/warp-command.d.ts.map +0 -1
  194. package/dist/flow/warp-command.js +0 -405
  195. package/dist/flow/warp-command.js.map +0 -1
@@ -7,8 +7,10 @@
7
7
  import * as fs from "node:fs/promises";
8
8
  import * as path from "node:path";
9
9
  import { execFile } from "node:child_process";
10
- import { MAX_LINES, MAX_BYTES, SAFE_FULL_READ_LIMIT, TARGETED_READ_LINE_LIMIT, MAX_TOTAL_RESULT_LINES, BATCH_READ_MAX_TOTAL_BYTES, } from "./constants.js";
10
+ import { withFileMutationQueue } from "@mariozechner/pi-coding-agent";
11
+ import { MAX_LINES, MAX_BYTES, SAFE_FULL_READ_LIMIT, TARGETED_READ_LINE_LIMIT, MAX_TOTAL_RESULT_LINES, BATCH_READ_MAX_TOTAL_BYTES, RG_SIGNATURES_MAX_FILES, } from "./constants.js";
11
12
  import { normalizeToLF, restoreLineEndings, detectLineEnding, stripBom, applyEdits, levenshtein, validatePath, } from "./fuzzy-edit.js";
13
+ import { applyPatch } from "./apply-patch.js";
12
14
  import { buildFileContextMap } from "./symbols.js";
13
15
  // ---------------------------------------------------------------------------
14
16
  // Read helpers
@@ -123,9 +125,6 @@ export async function suggestSimilarFiles(inputPath, cwd) {
123
125
  return [];
124
126
  }
125
127
  }
126
- // ---------------------------------------------------------------------------
127
- // Error hints
128
- // ---------------------------------------------------------------------------
129
128
  function getErrorHint(error) {
130
129
  if (error.includes("File not found") || error.includes("file not found"))
131
130
  return "Verify the path exists.";
@@ -143,15 +142,27 @@ function getErrorHint(error) {
143
142
  return "Verify the path exists.";
144
143
  if (error.includes("is beyond end of file"))
145
144
  return "Use a smaller offset within the file length.";
145
+ if (error.includes("ripgrep failed"))
146
+ return "Ripgrep crashed or was killed. Try narrowing the search path or adding max-count to limit output.";
146
147
  return "";
147
148
  }
149
+ function isRetryable(error) {
150
+ const transient = [
151
+ "File not found",
152
+ "file not found",
153
+ "ENOENT",
154
+ "no such file",
155
+ "Could not find",
156
+ "occurrences",
157
+ ];
158
+ return transient.some((p) => error.includes(p));
159
+ }
148
160
  // ---------------------------------------------------------------------------
149
161
  // Main execute function
150
162
  // ---------------------------------------------------------------------------
151
- export async function executeOperations(operations, cwd, signal, options = {}) {
163
+ export async function executeOperations(operations, cwd, signal, options = {}, onUpdate) {
152
164
  const results = [];
153
- let failed = false;
154
- const counts = { read: 0, write: 0, edit: 0, delete: 0, rg: 0, error: 0, skipped: 0 };
165
+ const counts = { read: 0, write: 0, edit: 0, delete: 0, rg: 0, patch: 0, bash: 0, error: 0, skipped: 0 };
155
166
  const errors = [];
156
167
  const truncatedFiles = [];
157
168
  const aggregateLimitSkipped = [];
@@ -159,38 +170,72 @@ export async function executeOperations(operations, cwd, signal, options = {}) {
159
170
  let aggregateLinesRead = 0;
160
171
  let aggregateBytesRead = 0;
161
172
  const includeLimitWarnings = options.includeLimitWarnings ?? true;
162
- for (const op of operations) {
173
+ let lastUpdateTime = 0;
174
+ let finalUpdateEmitted = false;
175
+ function emitPartialUpdate() {
176
+ if (!onUpdate)
177
+ return;
178
+ const now = Date.now();
179
+ const isFinal = results.length === operations.length;
180
+ if (!isFinal && now - lastUpdateTime < 100)
181
+ return;
182
+ if (isFinal && finalUpdateEmitted)
183
+ return;
184
+ finalUpdateEmitted = isFinal;
185
+ lastUpdateTime = now;
186
+ const partialSummary = buildSummary(counts, errors, truncatedFiles, aggregateLimitSkipped, aggregateByteLimitSkipped);
187
+ const partialContentText = buildContentText(partialSummary, results);
188
+ onUpdate({
189
+ content: [{ type: "text", text: partialContentText }],
190
+ details: { results: [...results] },
191
+ });
192
+ }
193
+ for (let i = 0; i < operations.length; i++) {
194
+ const op = operations[i];
163
195
  if (signal?.aborted) {
164
- results.push({ op: op.o, path: op.p, status: "skipped", error: "Operation aborted." });
165
- counts.skipped++;
166
- continue;
167
- }
168
- if (failed) {
169
- results.push({ op: op.o, path: op.p, status: "skipped" });
170
- counts.skipped++;
171
- continue;
196
+ for (let j = i; j < operations.length; j++) {
197
+ const r = operations[j];
198
+ results.push({ op: r.o, path: r.p, status: "skipped", error: "Operation aborted.", s: r.s, l: r.l, q: r.q });
199
+ counts.skipped++;
200
+ }
201
+ emitPartialUpdate();
202
+ break;
172
203
  }
173
204
  try {
174
- const resolvedPath = await validatePath(op.p, cwd);
205
+ const { path: resolvedPath, warning: pathWarning } = await validatePath(op.p, cwd);
175
206
  switch (op.o) {
176
207
  case "read": {
177
208
  if (aggregateLinesRead >= MAX_TOTAL_RESULT_LINES) {
209
+ const remainingOps = operations.length - i - 1;
178
210
  results.push({
179
211
  op: "read",
180
212
  path: op.p,
181
213
  status: "skipped",
182
- error: `Skipped: aggregate line limit of ${MAX_TOTAL_RESULT_LINES} already reached. Use separate batch/batch_read calls.`,
214
+ skipped: true,
215
+ reason: "aggregate_line_limit",
216
+ consumed: { lines: aggregateLinesRead, bytes: aggregateBytesRead },
217
+ remainingOps,
218
+ error: `Skipped: aggregate line limit of ${MAX_TOTAL_RESULT_LINES} reached (${aggregateLinesRead} lines consumed). ${remainingOps} remaining operation(s) will still execute. Use separate batch/batch_read calls.`,
219
+ s: op.s,
220
+ l: op.l,
183
221
  });
184
222
  counts.skipped++;
185
223
  aggregateLimitSkipped.push({ path: op.p });
186
224
  break;
187
225
  }
188
226
  if (aggregateBytesRead >= BATCH_READ_MAX_TOTAL_BYTES) {
227
+ const remainingOps = operations.length - i - 1;
189
228
  results.push({
190
229
  op: "read",
191
230
  path: op.p,
192
231
  status: "skipped",
193
- error: `Skipped: aggregate byte limit of ${BATCH_READ_MAX_TOTAL_BYTES} already reached. Use separate batch/batch_read calls.`,
232
+ skipped: true,
233
+ reason: "aggregate_byte_limit",
234
+ consumed: { lines: aggregateLinesRead, bytes: aggregateBytesRead },
235
+ remainingOps,
236
+ error: `Skipped: aggregate byte limit of ${BATCH_READ_MAX_TOTAL_BYTES} reached (${aggregateBytesRead} bytes consumed). ${remainingOps} remaining operation(s) will still execute. Use separate batch/batch_read calls.`,
237
+ s: op.s,
238
+ l: op.l,
194
239
  });
195
240
  counts.skipped++;
196
241
  aggregateByteLimitSkipped.push({ path: op.p });
@@ -260,9 +305,11 @@ export async function executeOperations(operations, cwd, signal, options = {}) {
260
305
  status: "ok",
261
306
  content: finalContent,
262
307
  totalLines: totalFileLines,
263
- warning: safetyWarning,
308
+ warning: [pathWarning, safetyWarning].filter(Boolean).join("\n") || undefined,
264
309
  truncated: finalTruncated || undefined,
265
310
  nextOffset,
311
+ s: op.s,
312
+ l: op.l,
266
313
  });
267
314
  counts.read++;
268
315
  break;
@@ -271,13 +318,16 @@ export async function executeOperations(operations, cwd, signal, options = {}) {
271
318
  if (!op.c && op.c !== "") {
272
319
  throw new Error("c (content) is required for write operations.");
273
320
  }
274
- await fs.mkdir(path.dirname(resolvedPath), { recursive: true });
275
- await fs.writeFile(resolvedPath, op.c, "utf-8");
321
+ await withFileMutationQueue(resolvedPath, async () => {
322
+ await fs.mkdir(path.dirname(resolvedPath), { recursive: true });
323
+ await fs.writeFile(resolvedPath, op.c, "utf-8");
324
+ });
276
325
  results.push({
277
326
  op: "write",
278
327
  path: op.p,
279
328
  status: "ok",
280
329
  bytes: Buffer.byteLength(op.c, "utf-8"),
330
+ warning: pathWarning,
281
331
  });
282
332
  counts.write++;
283
333
  break;
@@ -286,38 +336,45 @@ export async function executeOperations(operations, cwd, signal, options = {}) {
286
336
  if (!op.e || op.e.length === 0) {
287
337
  throw new Error("e (edits) array is required for edit operations.");
288
338
  }
289
- const rawContent = await fs.readFile(resolvedPath, "utf-8");
290
- const { bom, text: contentWithoutBom } = stripBom(rawContent);
291
- const originalEnding = detectLineEnding(contentWithoutBom);
292
- const normalizedContent = normalizeToLF(contentWithoutBom);
293
- const { newContent, blocksChanged } = applyEdits(normalizedContent, op.e, op.p);
294
- const finalContent = bom + restoreLineEndings(newContent, originalEnding);
295
- await fs.writeFile(resolvedPath, finalContent, "utf-8");
339
+ const edits = op.e;
340
+ const blocksChanged = await withFileMutationQueue(resolvedPath, async () => {
341
+ const rawContent = await fs.readFile(resolvedPath, "utf-8");
342
+ const { bom, text: contentWithoutBom } = stripBom(rawContent);
343
+ const originalEnding = detectLineEnding(contentWithoutBom);
344
+ const normalizedContent = normalizeToLF(contentWithoutBom);
345
+ const { newContent, blocksChanged: changed } = applyEdits(normalizedContent, edits, op.p);
346
+ const finalContent = bom + restoreLineEndings(newContent, originalEnding);
347
+ await fs.writeFile(resolvedPath, finalContent, "utf-8");
348
+ return changed;
349
+ });
296
350
  results.push({
297
351
  op: "edit",
298
352
  path: op.p,
299
353
  status: "ok",
300
354
  blocksChanged,
355
+ warning: pathWarning,
301
356
  });
302
357
  counts.edit++;
303
358
  break;
304
359
  }
305
360
  case "delete": {
306
- let stat;
307
- try {
308
- stat = await fs.lstat(resolvedPath);
309
- }
310
- catch (err) {
311
- if (err.code === "ENOENT") {
312
- throw new Error(`File not found: ${op.p}`);
361
+ await withFileMutationQueue(resolvedPath, async () => {
362
+ let stat;
363
+ try {
364
+ stat = await fs.lstat(resolvedPath);
313
365
  }
314
- throw err;
315
- }
316
- if (stat.isDirectory()) {
317
- throw new Error(`Cannot delete directory: ${op.p}. Use a recursive removal tool or delete files individually.`);
318
- }
319
- await fs.unlink(resolvedPath);
320
- results.push({ op: "delete", path: op.p, status: "ok" });
366
+ catch (err) {
367
+ if (err.code === "ENOENT") {
368
+ throw new Error(`File not found: ${op.p}`);
369
+ }
370
+ throw err;
371
+ }
372
+ if (stat.isDirectory()) {
373
+ throw new Error(`Cannot delete directory: ${op.p}. Use a recursive removal tool or delete files individually.`);
374
+ }
375
+ await fs.unlink(resolvedPath);
376
+ });
377
+ results.push({ op: "delete", path: op.p, status: "ok", warning: pathWarning });
321
378
  counts.delete++;
322
379
  break;
323
380
  }
@@ -328,26 +385,61 @@ export async function executeOperations(operations, cwd, signal, options = {}) {
328
385
  }
329
386
  const searchPath = (rgOp.p.startsWith("~") || path.isAbsolute(rgOp.p)) ? resolvedPath : rgOp.p;
330
387
  const args = buildRgArgs({ ...rgOp, p: searchPath });
331
- const matches = await execRg(args, cwd);
388
+ const matches = await execRg(args, cwd, signal);
332
389
  const content = matches.join("\n");
390
+ // Try to attach enclosing signatures (only when we have line numbers)
391
+ let enclosingSignatures;
392
+ const uniqueFiles = extractUniqueFilesFromRg(matches);
393
+ if (uniqueFiles.size > 0 && uniqueFiles.size <= RG_SIGNATURES_MAX_FILES && !isFilesOnlyRg(matches)) {
394
+ enclosingSignatures = await buildEnclosingSignatures(uniqueFiles, matches, cwd);
395
+ }
333
396
  results.push({
334
397
  op: "rg",
335
398
  path: rgOp.p,
336
399
  status: "ok",
337
400
  content,
338
401
  totalLines: matches.length,
402
+ enclosingSignatures,
403
+ warning: pathWarning,
404
+ q: rgOp.q,
339
405
  });
340
406
  counts.rg++;
341
407
  break;
342
408
  }
409
+ case "patch": {
410
+ if (!op.c && op.c !== "") {
411
+ throw new Error("c (patch text) is required for patch operations.");
412
+ }
413
+ const { affected, exact } = await applyPatch(op.c, cwd);
414
+ results.push({
415
+ op: "patch",
416
+ path: op.p,
417
+ status: "ok",
418
+ affected,
419
+ exact,
420
+ warning: pathWarning,
421
+ });
422
+ counts.patch++;
423
+ break;
424
+ }
343
425
  default:
344
426
  throw new Error(`Unknown operation type: ${op.o}`);
345
427
  }
346
428
  }
347
429
  catch (err) {
348
- failed = true;
349
- counts.error++;
350
430
  const message = err instanceof Error ? err.message : String(err);
431
+ // Treat mid-flight rg abort as skipped rather than error
432
+ if (message === "Aborted" && signal?.aborted) {
433
+ counts.skipped++;
434
+ results.push({
435
+ op: op.o,
436
+ path: op.p,
437
+ status: "skipped",
438
+ error: "Operation aborted.",
439
+ });
440
+ continue;
441
+ }
442
+ counts.error++;
351
443
  // Enrich file-not-found errors with fuzzy filename suggestions
352
444
  let hint = getErrorHint(message);
353
445
  if (message.includes("File not found") ||
@@ -359,6 +451,8 @@ export async function executeOperations(operations, cwd, signal, options = {}) {
359
451
  hint += ` Did you mean: ${suggestions.join(", ")}?`;
360
452
  }
361
453
  }
454
+ const retryable = isRetryable(message);
455
+ const suggestedFix = hint || undefined;
362
456
  errors.push({ path: op.p, op: op.o, message, hint });
363
457
  results.push({
364
458
  op: op.o,
@@ -366,9 +460,16 @@ export async function executeOperations(operations, cwd, signal, options = {}) {
366
460
  status: "error",
367
461
  error: message,
368
462
  hint,
463
+ retryable,
464
+ suggestedFix,
465
+ s: op.s,
466
+ l: op.l,
467
+ q: op.q,
369
468
  });
370
469
  }
470
+ emitPartialUpdate();
371
471
  }
472
+ emitPartialUpdate();
372
473
  // Build the enhanced summary and content text
373
474
  const summary = buildSummary(counts, errors, truncatedFiles, aggregateLimitSkipped, aggregateByteLimitSkipped);
374
475
  const contentText = buildContentText(summary, results);
@@ -378,36 +479,70 @@ export async function executeOperations(operations, cwd, signal, options = {}) {
378
479
  // Summary / content rendering
379
480
  // ---------------------------------------------------------------------------
380
481
  function buildSummary(counts, errors, truncatedFiles, aggregateLimitSkipped = [], aggregateByteLimitSkipped = []) {
381
- const totalSuccess = counts.read + counts.write + counts.edit + counts.delete + counts.rg;
382
- const totalOps = totalSuccess + counts.error + counts.skipped;
383
482
  const parts = [];
384
- // Build the success breakdown
483
+ // Build success parts from counts
385
484
  const successParts = [];
386
485
  if (counts.read > 0)
387
- successParts.push(`${counts.read} read${counts.read > 1 ? "s" : ""}`);
486
+ successParts.push(`${counts.read} read`);
388
487
  if (counts.write > 0)
389
- successParts.push(`${counts.write} write${counts.write > 1 ? "s" : ""}`);
488
+ successParts.push(`${counts.write} write`);
390
489
  if (counts.edit > 0)
391
- successParts.push(`${counts.edit} edit${counts.edit > 1 ? "s" : ""}`);
490
+ successParts.push(`${counts.edit} edit`);
392
491
  if (counts.delete > 0)
393
- successParts.push(`${counts.delete} delete${counts.delete > 1 ? "s" : ""}`);
492
+ successParts.push(`${counts.delete} delete`);
394
493
  if (counts.rg > 0)
395
- successParts.push(`${counts.rg} rg${counts.rg > 1 ? "s" : ""}`);
396
- if (counts.error === 0) {
397
- // All success
398
- parts.push(`${totalOps} operations: ${successParts.join(", ")}`);
494
+ successParts.push(`${counts.rg} rg`);
495
+ if (counts.patch > 0)
496
+ successParts.push(`${counts.patch} patch`);
497
+ if (counts.bash > 0)
498
+ successParts.push(`${counts.bash} bash`);
499
+ // Build failure parts from errors
500
+ const failedCounts = {};
501
+ for (const err of errors) {
502
+ failedCounts[err.op] = (failedCounts[err.op] || 0) + 1;
503
+ }
504
+ const failedParts = [];
505
+ if (failedCounts.read > 0)
506
+ failedParts.push(`${failedCounts.read} read`);
507
+ if (failedCounts.write > 0)
508
+ failedParts.push(`${failedCounts.write} write`);
509
+ if (failedCounts.edit > 0)
510
+ failedParts.push(`${failedCounts.edit} edit`);
511
+ if (failedCounts.delete > 0)
512
+ failedParts.push(`${failedCounts.delete} delete`);
513
+ if (failedCounts.rg > 0)
514
+ failedParts.push(`${failedCounts.rg} rg`);
515
+ if (failedCounts.patch > 0)
516
+ failedParts.push(`${failedCounts.patch} patch`);
517
+ if (failedCounts.bash > 0)
518
+ failedParts.push(`${failedCounts.bash} bash`);
519
+ const hasSuccess = successParts.length > 0;
520
+ const hasFailure = failedParts.length > 0;
521
+ const hasSkipped = counts.skipped > 0;
522
+ if (!hasFailure) {
523
+ // All success (or skipped)
524
+ const summaryParts = [...successParts];
525
+ if (hasSkipped)
526
+ summaryParts.push(`${counts.skipped} skipped`);
527
+ parts.push(`✔ ${summaryParts.join(", ")}`);
399
528
  }
400
529
  else {
401
- // Mixed success/failure
402
- parts.push(`${counts.error} failed${counts.skipped > 0 ? `, ${counts.skipped} skipped` : ""}`);
403
- if (totalSuccess > 0) {
404
- parts.push(` ${successParts.join(", ")} ok`);
530
+ // Mixed or all failed
531
+ if (hasSuccess) {
532
+ parts.push(`✔ ${successParts.join(", ")} | ✗ ${failedParts.join(", ")}`);
405
533
  }
406
- for (const err of errors) {
407
- const hint = err.hint ?? "";
408
- const hintSuffix = hint ? ` — ${hint}` : "";
409
- parts.push(` ${err.op} ${err.path}: ${err.message}${hintSuffix}`);
534
+ else {
535
+ parts.push(`✗ ${failedParts.join(", ")}`);
410
536
  }
537
+ if (hasSkipped) {
538
+ parts.push(`${counts.skipped} skipped`);
539
+ }
540
+ }
541
+ // Error details
542
+ for (const err of errors) {
543
+ const hint = err.hint ?? "";
544
+ const hintSuffix = hint ? ` — ${hint}` : "";
545
+ parts.push(` ${err.op} ${err.path}: ${err.message}${hintSuffix}`);
411
546
  }
412
547
  // Truncation warnings
413
548
  for (const tf of truncatedFiles) {
@@ -472,11 +607,30 @@ function buildContentText(summary, results) {
472
607
  sections.push(`\n--- delete: ${r.path} ---`);
473
608
  }
474
609
  else if (r.op === "rg" && r.status === "ok") {
475
- sections.push(`\n--- rg: ${r.path} ---\n${r.content}`);
610
+ if (r.enclosingSignatures && Object.keys(r.enclosingSignatures).length > 0) {
611
+ const grouped = groupRgMatchesByFile(r.content ?? "", r.enclosingSignatures);
612
+ sections.push(`\n--- rg: ${r.path} ---\n${grouped}`);
613
+ }
614
+ else {
615
+ sections.push(`\n--- rg: ${r.path} ---\n${r.content}`);
616
+ }
476
617
  }
477
618
  else if (r.status === "error") {
478
619
  sections.push(`\n--- ${r.op}: ${r.path} ---\nError: ${r.error}`);
479
620
  }
621
+ else if (r.op === "patch" && r.status === "ok") {
622
+ const parts = [];
623
+ if (r.affected?.added.length)
624
+ parts.push(`A ${r.affected.added.join(', ')}`);
625
+ if (r.affected?.modified.length)
626
+ parts.push(`M ${r.affected.modified.join(', ')}`);
627
+ if (r.affected?.deleted.length)
628
+ parts.push(`D ${r.affected.deleted.join(', ')}`);
629
+ sections.push(`\n--- patch: ${r.path} ---\n${parts.join('\n')}`);
630
+ }
631
+ else if (r.status === "skipped") {
632
+ sections.push(`\n--- ${r.op}: ${r.path} ---\n${r.error ?? "Skipped"}`);
633
+ }
480
634
  }
481
635
  return sections.join("");
482
636
  }
@@ -485,6 +639,7 @@ function buildContentText(summary, results) {
485
639
  // ---------------------------------------------------------------------------
486
640
  function buildRgArgs(op) {
487
641
  const args = [];
642
+ args.push("-n");
488
643
  if (op.l === true || op.l === undefined)
489
644
  args.push("-l");
490
645
  if (op.i === true)
@@ -502,16 +657,111 @@ function buildRgArgs(op) {
502
657
  args.push(op.p);
503
658
  return args;
504
659
  }
505
- function execRg(args, cwd) {
660
+ function isFilesOnlyRg(matches) {
661
+ // When rg runs with -l, matches are just filenames with no line numbers
662
+ return matches.length > 0 && !matches.some((m) => m.includes(":"));
663
+ }
664
+ function extractUniqueFilesFromRg(matches) {
665
+ const files = new Set();
666
+ for (const m of matches) {
667
+ const colonIdx = m.indexOf(":");
668
+ if (colonIdx > 0)
669
+ files.add(m.substring(0, colonIdx));
670
+ }
671
+ return files;
672
+ }
673
+ function parseRgLineNumber(match) {
674
+ // format: path:line:content
675
+ const parts = match.split(":");
676
+ if (parts.length < 3)
677
+ return null;
678
+ const lineNum = parseInt(parts[1], 10);
679
+ return Number.isFinite(lineNum) ? lineNum : null;
680
+ }
681
+ function parseRgFilePath(match) {
682
+ const colonIdx = match.indexOf(":");
683
+ return colonIdx > 0 ? match.substring(0, colonIdx) : null;
684
+ }
685
+ async function buildEnclosingSignatures(files, matches, cwd) {
686
+ const sigMap = {};
687
+ for (const filePath of files) {
688
+ try {
689
+ const abs = path.isAbsolute(filePath) ? filePath : path.join(cwd, filePath);
690
+ const raw = await fs.readFile(abs, "utf-8");
691
+ const lines = raw.split("\n");
692
+ const ctxMap = buildFileContextMap(filePath, lines);
693
+ if (!ctxMap.symbols || ctxMap.symbols.length === 0)
694
+ continue;
695
+ // For each match line in this file, find enclosing symbol
696
+ for (const match of matches) {
697
+ const lineNum = parseRgLineNumber(match);
698
+ if (lineNum === null)
699
+ continue;
700
+ const matchPath = parseRgFilePath(match);
701
+ if (matchPath !== filePath)
702
+ continue;
703
+ const enclosing = ctxMap.symbols
704
+ .filter((s) => lineNum >= s.startLine && lineNum <= s.endLine)
705
+ .sort((a, b) => (a.endLine - a.startLine) - (b.endLine - b.startLine))[0];
706
+ if (enclosing?.signature) {
707
+ sigMap[match] = enclosing.signature;
708
+ }
709
+ }
710
+ }
711
+ catch {
712
+ // File not readable, skip
713
+ }
714
+ }
715
+ return sigMap;
716
+ }
717
+ function groupRgMatchesByFile(content, sigMap) {
718
+ // Group matches by file, deduplicate signatures per file
719
+ const fileGroups = new Map();
720
+ for (const match of content.split("\n").filter(Boolean)) {
721
+ const filePath = parseRgFilePath(match);
722
+ if (!filePath) {
723
+ // Fallback: keep bare match as-is
724
+ const fallbackKey = "";
725
+ const group = fileGroups.get(fallbackKey) ?? { sigs: new Set(), lines: [] };
726
+ group.lines.push(match);
727
+ fileGroups.set(fallbackKey, group);
728
+ continue;
729
+ }
730
+ const group = fileGroups.get(filePath) ?? { sigs: new Set(), lines: [] };
731
+ const sig = sigMap[match];
732
+ if (sig)
733
+ group.sigs.add(sig);
734
+ group.lines.push(match);
735
+ fileGroups.set(filePath, group);
736
+ }
737
+ const out = [];
738
+ for (const [filePath, { sigs, lines }] of fileGroups) {
739
+ if (!filePath) {
740
+ out.push(...lines);
741
+ continue;
742
+ }
743
+ out.push(filePath);
744
+ for (const sig of sigs) {
745
+ out.push(` ${sig}`);
746
+ }
747
+ for (const line of lines) {
748
+ const colonIdx = line.indexOf(":");
749
+ const afterPath = colonIdx > 0 ? line.substring(colonIdx + 1) : line;
750
+ out.push(` → ${afterPath}`);
751
+ }
752
+ }
753
+ return out.join("\n");
754
+ }
755
+ function execRg(args, cwd, signal) {
506
756
  return new Promise((resolve, reject) => {
507
- execFile("rg", args, { cwd, maxBuffer: 10 * 1024 * 1024 }, (err, stdout, stderr) => {
757
+ const child = execFile("rg", args, { cwd, maxBuffer: 10 * 1024 * 1024 }, (err, stdout, stderr) => {
508
758
  if (err) {
509
759
  // ripgrep exits with code 1 when no matches are found
510
760
  if (err.code === 1) {
511
761
  resolve([]);
512
762
  return;
513
763
  }
514
- if (err.code === "ENOBUFS") {
764
+ if (err.code === "ENOBUFS" || err.code === "ERR_CHILD_PROCESS_STDIO_MAXBUFFER") {
515
765
  reject(new Error("ripgrep output exceeded 10MB buffer limit. Use a more specific pattern or add max-count."));
516
766
  return;
517
767
  }
@@ -520,12 +770,30 @@ function execRg(args, cwd) {
520
770
  return;
521
771
  }
522
772
  const stderrMsg = stderr?.trim() ? ` — ${stderr.trim()}` : "";
523
- reject(new Error(`ripgrep failed${stderrMsg}`));
773
+ const codeInfo = err.code ? ` (code: ${err.code})` : "";
774
+ const msgInfo = err.message ? `: ${err.message}` : "";
775
+ reject(new Error(`ripgrep failed${codeInfo}${msgInfo}${stderrMsg}`));
524
776
  return;
525
777
  }
526
778
  const lines = stdout.split("\n").filter((line) => line.length > 0);
527
779
  resolve(lines);
528
780
  });
781
+ if (signal) {
782
+ const onAbort = () => {
783
+ try {
784
+ child.kill("SIGTERM");
785
+ }
786
+ catch { /* already dead */ }
787
+ reject(new Error("Aborted"));
788
+ };
789
+ if (signal.aborted) {
790
+ onAbort();
791
+ }
792
+ else {
793
+ signal.addEventListener("abort", onAbort, { once: true });
794
+ child.on("close", () => signal.removeEventListener("abort", onAbort));
795
+ }
796
+ }
529
797
  });
530
798
  }
531
799
  //# sourceMappingURL=execute.js.map