@shanepadgett/tau-agent 0.33.1 → 0.34.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.
Files changed (190) hide show
  1. package/docs/context.md +29 -20
  2. package/docs/extending-tau-agent.md +63 -24
  3. package/extensions/appshot/index.ts +112 -111
  4. package/extensions/aside/README.md +7 -0
  5. package/extensions/aside/index.ts +168 -0
  6. package/extensions/aside/panel.ts +83 -0
  7. package/extensions/branch/index.ts +38 -21
  8. package/extensions/cache-diagnostics/index.ts +70 -58
  9. package/extensions/checkpoint/README.md +9 -0
  10. package/extensions/checkpoint/checkpoint-budget.ts +79 -0
  11. package/extensions/checkpoint/checkpoint.ts +277 -0
  12. package/extensions/checkpoint/index.ts +102 -0
  13. package/extensions/checkpoint/messages.ts +169 -0
  14. package/extensions/checkpoint/prompt.ts +24 -0
  15. package/extensions/checkpoint/settings.ts +27 -0
  16. package/extensions/commit/commit-effort.ts +3 -3
  17. package/extensions/context/README.md +12 -8
  18. package/extensions/context/definitions.ts +178 -55
  19. package/extensions/context/index.ts +192 -160
  20. package/extensions/context/panel.ts +57 -37
  21. package/extensions/context/sync.ts +139 -89
  22. package/extensions/context/validation.ts +37 -25
  23. package/extensions/effort/README.md +1 -1
  24. package/extensions/effort/index.ts +3 -3
  25. package/extensions/effort/state.ts +3 -18
  26. package/extensions/explore/guidance.ts +6 -47
  27. package/extensions/explore/index.ts +38 -53
  28. package/extensions/explore/{ast/read → read}/hook.ts +12 -7
  29. package/extensions/explore/{ast/tools → tools}/ast-search.ts +15 -19
  30. package/extensions/explore/{ast/tools → tools}/context.ts +16 -23
  31. package/extensions/explore/{ast/tools → tools}/deps.ts +11 -15
  32. package/extensions/explore/{ast/tools → tools}/discover.ts +15 -17
  33. package/extensions/explore/{ast/tools → tools}/impact.ts +20 -23
  34. package/extensions/explore/{ast/tools → tools}/outline.ts +21 -24
  35. package/extensions/explore/{ast/tools → tools}/relationships.ts +23 -32
  36. package/extensions/explore/{ast/tools → tools}/render.ts +1 -1
  37. package/extensions/explore/{ast/tools → tools}/reverse-deps.ts +11 -15
  38. package/extensions/explore/{ast/tools → tools}/show.ts +16 -19
  39. package/extensions/explore/tools/target-params.ts +9 -0
  40. package/extensions/footer/index.ts +75 -56
  41. package/extensions/handoff/index.ts +8 -30
  42. package/extensions/image-gen/index.ts +46 -40
  43. package/extensions/patch/executor.ts +190 -135
  44. package/extensions/patch/matcher.ts +82 -48
  45. package/extensions/patch/parser.ts +149 -105
  46. package/extensions/patch/render.ts +32 -39
  47. package/extensions/patch/summary.ts +30 -26
  48. package/extensions/ready/README.md +7 -0
  49. package/extensions/ready/index.ts +58 -0
  50. package/extensions/ready/model.ts +128 -0
  51. package/extensions/ready/packs.ts +272 -0
  52. package/extensions/ready/render.ts +445 -0
  53. package/extensions/ready/scan.ts +760 -0
  54. package/extensions/reference/panel.ts +28 -16
  55. package/extensions/review/index.ts +63 -51
  56. package/extensions/review/panel.ts +5 -34
  57. package/extensions/runtime-context/context.ts +30 -26
  58. package/extensions/script-runner/README.md +2 -2
  59. package/extensions/script-runner/index.ts +122 -82
  60. package/extensions/silent-command-runner/index.ts +38 -23
  61. package/extensions/subagent/agents/context-sync.md +185 -33
  62. package/extensions/subagent/agents/scout.md +2 -2
  63. package/extensions/subagent/agents.ts +137 -84
  64. package/extensions/subagent/cmux-dashboard.ts +39 -30
  65. package/extensions/subagent/index.ts +79 -59
  66. package/extensions/subagent/resume.ts +1 -1
  67. package/extensions/subagent/run.ts +304 -193
  68. package/extensions/subagent/runtime.ts +781 -343
  69. package/extensions/tau/index.ts +47 -38
  70. package/extensions/tau-help/help.md +14 -6
  71. package/extensions/tool-loader/README.md +4 -2
  72. package/extensions/tool-loader/index.ts +77 -52
  73. package/extensions/web/index.ts +6 -3
  74. package/extensions/web/webfetch.ts +114 -86
  75. package/package.json +3 -3
  76. package/schemas/tau.schema.json +20 -33
  77. package/shared/bounded-text-result.ts +1 -0
  78. package/shared/checkpoint-visibility.ts +9 -0
  79. package/shared/events.ts +28 -10
  80. package/shared/isolated-session.ts +71 -29
  81. package/shared/model-effort.ts +9 -19
  82. package/shared/model-fallback/index.ts +66 -41
  83. package/shared/settings/load.ts +45 -38
  84. package/shared/temporary-output-store.ts +16 -10
  85. package/{extensions/explore → src}/ast/adapter.ts +11 -2
  86. package/{extensions/explore → src}/ast/engine.ts +54 -38
  87. package/{extensions/explore → src}/ast/format/ast-search.ts +15 -6
  88. package/{extensions/explore → src}/ast/format/context.ts +3 -6
  89. package/src/ast/format/deps.ts +94 -0
  90. package/{extensions/explore → src}/ast/format/discover.ts +2 -2
  91. package/{extensions/explore → src}/ast/format/impact.ts +9 -11
  92. package/{extensions/explore → src}/ast/format/outline.ts +2 -2
  93. package/{extensions/explore → src}/ast/format/relationships.ts +11 -7
  94. package/{extensions/explore → src}/ast/format/show.ts +1 -1
  95. package/src/ast/format/targeted-result.ts +20 -0
  96. package/src/ast/grammars/c_sharp.wasm +0 -0
  97. package/{extensions/explore → src}/ast/grammars/manifest.json +9 -12
  98. package/{extensions/explore → src}/ast/grammars/manifest.ts +0 -4
  99. package/{extensions/explore → src}/ast/graph/file-graph.ts +210 -98
  100. package/{extensions/explore → src}/ast/graph/relationships.ts +285 -72
  101. package/{extensions/explore → src}/ast/ir.ts +6 -0
  102. package/src/ast/languages/csharp-file-deps.ts +90 -0
  103. package/{extensions/explore → src}/ast/languages/csharp.ts +9 -40
  104. package/src/ast/languages/dotted-index.ts +131 -0
  105. package/{extensions/explore → src}/ast/languages/file-dep-util.ts +22 -13
  106. package/src/ast/languages/fixtures/sample.cs +53 -0
  107. package/src/ast/languages/fixtures/sample.cs.expected.json +156 -0
  108. package/src/ast/languages/fixtures/sample.go +37 -0
  109. package/src/ast/languages/fixtures/sample.go.expected.json +96 -0
  110. package/src/ast/languages/fixtures/sample.java +40 -0
  111. package/src/ast/languages/fixtures/sample.java.expected.json +126 -0
  112. package/src/ast/languages/fixtures/sample.kt +31 -0
  113. package/src/ast/languages/fixtures/sample.kt.expected.json +134 -0
  114. package/src/ast/languages/fixtures/sample.md +23 -0
  115. package/src/ast/languages/fixtures/sample.md.expected.json +44 -0
  116. package/src/ast/languages/fixtures/sample.rs +54 -0
  117. package/src/ast/languages/fixtures/sample.rs.expected.json +178 -0
  118. package/src/ast/languages/fixtures/sample.swift +45 -0
  119. package/src/ast/languages/fixtures/sample.swift.expected.json +147 -0
  120. package/src/ast/languages/fixtures/sample.ts +45 -0
  121. package/src/ast/languages/fixtures/sample.ts.expected.json +141 -0
  122. package/src/ast/languages/fixtures/sample.tsx +36 -0
  123. package/src/ast/languages/fixtures/sample.tsx.expected.json +59 -0
  124. package/{extensions/explore → src}/ast/languages/go-file-deps.ts +6 -0
  125. package/{extensions/explore → src}/ast/languages/go.ts +21 -3
  126. package/{extensions/explore → src}/ast/languages/java.ts +5 -38
  127. package/src/ast/languages/jvm-file-deps.ts +135 -0
  128. package/{extensions/explore → src}/ast/languages/kotlin.ts +5 -39
  129. package/{extensions/explore → src}/ast/languages/rust-file-deps.ts +3 -3
  130. package/{extensions/explore → src}/ast/languages/rust.ts +21 -13
  131. package/{extensions/explore → src}/ast/languages/swift-file-deps.ts +22 -15
  132. package/{extensions/explore → src}/ast/languages/swift.ts +9 -39
  133. package/{extensions/explore → src}/ast/languages/tree.ts +47 -0
  134. package/{extensions/explore → src}/ast/languages/typescript-file-deps.ts +108 -0
  135. package/{extensions/explore → src}/ast/languages/typescript-package-surface.ts +4 -3
  136. package/{extensions/explore → src}/ast/languages/typescript.ts +3 -5
  137. package/{extensions/explore → src}/ast/markdown.ts +72 -47
  138. package/{extensions/explore → src}/ast/queries/ast-search.ts +164 -146
  139. package/{extensions/explore → src}/ast/queries/composite-target.ts +1 -1
  140. package/{extensions/explore → src}/ast/queries/context.ts +116 -80
  141. package/{extensions/explore → src}/ast/queries/discover.ts +154 -105
  142. package/src/ast/queries/impact.ts +291 -0
  143. package/{extensions/explore → src}/ast/queries/outline.ts +4 -2
  144. package/{extensions/explore → src}/ast/queries/show.ts +46 -42
  145. package/{extensions/explore → src}/ast/query.ts +1 -1
  146. package/{extensions/explore → src}/ast/registry.ts +1 -2
  147. package/{extensions/explore → src}/ast/scan.ts +1 -1
  148. package/src/ast/session.ts +50 -0
  149. package/{extensions/explore → src}/ast/slice.ts +8 -0
  150. package/{extensions/explore → src/ast}/traverse.ts +32 -20
  151. package/src/file-injection/index.ts +547 -0
  152. package/src/image-generation/client.ts +50 -31
  153. package/src/image-generation/index.ts +177 -134
  154. package/src/index.ts +2 -0
  155. package/src/tool-loading/index.ts +58 -0
  156. package/extensions/context/evidence.ts +0 -517
  157. package/extensions/context/projection.ts +0 -141
  158. package/extensions/context/state.ts +0 -30
  159. package/extensions/context/write-scope.ts +0 -109
  160. package/extensions/explore/ast/format/deps.ts +0 -86
  161. package/extensions/explore/ast/grammars/odin.wasm +0 -0
  162. package/extensions/explore/ast/languages/csharp-file-deps.ts +0 -189
  163. package/extensions/explore/ast/languages/fixtures/sample.cs +0 -17
  164. package/extensions/explore/ast/languages/fixtures/sample.java +0 -21
  165. package/extensions/explore/ast/languages/fixtures/sample.kt +0 -15
  166. package/extensions/explore/ast/languages/fixtures/sample.odin +0 -28
  167. package/extensions/explore/ast/languages/fixtures/sample.rs +0 -31
  168. package/extensions/explore/ast/languages/fixtures/sample.swift +0 -25
  169. package/extensions/explore/ast/languages/jvm-file-deps.ts +0 -217
  170. package/extensions/explore/ast/languages/odin-file-deps.ts +0 -175
  171. package/extensions/explore/ast/languages/odin.ts +0 -407
  172. package/extensions/explore/ast/queries/impact.ts +0 -250
  173. package/extensions/explore/outline-injection.ts +0 -151
  174. package/extensions/explore/read/autoread.ts +0 -118
  175. package/extensions/working-memory/README.md +0 -17
  176. package/extensions/working-memory/checkpoint.ts +0 -242
  177. package/extensions/working-memory/index.ts +0 -341
  178. package/extensions/working-memory/memory.ts +0 -235
  179. package/extensions/working-memory/render.ts +0 -185
  180. package/extensions/working-memory/settings.ts +0 -38
  181. package/extensions/working-memory/state.ts +0 -158
  182. package/shared/autoread.ts +0 -234
  183. package/shared/context-messages.ts +0 -19
  184. package/shared/outline-injection.ts +0 -56
  185. /package/{extensions/explore → src}/ast/format/composite.ts +0 -0
  186. /package/{extensions/explore → src}/ast/grammars/kotlin.wasm +0 -0
  187. /package/{extensions/explore → src}/ast/grammars/swift.wasm +0 -0
  188. /package/{extensions/explore → src}/ast/identity.ts +0 -0
  189. /package/{extensions/explore → src}/ast/package-surface.ts +0 -0
  190. /package/{extensions/explore/ast/read/policy.ts → src/ast/read-policy.ts} +0 -0
@@ -230,19 +230,32 @@ async function stageUpdate(cwd: string, op: Extract<PatchOperation, { type: "upd
230
230
  };
231
231
  }
232
232
 
233
+ function operationPaths(cwd: string, op: PatchOperation): string[] {
234
+ const paths = [resolvePath(cwd, op.path)];
235
+ if (op.type === "update" && op.movePath) paths.push(resolvePath(cwd, op.movePath));
236
+ return paths;
237
+ }
238
+
239
+ function recordPathSection(
240
+ pathSections: Map<string, number[]>,
241
+ displayPaths: Map<string, string>,
242
+ path: string,
243
+ sectionIndex: number,
244
+ displayPath: string,
245
+ ): void {
246
+ const existing = pathSections.get(path) ?? [];
247
+ existing.push(sectionIndex);
248
+ pathSections.set(path, existing);
249
+ if (!displayPaths.has(path)) displayPaths.set(path, displayPath);
250
+ }
251
+
233
252
  function collectDupPathFailures(cwd: string, operations: PatchOperation[]): PatchFailure[] {
234
253
  const pathSections = new Map<string, number[]>();
235
254
  const displayPaths = new Map<string, string>();
236
255
 
237
256
  for (const op of operations) {
238
- const paths = [resolvePath(cwd, op.path)];
239
- if (op.type === "update" && op.movePath) paths.push(resolvePath(cwd, op.movePath));
240
-
241
- for (const p of paths) {
242
- const existing = pathSections.get(p) ?? [];
243
- existing.push(op.sectionIndex);
244
- pathSections.set(p, existing);
245
- if (!displayPaths.has(p)) displayPaths.set(p, op.path);
257
+ for (const path of operationPaths(cwd, op)) {
258
+ recordPathSection(pathSections, displayPaths, path, op.sectionIndex, op.path);
246
259
  }
247
260
  }
248
261
 
@@ -272,12 +285,77 @@ function withMutationQueuePaths<T>(paths: string[], fn: () => Promise<T>): Promi
272
285
  return current();
273
286
  }
274
287
 
275
- export async function applyExactSourceMutations(
288
+ function displayPathFor(cwd: string, absolutePath: string): string {
289
+ const displayPath = relative(resolve(cwd), absolutePath);
290
+ return displayPath.startsWith("..") || isAbsolute(displayPath) ? absolutePath : displayPath;
291
+ }
292
+
293
+ async function stageExactSourceMutation(
276
294
  cwd: string,
277
- mutations: readonly ExactSourceMutation[],
295
+ mutation: ExactSourceMutation,
296
+ sectionIndex: number,
297
+ signal?: AbortSignal,
298
+ ): Promise<{ mutation: ExactSourceMutation; change: ApplyPatchChange }> {
299
+ throwIfAborted(signal);
300
+ await assertExistingFile(mutation.path, mutation.path);
301
+ const canonical = await realpath(mutation.path);
302
+ if (canonical !== mutation.path) {
303
+ throw new Error(`Exact-source mutation path is not canonical: ${mutation.path}`);
304
+ }
305
+ const current = await readUtf8(mutation.path, mutation.path);
306
+ const currentFingerprint = fingerprint(current);
307
+ if (currentFingerprint !== mutation.expectedFingerprint) {
308
+ throw new Error(
309
+ `Source changed before exact-source mutation for ${mutation.path}; expected ${mutation.expectedFingerprint}, found ${currentFingerprint}.`,
310
+ );
311
+ }
312
+ const lineCount = countLogicalLines(mutation.source);
313
+ return {
314
+ mutation,
315
+ change: {
316
+ sectionIndex,
317
+ kind: "update",
318
+ path: displayPathFor(cwd, mutation.path),
319
+ linesAdded: lineCount,
320
+ linesRemoved: countLogicalLines(current),
321
+ resultingFingerprint: fingerprint(mutation.source),
322
+ snapshotRanges: lineCount > 0 ? [{ startLine: 1, endLine: Math.min(lineCount, 120) }] : undefined,
323
+ },
324
+ };
325
+ }
326
+
327
+ async function commitExactSourceMutations(
328
+ staged: Array<{ mutation: ExactSourceMutation; change: ApplyPatchChange }>,
278
329
  signal?: AbortSignal,
279
330
  ): Promise<ApplyPatchSummary> {
280
331
  throwIfAborted(signal);
332
+ const changes: ApplyPatchChange[] = [];
333
+ for (const { mutation, change } of staged) {
334
+ try {
335
+ throwIfAborted(signal);
336
+ await writeFile(mutation.path, mutation.source, "utf8");
337
+ changes.push(change);
338
+ } catch (error) {
339
+ return {
340
+ status: changes.length === 0 ? "failed" : "partial",
341
+ changes,
342
+ failures: [
343
+ {
344
+ phase: "apply",
345
+ sectionIndex: change.sectionIndex,
346
+ path: change.path,
347
+ kind: "update",
348
+ message: error instanceof Error ? error.message : String(error),
349
+ },
350
+ ],
351
+ totalSections: staged.length,
352
+ };
353
+ }
354
+ }
355
+ return { status: "completed", changes, failures: [], totalSections: staged.length };
356
+ }
357
+
358
+ function orderExactSourceMutations(cwd: string, mutations: readonly ExactSourceMutation[]): ExactSourceMutation[] {
281
359
  if (mutations.length === 0) throw new Error("Exact-source mutation requires at least one file.");
282
360
  const byPath = new Map<string, ExactSourceMutation>();
283
361
  for (const mutation of mutations) {
@@ -285,71 +363,115 @@ export async function applyExactSourceMutations(
285
363
  if (byPath.has(path)) throw new Error(`Conflicting exact-source mutations for path: ${mutation.path}`);
286
364
  byPath.set(path, { ...mutation, path });
287
365
  }
288
- const ordered = [...byPath.values()].sort((left, right) => left.path.localeCompare(right.path));
366
+ return [...byPath.values()].sort((left, right) => left.path.localeCompare(right.path));
367
+ }
289
368
 
369
+ export async function applyExactSourceMutations(
370
+ cwd: string,
371
+ mutations: readonly ExactSourceMutation[],
372
+ signal?: AbortSignal,
373
+ ): Promise<ApplyPatchSummary> {
374
+ throwIfAborted(signal);
375
+ const ordered = orderExactSourceMutations(cwd, mutations);
290
376
  return withMutationQueuePaths(
291
377
  ordered.map((mutation) => mutation.path),
292
378
  async () => {
293
379
  const staged: Array<{ mutation: ExactSourceMutation; change: ApplyPatchChange }> = [];
294
380
  for (const [sectionIndex, mutation] of ordered.entries()) {
295
- throwIfAborted(signal);
296
- await assertExistingFile(mutation.path, mutation.path);
297
- const canonical = await realpath(mutation.path);
298
- if (canonical !== mutation.path) {
299
- throw new Error(`Exact-source mutation path is not canonical: ${mutation.path}`);
300
- }
301
- const current = await readUtf8(mutation.path, mutation.path);
302
- const currentFingerprint = fingerprint(current);
303
- if (currentFingerprint !== mutation.expectedFingerprint) {
304
- throw new Error(
305
- `Source changed before exact-source mutation for ${mutation.path}; expected ${mutation.expectedFingerprint}, found ${currentFingerprint}.`,
306
- );
307
- }
308
- const displayPath = relative(resolve(cwd), mutation.path);
309
- const lineCount = countLogicalLines(mutation.source);
310
- staged.push({
311
- mutation,
312
- change: {
313
- sectionIndex,
314
- kind: "update",
315
- path: displayPath.startsWith("..") || isAbsolute(displayPath) ? mutation.path : displayPath,
316
- linesAdded: lineCount,
317
- linesRemoved: countLogicalLines(current),
318
- resultingFingerprint: fingerprint(mutation.source),
319
- snapshotRanges: lineCount > 0 ? [{ startLine: 1, endLine: Math.min(lineCount, 120) }] : undefined,
320
- },
321
- });
381
+ staged.push(await stageExactSourceMutation(cwd, mutation, sectionIndex, signal));
322
382
  }
323
-
324
- throwIfAborted(signal);
325
- const changes: ApplyPatchChange[] = [];
326
- for (const { mutation, change } of staged) {
327
- try {
328
- throwIfAborted(signal);
329
- await writeFile(mutation.path, mutation.source, "utf8");
330
- changes.push(change);
331
- } catch (error) {
332
- return {
333
- status: changes.length === 0 ? "failed" : "partial",
334
- changes,
335
- failures: [
336
- {
337
- phase: "apply",
338
- sectionIndex: change.sectionIndex,
339
- path: change.path,
340
- kind: "update",
341
- message: error instanceof Error ? error.message : String(error),
342
- },
343
- ],
344
- totalSections: staged.length,
345
- };
346
- }
347
- }
348
- return { status: "completed", changes, failures: [], totalSections: staged.length };
383
+ return commitExactSourceMutations(staged, signal);
349
384
  },
350
385
  );
351
386
  }
352
387
 
388
+ function toApplyFailure(op: PatchOperation, error: unknown): PatchFailure {
389
+ const failure: PatchFailure = {
390
+ phase: "apply",
391
+ sectionIndex: op.sectionIndex,
392
+ path: op.path,
393
+ kind: op.type,
394
+ message: error instanceof Error ? error.message : String(error),
395
+ };
396
+ if (error instanceof UpdateChunkApplyError) {
397
+ failure.chunkIndex = error.chunkIndex;
398
+ failure.totalChunks = error.totalChunks;
399
+ failure.contextHint = error.contextHint;
400
+ }
401
+ return failure;
402
+ }
403
+
404
+ async function stageOperation(cwd: string, op: PatchOperation): Promise<StagedMutation> {
405
+ if (op.type === "add" || op.type === "replace") return stageWholeFile(cwd, op);
406
+ if (op.type === "delete") return stageDelete(cwd, op);
407
+ return stageUpdate(cwd, op);
408
+ }
409
+
410
+ async function stageRunnableOperations(
411
+ cwd: string,
412
+ operations: PatchOperation[],
413
+ initialFailures: PatchFailure[],
414
+ ): Promise<{ staged: StagedMutation[]; failures: PatchFailure[] }> {
415
+ const staged: StagedMutation[] = [];
416
+ const failures: PatchFailure[] = [...initialFailures];
417
+ for (const op of operations) {
418
+ try {
419
+ staged.push(await stageOperation(cwd, op));
420
+ } catch (error) {
421
+ if (error instanceof Error && error.message === "Operation aborted") throw error;
422
+ failures.push(toApplyFailure(op, error));
423
+ }
424
+ }
425
+ return { staged, failures };
426
+ }
427
+
428
+ async function commitStagedMutations(
429
+ staged: StagedMutation[],
430
+ failures: PatchFailure[],
431
+ totalSections: number,
432
+ signal: AbortSignal | undefined,
433
+ onProgress?: (summary: ApplyPatchSummary) => void | Promise<void>,
434
+ ): Promise<ApplyPatchSummary> {
435
+ if (staged.length === 0) {
436
+ const summary: ApplyPatchSummary = { status: "failed", changes: [], failures, totalSections };
437
+ await onProgress?.(summary);
438
+ return summary;
439
+ }
440
+
441
+ throwIfAborted(signal);
442
+ const changes: ApplyPatchChange[] = [];
443
+ for (const mutation of staged) {
444
+ try {
445
+ await mutation.commit();
446
+ changes.push(mutation.change);
447
+ await onProgress?.({ status: "partial", changes: [...changes], failures: [...failures], totalSections });
448
+ } catch (error) {
449
+ failures.push({
450
+ phase: "apply",
451
+ sectionIndex: mutation.change.sectionIndex,
452
+ path: mutation.change.path,
453
+ kind: mutation.change.kind,
454
+ message: error instanceof Error ? error.message : String(error),
455
+ });
456
+ const summary: ApplyPatchSummary = {
457
+ status: changes.length === 0 ? "failed" : "partial",
458
+ changes: [...changes],
459
+ failures: [...failures],
460
+ totalSections,
461
+ };
462
+ await onProgress?.(summary);
463
+ return summary;
464
+ }
465
+ }
466
+
467
+ return {
468
+ status: failures.length > 0 ? "partial" : "completed",
469
+ changes,
470
+ failures,
471
+ totalSections,
472
+ };
473
+ }
474
+
353
475
  export async function applyPatch(
354
476
  cwd: string,
355
477
  input: string,
@@ -370,80 +492,13 @@ export async function applyPatch(
370
492
  return summary;
371
493
  }
372
494
 
373
- const dupFailures = collectDupPathFailures(cwd, operations);
374
- const initialFailures = [...parseFailures, ...dupFailures];
495
+ const initialFailures = [...parseFailures, ...collectDupPathFailures(cwd, operations)];
375
496
  const blockedSections = new Set(initialFailures.map((failure) => failure.sectionIndex));
376
497
  const runnableOperations = operations.filter((op) => !blockedSections.has(op.sectionIndex));
377
-
378
- const queuePaths = runnableOperations.flatMap((op) => {
379
- const paths = [resolvePath(cwd, op.path)];
380
- if (op.type === "update" && op.movePath) paths.push(resolvePath(cwd, op.movePath));
381
- return paths;
382
- });
498
+ const queuePaths = runnableOperations.flatMap((op) => operationPaths(cwd, op));
383
499
 
384
500
  return withMutationQueuePaths(queuePaths, async () => {
385
- const staged: StagedMutation[] = [];
386
- const failures: PatchFailure[] = [...initialFailures];
387
-
388
- for (const op of runnableOperations) {
389
- try {
390
- if (op.type === "add" || op.type === "replace") staged.push(await stageWholeFile(cwd, op));
391
- else if (op.type === "delete") staged.push(await stageDelete(cwd, op));
392
- else staged.push(await stageUpdate(cwd, op));
393
- } catch (error) {
394
- if (error instanceof Error && error.message === "Operation aborted") throw error;
395
- const failure: PatchFailure = {
396
- phase: "apply",
397
- sectionIndex: op.sectionIndex,
398
- path: op.path,
399
- kind: op.type,
400
- message: error instanceof Error ? error.message : String(error),
401
- };
402
- if (error instanceof UpdateChunkApplyError) {
403
- failure.chunkIndex = error.chunkIndex;
404
- failure.totalChunks = error.totalChunks;
405
- failure.contextHint = error.contextHint;
406
- }
407
- failures.push(failure);
408
- }
409
- }
410
-
411
- if (staged.length === 0) {
412
- const summary: ApplyPatchSummary = { status: "failed", changes: [], failures, totalSections };
413
- await onProgress?.(summary);
414
- return summary;
415
- }
416
-
417
- throwIfAborted(signal);
418
-
419
- const changes: ApplyPatchChange[] = [];
420
- for (const mutation of staged) {
421
- try {
422
- await mutation.commit();
423
- changes.push(mutation.change);
424
- await onProgress?.({ status: "partial", changes: [...changes], failures: [...failures], totalSections });
425
- } catch (error) {
426
- const message = error instanceof Error ? error.message : String(error);
427
- failures.push({
428
- phase: "apply",
429
- sectionIndex: mutation.change.sectionIndex,
430
- path: mutation.change.path,
431
- kind: mutation.change.kind,
432
- message,
433
- });
434
- const status = changes.length === 0 ? "failed" : "partial";
435
- const summary: ApplyPatchSummary = {
436
- status,
437
- changes: [...changes],
438
- failures: [...failures],
439
- totalSections,
440
- };
441
- await onProgress?.(summary);
442
- return summary;
443
- }
444
- }
445
-
446
- const status: ApplyPatchSummary["status"] = failures.length > 0 ? "partial" : "completed";
447
- return { status, changes, failures, totalSections };
501
+ const { staged, failures } = await stageRunnableOperations(cwd, runnableOperations, initialFailures);
502
+ return commitStagedMutations(staged, failures, totalSections, signal, onProgress);
448
503
  });
449
504
  }
@@ -125,10 +125,86 @@ function buildReplacement(chunk: UpdateFileChunk, fileLines: string[], matchInde
125
125
  return result;
126
126
  }
127
127
 
128
+ interface ChunkReplacement {
129
+ index: number;
130
+ deleteCount: number;
131
+ insert: string[];
132
+ order: number;
133
+ }
134
+
135
+ function seekChunkContext(
136
+ lines: string[],
137
+ chunk: UpdateFileChunk,
138
+ lineIndex: number,
139
+ chunkNumber: number,
140
+ total: number,
141
+ ): number {
142
+ if (!chunk.changeContext) return lineIndex;
143
+ const contextIndex = seekSequence(lines, [chunk.changeContext], lineIndex);
144
+ if (contextIndex < 0) {
145
+ throw new UpdateChunkApplyError(chunkNumber, total, formatContextHint(chunk), "could not find update context");
146
+ }
147
+ return contextIndex + 1;
148
+ }
149
+
150
+ function matchChunkIndex(
151
+ lines: string[],
152
+ chunk: UpdateFileChunk,
153
+ oldLines: string[],
154
+ lineIndex: number,
155
+ chunkNumber: number,
156
+ total: number,
157
+ ): number {
158
+ if (oldLines.length === 0) return lines.length;
159
+ if (chunk.isEndOfFile) {
160
+ const eofIndex = lines.length - oldLines.length;
161
+ if (eofIndex >= lineIndex && seekSequence(lines, oldLines, eofIndex) === eofIndex) return eofIndex;
162
+ }
163
+ const matchIndex = seekSequence(lines, oldLines, lineIndex);
164
+ if (matchIndex < 0) {
165
+ throw new UpdateChunkApplyError(chunkNumber, total, formatContextHint(chunk), "could not match");
166
+ }
167
+ return matchIndex;
168
+ }
169
+
170
+ function planChunkReplacement(
171
+ lines: string[],
172
+ chunk: UpdateFileChunk,
173
+ lineIndex: number,
174
+ order: number,
175
+ chunkNumber: number,
176
+ total: number,
177
+ ): {
178
+ replacement: ChunkReplacement;
179
+ nextLineIndex: number;
180
+ rawRange: { index: number; deleteCount: number; insertCount: number };
181
+ } {
182
+ const searchFrom = seekChunkContext(lines, chunk, lineIndex, chunkNumber, total);
183
+ const oldLines = chunk.lines.filter((l) => l.prefix !== "+").map((l) => l.text);
184
+ const matchIndex = matchChunkIndex(lines, chunk, oldLines, searchFrom, chunkNumber, total);
185
+ const insert = oldLines.length === 0 ? chunk.lines.map((l) => l.text) : buildReplacement(chunk, lines, matchIndex);
186
+ const deleteCount = oldLines.length;
187
+ return {
188
+ replacement: { index: matchIndex, deleteCount, insert, order },
189
+ nextLineIndex: oldLines.length === 0 ? searchFrom : matchIndex + oldLines.length,
190
+ rawRange: { index: matchIndex, deleteCount, insertCount: insert.length },
191
+ };
192
+ }
193
+
194
+ function applyPlannedReplacements(lines: string[], replacements: ChunkReplacement[]): string[] {
195
+ const output = [...lines];
196
+ replacements
197
+ .sort((a, b) => b.index - a.index || b.order - a.order)
198
+ .forEach((r) => {
199
+ output.splice(r.index, r.deleteCount, ...r.insert);
200
+ });
201
+ return output;
202
+ }
203
+
128
204
  export function applyChunksWithRanges(currentContent: string, chunks: UpdateFileChunk[]): ApplyChunksResult {
129
205
  const parts = splitText(currentContent);
130
206
  const lines = splitLogicalLines(parts.text);
131
- const replacements: Array<{ index: number; deleteCount: number; insert: string[]; order: number }> = [];
207
+ const replacements: ChunkReplacement[] = [];
132
208
  const rawRanges: Array<{ index: number; deleteCount: number; insertCount: number }> = [];
133
209
  let lineIndex = 0;
134
210
  let order = 0;
@@ -136,56 +212,14 @@ export function applyChunksWithRanges(currentContent: string, chunks: UpdateFile
136
212
  for (let ci = 0; ci < chunks.length; ci += 1) {
137
213
  const chunk = chunks[ci];
138
214
  if (chunk === undefined) continue;
139
- const oldLines = chunk.lines.filter((l) => l.prefix !== "+").map((l) => l.text);
140
-
141
- if (chunk.changeContext) {
142
- const contextIndex = seekSequence(lines, [chunk.changeContext], lineIndex);
143
- if (contextIndex < 0) {
144
- throw new UpdateChunkApplyError(
145
- ci + 1,
146
- chunks.length,
147
- formatContextHint(chunk),
148
- "could not find update context",
149
- );
150
- }
151
- lineIndex = contextIndex + 1;
152
- }
153
-
154
- if (oldLines.length === 0) {
155
- const insertIndex = lines.length;
156
- const insert = chunk.lines.map((l) => l.text);
157
- replacements.push({ index: insertIndex, deleteCount: 0, insert, order });
158
- order += 1;
159
- rawRanges.push({ index: insertIndex, deleteCount: 0, insertCount: insert.length });
160
- continue;
161
- }
162
-
163
- let matchIndex = -1;
164
- if (chunk.isEndOfFile) {
165
- const eofIndex = lines.length - oldLines.length;
166
- if (eofIndex >= lineIndex && seekSequence(lines, oldLines, eofIndex) === eofIndex) {
167
- matchIndex = eofIndex;
168
- }
169
- }
170
- if (matchIndex < 0) matchIndex = seekSequence(lines, oldLines, lineIndex);
171
- if (matchIndex < 0) {
172
- throw new UpdateChunkApplyError(ci + 1, chunks.length, formatContextHint(chunk), "could not match");
173
- }
174
-
175
- const replacement = buildReplacement(chunk, lines, matchIndex);
176
- replacements.push({ index: matchIndex, deleteCount: oldLines.length, insert: replacement, order });
215
+ const planned = planChunkReplacement(lines, chunk, lineIndex, order, ci + 1, chunks.length);
216
+ replacements.push(planned.replacement);
217
+ rawRanges.push(planned.rawRange);
218
+ lineIndex = planned.nextLineIndex;
177
219
  order += 1;
178
- rawRanges.push({ index: matchIndex, deleteCount: oldLines.length, insertCount: replacement.length });
179
- lineIndex = matchIndex + oldLines.length;
180
220
  }
181
221
 
182
- const output = [...lines];
183
- replacements
184
- .sort((a, b) => b.index - a.index || b.order - a.order)
185
- .forEach((r) => {
186
- output.splice(r.index, r.deleteCount, ...r.insert);
187
- });
188
-
222
+ const output = applyPlannedReplacements(lines, replacements);
189
223
  const joined = output.join("\n");
190
224
  const normalized = joined === "" ? "" : `${joined}\n`;
191
225
  const next = parts.bom + restoreLineEndings(normalized, parts.lineEnding);