mixdog 0.9.87 → 0.9.89

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 (167) hide show
  1. package/package.json +13 -7
  2. package/scripts/tool-failures.mjs +60 -24
  3. package/src/agents/heavy-worker/AGENT.md +3 -4
  4. package/src/agents/worker/AGENT.md +1 -2
  5. package/src/cli.mjs +8 -0
  6. package/src/defaults/cycle3-review-prompt.md +4 -4
  7. package/src/rules/agent/00-core.md +3 -0
  8. package/src/rules/agent/42-cycle3-agent.md +5 -6
  9. package/src/rules/lead/01-general.md +2 -0
  10. package/src/rules/shared/01-tool.md +29 -35
  11. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +3 -51
  12. package/src/runtime/agent/orchestrator/agent-runtime/maintenance-route.mjs +59 -0
  13. package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +2 -2
  14. package/src/runtime/agent/orchestrator/agent-runtime/title-completion.mjs +67 -0
  15. package/src/runtime/agent/orchestrator/agent-trace-format.mjs +57 -4
  16. package/src/runtime/agent/orchestrator/agent-trace-io.mjs +2 -3
  17. package/src/runtime/agent/orchestrator/config.mjs +29 -11
  18. package/src/runtime/agent/orchestrator/context/collect.mjs +1 -1
  19. package/src/runtime/agent/orchestrator/internal-agents.mjs +1 -1
  20. package/src/runtime/agent/orchestrator/mcp/client.mjs +0 -24
  21. package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +3 -3
  22. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +25 -17
  23. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +321 -22
  24. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +35 -22
  25. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +28 -23
  26. package/src/runtime/agent/orchestrator/providers/gemini.mjs +10 -2
  27. package/src/runtime/agent/orchestrator/providers/grok-oauth-login.mjs +0 -1
  28. package/src/runtime/agent/orchestrator/providers/grok-oauth-tokens.mjs +0 -1
  29. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +2 -5
  30. package/src/runtime/agent/orchestrator/providers/lib/anthropic-native-blocks.mjs +27 -0
  31. package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +23 -0
  32. package/src/runtime/agent/orchestrator/providers/lib/stream-outcome.mjs +329 -0
  33. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +23 -1
  34. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +107 -20
  35. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +4 -3
  36. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +124 -12
  37. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +19 -2
  38. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +1 -1
  39. package/src/runtime/agent/orchestrator/providers/openai-responses-payload.mjs +1 -1
  40. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +110 -75
  41. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +96 -113
  42. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +160 -11
  43. package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +18 -1
  44. package/src/runtime/agent/orchestrator/session/lifecycle-scan.mjs +147 -117
  45. package/src/runtime/agent/orchestrator/session/loop/stop-hooks.mjs +88 -0
  46. package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +50 -8
  47. package/src/runtime/agent/orchestrator/session/loop/termination.mjs +25 -0
  48. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +1 -1
  49. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +47 -6
  50. package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +485 -24
  51. package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +100 -2
  52. package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +28 -0
  53. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +80 -14
  54. package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint.mjs +142 -2
  55. package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +128 -32
  56. package/src/runtime/agent/orchestrator/session/manager.mjs +5 -1
  57. package/src/runtime/agent/orchestrator/session/save-session-worker.mjs +77 -4
  58. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +70 -39
  59. package/src/runtime/agent/orchestrator/session/store/fs-probe.mjs +91 -0
  60. package/src/runtime/agent/orchestrator/session/store/listing.mjs +141 -64
  61. package/src/runtime/agent/orchestrator/session/store/live-state.mjs +253 -0
  62. package/src/runtime/agent/orchestrator/session/store/load-cache.mjs +211 -28
  63. package/src/runtime/agent/orchestrator/session/store/save-fault.mjs +313 -0
  64. package/src/runtime/agent/orchestrator/session/store/save-worker.mjs +622 -53
  65. package/src/runtime/agent/orchestrator/session/store/serialize.mjs +43 -10
  66. package/src/runtime/agent/orchestrator/session/store/summary-cache.mjs +53 -8
  67. package/src/runtime/agent/orchestrator/session/store/summary-rebuild-worker.mjs +20 -0
  68. package/src/runtime/agent/orchestrator/session/store/write-guards.mjs +113 -9
  69. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +2 -0
  70. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +476 -52
  71. package/src/runtime/agent/orchestrator/session/store.mjs +721 -196
  72. package/src/runtime/agent/orchestrator/session/token-native.mjs +2 -4
  73. package/src/runtime/agent/orchestrator/session/tool-batch.mjs +19 -0
  74. package/src/runtime/agent/orchestrator/tools/bash-session.mjs +4 -6
  75. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +5 -5
  76. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +146 -90
  77. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +6 -0
  78. package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +93 -17
  79. package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +5 -0
  80. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +7 -7
  81. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +6 -1
  82. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-spawn.mjs +17 -1
  83. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +85 -5
  84. package/src/runtime/agent/orchestrator/tools/builtin/snapshot-store.mjs +52 -0
  85. package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +54 -51
  86. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +14 -15
  87. package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +132 -8
  88. package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +86 -3
  89. package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +6 -1
  90. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +210 -38
  91. package/src/runtime/agent/orchestrator/tools/patch/paths.mjs +23 -1
  92. package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +87 -19
  93. package/src/runtime/agent/orchestrator/tools/patch-manifest.json +11 -11
  94. package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +33 -8
  95. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +12 -2
  96. package/src/runtime/media/lanes.mjs +2 -2
  97. package/src/runtime/media/renditions.mjs +101 -17
  98. package/src/runtime/media/renditions.test.mjs +54 -0
  99. package/src/runtime/media/store.mjs +111 -43
  100. package/src/runtime/media/store.test.mjs +53 -11
  101. package/src/runtime/memory/index.mjs +26 -50
  102. package/src/runtime/memory/lib/core-memory-candidates.mjs +2 -7
  103. package/src/runtime/memory/lib/core-memory-store.mjs +2 -9
  104. package/src/runtime/memory/lib/cycle-scheduler.mjs +10 -0
  105. package/src/runtime/memory/lib/embedding-provider.mjs +3 -3
  106. package/src/runtime/memory/lib/embedding-worker.mjs +60 -15
  107. package/src/runtime/memory/lib/http-router.mjs +4 -0
  108. package/src/runtime/memory/lib/ko-morph.mjs +49 -6
  109. package/src/runtime/memory/lib/memory-action-handlers.mjs +21 -18
  110. package/src/runtime/memory/lib/memory-cycle2.mjs +3 -3
  111. package/src/runtime/memory/lib/memory-cycle3.mjs +1 -3
  112. package/src/runtime/memory/lib/memory-recall-store.mjs +24 -0
  113. package/src/runtime/memory/lib/query-handlers.mjs +26 -28
  114. package/src/runtime/memory/tool-defs.mjs +3 -3
  115. package/src/runtime/shared/atomic-file.mjs +5 -1
  116. package/src/runtime/shared/child-guardian.mjs +73 -2
  117. package/src/runtime/shared/provider-api-key.mjs +22 -5
  118. package/src/runtime/shared/tool-execution-contract.mjs +53 -0
  119. package/src/session-runtime/config-helpers.mjs +6 -5
  120. package/src/session-runtime/lifecycle-api.mjs +16 -4
  121. package/src/session-runtime/media-api.mjs +31 -16
  122. package/src/session-runtime/prewarm.mjs +2 -24
  123. package/src/session-runtime/runtime-core.mjs +33 -15
  124. package/src/session-runtime/runtime-tunables.mjs +0 -3
  125. package/src/session-runtime/session-lifecycle.mjs +0 -6
  126. package/src/session-runtime/session-title.mjs +228 -0
  127. package/src/session-runtime/session-turn-api.mjs +11 -7
  128. package/src/session-runtime/workflow-agents-api.mjs +16 -0
  129. package/src/session-runtime/workflow.mjs +7 -5
  130. package/src/standalone/agent-tool/job-views.mjs +90 -36
  131. package/src/standalone/agent-tool/spawn-flow.mjs +25 -216
  132. package/src/standalone/agent-tool/worker-index.mjs +8 -0
  133. package/src/standalone/agent-tool/worker-rows.mjs +2 -1
  134. package/src/standalone/agent-tool.mjs +17 -17
  135. package/src/standalone/agent-watchdog-registry.mjs +19 -2
  136. package/src/standalone/channel-daemon.mjs +8 -0
  137. package/src/standalone/explore-tool.mjs +1 -1
  138. package/src/standalone/memory-runtime-proxy.mjs +4 -7
  139. package/src/standalone/projects.mjs +4 -1
  140. package/src/standalone/usage-dashboard.mjs +25 -3
  141. package/src/tui/App.jsx +5 -3
  142. package/src/tui/app/resume-picker.mjs +7 -2
  143. package/src/tui/app/route-pickers.mjs +11 -117
  144. package/src/tui/app/slash-dispatch.mjs +13 -3
  145. package/src/tui/app/transcript-row-estimate.mjs +1 -1
  146. package/src/tui/app/transcript-window.mjs +17 -93
  147. package/src/tui/app/use-transcript-scroll.mjs +32 -2
  148. package/src/tui/app/use-transcript-window.mjs +64 -114
  149. package/src/tui/components/Markdown.jsx +19 -2
  150. package/src/tui/components/Message.jsx +8 -3
  151. package/src/tui/components/ToolExecution.jsx +7 -3
  152. package/src/tui/dist/index.mjs +243 -223
  153. package/src/tui/engine/live-share.mjs +15 -5
  154. package/src/tui/engine/render-timing.mjs +2 -2
  155. package/src/tui/engine/session-api-ext.mjs +74 -4
  156. package/src/tui/engine/session-flow.mjs +3 -1
  157. package/src/tui/engine.mjs +2 -2
  158. package/src/tui/hooks/useSharedTick.mjs +0 -2
  159. package/src/tui/index.jsx +5 -5
  160. package/src/ui/statusline-agents.mjs +44 -11
  161. package/src/vendor/statusline/bin/statusline-route.mjs +26 -3
  162. package/src/workflows/default/WORKFLOW.md +3 -3
  163. package/src/workflows/solo/WORKFLOW.md +5 -3
  164. package/src/runtime/media/index.mjs +0 -18
  165. package/src/runtime/memory/lib/embedding-warmup.mjs +0 -68
  166. package/src/standalone/agent-shard/shard-child.mjs +0 -300
  167. package/src/standalone/agent-shard/shard-pool.mjs +0 -443
@@ -3,8 +3,8 @@
3
3
  // executePatchTool entry point + replay capture. Moved verbatim from
4
4
  // patch.mjs; control flow and output are unchanged.
5
5
 
6
- import { chmodSync, existsSync, readFileSync, readdirSync, writeFileSync, mkdirSync, rmSync, statSync } from 'node:fs';
7
- import { dirname as pathDirname, resolve as pathResolve, isAbsolute, join as pathJoin } from 'node:path';
6
+ import { chmodSync, existsSync, statSync, readFileSync, readdirSync, writeFileSync, mkdirSync, rmSync } from 'node:fs';
7
+ import { dirname as pathDirname, resolve as pathResolve, relative as pathRelative, isAbsolute, join as pathJoin } from 'node:path';
8
8
  import { parsePatch } from 'diff';
9
9
  import { getAbortSignalForSession } from '../../session/abort-lookup.mjs';
10
10
  import {
@@ -20,6 +20,7 @@ import { markCodeGraphDirtyPaths } from '../code-graph-state.mjs';
20
20
  import { prepareInput, isV4APatchInput, hasUnifiedBareV4AHunk, canFallbackCountedUnified, parseV4APatch, parseUnifiedBareV4APatch, parseUnifiedCountedAsV4APatch, isCompactedPlaceholderPatch, salvageV4AOpening } from './parsing.mjs';
21
21
  import {
22
22
  resolveBasePath,
23
+ resolveEntryPath,
23
24
  resolveV4AEntryPath,
24
25
  parsedEntryResolvedPath,
25
26
  isResolvedPathOutsideBase,
@@ -32,14 +33,13 @@ import {
32
33
  } from './paths.mjs';
33
34
  import { ensureNativePatchBinaryAvailable } from './native-server.mjs';
34
35
  import { assertPathReachable } from '../builtin/fs-reachability.mjs';
36
+ import { resolveReadPathRedirect } from '../builtin/snapshot-store.mjs';
35
37
  import { dispatchNativePatch, dispatchJsPatchEntries } from './dispatch.mjs';
36
38
  import {
37
39
  planV4ARenameSections,
38
40
  applyV4ARenameSections,
39
41
  formatV4ARenameSuccessLines,
40
42
  convertV4ASectionsToUnifiedPatch,
41
- convertUnifiedBareV4AToUnifiedPatch,
42
- convertUnifiedCountedToUnifiedPatchViaV4A,
43
43
  isV4ARenameSection,
44
44
  } from './v4a-convert.mjs';
45
45
 
@@ -47,24 +47,80 @@ function isPatchErrorText(text) {
47
47
  return /^Error:/i.test(String(text ?? '').trimStart());
48
48
  }
49
49
 
50
+ function patchPathKey(fullPath) {
51
+ const value = String(fullPath || '');
52
+ return process.platform === 'win32' ? value.toLowerCase() : value;
53
+ }
54
+
55
+ function redirectedPatchPath(requestedFullPath, readStateScope) {
56
+ // A newly-created exact requested path always wins over an older redirect.
57
+ if (!requestedFullPath || existsSync(requestedFullPath)) return requestedFullPath;
58
+ const redirected = resolveReadPathRedirect(requestedFullPath, readStateScope);
59
+ return redirected && existsSync(redirected) ? redirected : requestedFullPath;
60
+ }
61
+
62
+ function patchHeaderPathForResolved(basePath, fullPath) {
63
+ const rel = pathRelative(pathResolve(basePath), pathResolve(fullPath));
64
+ if (rel && !isAbsolute(rel) && !rel.split(/[\\/]+/).some((part) => part === '..')) {
65
+ return rel.replace(/\\/g, '/');
66
+ }
67
+ return fullPath;
68
+ }
69
+
70
+ function rewriteV4AReadRedirects(sections, basePath, readStateScope) {
71
+ return (sections || []).map((section) => {
72
+ if (!section || section.kind === 'add' || !section.path) return section;
73
+ const requested = resolveV4AEntryPath(basePath, section.path);
74
+ const redirected = redirectedPatchPath(requested, readStateScope);
75
+ if (patchPathKey(redirected) === patchPathKey(requested)) return section;
76
+ return { ...section, path: patchHeaderPathForResolved(basePath, redirected) };
77
+ });
78
+ }
79
+
80
+ function rewriteParsedReadRedirects(parsed, basePath, readStateScope) {
81
+ return (parsed || []).map((entry) => {
82
+ const kind = classifyEntry(entry);
83
+ if (kind === 'create' || !entry?.oldFileName) return entry;
84
+ const requested = resolveEntryPath(basePath, entry.oldFileName);
85
+ const redirected = redirectedPatchPath(requested, readStateScope);
86
+ if (patchPathKey(redirected) === patchPathKey(requested)) return entry;
87
+ const rewritten = {
88
+ ...entry,
89
+ oldFileName: patchHeaderPathForResolved(basePath, redirected),
90
+ };
91
+ if (kind === 'modify' && entry.newFileName) {
92
+ const newRequested = resolveEntryPath(basePath, entry.newFileName);
93
+ if (patchPathKey(newRequested) === patchPathKey(requested)) {
94
+ rewritten.newFileName = rewritten.oldFileName;
95
+ }
96
+ }
97
+ return rewritten;
98
+ });
99
+ }
100
+
101
+ // Pre-patch snapshot of every path this operation may touch: the bytes and
102
+ // mode of each existing target, or an "absent" marker. Restoring them undoes
103
+ // the whole batch. Hostile concurrent replacement of a path between capture
104
+ // and restore is out of scope.
50
105
  function capturePatchRollbackState(paths) {
51
106
  return paths.map((fullPath) => {
52
- if (!existsSync(fullPath)) return { fullPath, existed: false, content: null, mode: null };
53
- const stat = statSync(fullPath);
107
+ let stat;
108
+ try {
109
+ stat = statSync(fullPath);
110
+ } catch (err) {
111
+ if (err?.code === 'ENOENT') return { fullPath, existed: false, content: null, mode: null };
112
+ throw new Error(`apply_patch: rollback snapshot target unreadable: ${normalizeOutputPath(fullPath)} (${err?.code || err?.message || String(err)})`);
113
+ }
54
114
  if (!stat.isFile()) throw new Error(`apply_patch: rollback snapshot target is not a regular file: ${normalizeOutputPath(fullPath)}`);
55
- return {
56
- fullPath,
57
- existed: true,
58
- content: readFileSync(fullPath),
59
- mode: stat.mode,
60
- };
115
+ return { fullPath, existed: true, content: readFileSync(fullPath), mode: stat.mode };
61
116
  });
62
117
  }
63
118
 
64
119
  function restorePatchRollbackState(snapshots, readStateScope) {
65
120
  const errors = [];
66
- const paths = snapshots.map((snapshot) => snapshot.fullPath);
121
+ const paths = [];
67
122
  for (const snapshot of snapshots) {
123
+ const display = normalizeOutputPath(snapshot.fullPath);
68
124
  try {
69
125
  if (snapshot.existed) {
70
126
  mkdirSync(pathDirname(snapshot.fullPath), { recursive: true });
@@ -72,9 +128,12 @@ function restorePatchRollbackState(snapshots, readStateScope) {
72
128
  if (snapshot.mode != null) chmodSync(snapshot.fullPath, snapshot.mode);
73
129
  } else if (existsSync(snapshot.fullPath)) {
74
130
  rmSync(snapshot.fullPath, { force: true });
131
+ } else {
132
+ continue; // nothing written, nothing to undo
75
133
  }
134
+ paths.push(snapshot.fullPath);
76
135
  } catch (err) {
77
- errors.push(`${normalizeOutputPath(snapshot.fullPath)} — ${err?.message || String(err)}`);
136
+ errors.push(`${display} — ${err?.message || String(err)}`);
78
137
  }
79
138
  }
80
139
  invalidateBuiltinResultCache(paths);
@@ -186,7 +245,11 @@ async function applyPatchSequence(patchStr, requestedFormat, basePath, ctx) {
186
245
  if (isV4APatchInput(patchStr, requestedFormat)) {
187
246
  let allSections;
188
247
  try {
189
- allSections = parseV4APatch(patchStr);
248
+ allSections = rewriteV4AReadRedirects(
249
+ parseV4APatch(patchStr),
250
+ basePath,
251
+ readStateScope,
252
+ );
190
253
  } catch (err) {
191
254
  throw new Error(`apply_patch: V4A parse failed — ${err?.message || String(err)}`);
192
255
  }
@@ -197,7 +260,11 @@ async function applyPatchSequence(patchStr, requestedFormat, basePath, ctx) {
197
260
  // than converting the whole patch up front.
198
261
  let sections;
199
262
  try {
200
- sections = parseUnifiedBareV4APatch(patchStr);
263
+ sections = rewriteV4AReadRedirects(
264
+ parseUnifiedBareV4APatch(patchStr),
265
+ basePath,
266
+ readStateScope,
267
+ );
201
268
  } catch (err) {
202
269
  throw new Error(`apply_patch: bare @@ parse failed — ${err?.message || String(err)}`);
203
270
  }
@@ -215,7 +282,11 @@ async function applyPatchSequence(patchStr, requestedFormat, basePath, ctx) {
215
282
  // V4A-style sections and defer per-section conversion — same ordered-stop
216
283
  // guarantee as the V4A path (no whole-patch up-front convert).
217
284
  try {
218
- countedSections = parseUnifiedCountedAsV4APatch(patchStr);
285
+ countedSections = rewriteV4AReadRedirects(
286
+ parseUnifiedCountedAsV4APatch(patchStr),
287
+ basePath,
288
+ readStateScope,
289
+ );
219
290
  } catch (fallbackErr) {
220
291
  throw new Error(`apply_patch: parse failed — ${err?.message || String(err)}; V4A fallback failed — ${fallbackErr?.message || String(fallbackErr)}`);
221
292
  }
@@ -223,9 +294,17 @@ async function applyPatchSequence(patchStr, requestedFormat, basePath, ctx) {
223
294
  if (countedSections) {
224
295
  for (const section of countedSections) pushSectionUnit(section);
225
296
  } else {
297
+ parsed = rewriteParsedReadRedirects(parsed, basePath, readStateScope);
226
298
  for (const entry of parsed || []) {
227
299
  const kind = classifyEntry(entry);
228
300
  const headerName = kind === 'create' ? entry.newFileName : entry.oldFileName;
301
+ if (!headerName) {
302
+ throw new Error(
303
+ 'apply_patch: a file section header could not be parsed (no target path) — the patch body is not a valid diff. '
304
+ + 'Each section must start with `*** Update File: <path>` / `*** Add File: <path>` / `*** Delete File: <path>` '
305
+ + '(V4A, wrapped in `*** Begin Patch` / `*** End Patch`), or a `--- a/<path>` + `+++ b/<path>` pair (unified).',
306
+ );
307
+ }
229
308
  units.push({
230
309
  displayPath: normalizeOutputPath(stripDiffPrefix(headerName || '')),
231
310
  fullPath: parsedEntryResolvedPath(entry, basePath),
@@ -247,6 +326,42 @@ async function applyPatchSequence(patchStr, requestedFormat, basePath, ctx) {
247
326
 
248
327
  return withBuiltinPathLocks(lockPaths, () =>
249
328
  withAdvisoryLocks(lockPaths, async () => {
329
+ // Most multi-file failures are stale context in a later, independent
330
+ // file. For unique targets, validate every section against current bytes
331
+ // before the first write. Same-target sequences retain the existing
332
+ // ordered conversion/rollback path because later sections may
333
+ // intentionally depend on earlier edits.
334
+ const preparedUnits = new Map();
335
+ const uniqueTargets = new Set(lockPaths.map(patchPathKey)).size === units.length;
336
+ if (!dryRun && rollbackOnFailure && uniqueTargets) {
337
+ for (let i = 0; i < units.length; i++) {
338
+ const unit = units[i];
339
+ if (abortSignal?.aborted) return 'Error: apply_patch aborted during preflight; no files were written';
340
+ let parsed;
341
+ try {
342
+ parsed = await unit.buildParsed();
343
+ } catch (err) {
344
+ return `Error: apply_patch preflight rejected section ${i + 1}/${units.length} (${unit.displayPath}); no files were written.\n${err?.message || String(err)}`;
345
+ }
346
+ if (!Array.isArray(parsed) || parsed.length === 0) {
347
+ preparedUnits.set(i, { parsed, wave: null });
348
+ continue;
349
+ }
350
+ let wave;
351
+ try {
352
+ const { entries, headerRewrites } = await preValidateNativeBatch(parsed, basePath);
353
+ wave = { parsed, entries, headerRewrites };
354
+ } catch (err) {
355
+ return `Error: apply_patch preflight rejected section ${i + 1}/${units.length} (${unit.displayPath}); no files were written.\n${err?.message || String(err)}`;
356
+ }
357
+ const checked = await applyParsedWave(wave, basePath, { ...waveOpts, dryRun: true });
358
+ if (checked.error) {
359
+ const detail = checked.error.replace(/^Error:\s*/, '');
360
+ return `Error: apply_patch preflight rejected section ${i + 1}/${units.length} (${unit.displayPath}); no files were written.\n${detail}`;
361
+ }
362
+ preparedUnits.set(i, { parsed, wave });
363
+ }
364
+ }
250
365
  let rollbackSnapshots = [];
251
366
  if (!dryRun && rollbackOnFailure) {
252
367
  try {
@@ -268,13 +383,15 @@ async function applyPatchSequence(patchStr, requestedFormat, basePath, ctx) {
268
383
  failedIndex = i;
269
384
  continue;
270
385
  }
271
- let parsed;
272
- try {
273
- parsed = await unit.buildParsed();
274
- } catch (err) {
275
- failed = { displayPath: unit.displayPath, error: `Error: ${err?.message || String(err)}` };
276
- failedIndex = i;
277
- continue;
386
+ let parsed = preparedUnits.get(i)?.parsed;
387
+ if (!preparedUnits.has(i)) {
388
+ try {
389
+ parsed = await unit.buildParsed();
390
+ } catch (err) {
391
+ failed = { displayPath: unit.displayPath, error: `Error: ${err?.message || String(err)}` };
392
+ failedIndex = i;
393
+ continue;
394
+ }
278
395
  }
279
396
  if (!Array.isArray(parsed) || parsed.length === 0) {
280
397
  // Section produced no applicable hunks (all skipped / no-op). Nothing
@@ -282,14 +399,16 @@ async function applyPatchSequence(patchStr, requestedFormat, basePath, ctx) {
282
399
  applied.push({ displayPath: unit.displayPath, text: `(no changes) ${unit.displayPath}` });
283
400
  continue;
284
401
  }
285
- let wave;
286
- try {
287
- const { entries, headerRewrites } = await preValidateNativeBatch(parsed, basePath);
288
- wave = { parsed, entries, headerRewrites };
289
- } catch (err) {
290
- failed = { displayPath: unit.displayPath, error: `Error: ${err?.message || String(err)}` };
291
- failedIndex = i;
292
- continue;
402
+ let wave = preparedUnits.get(i)?.wave;
403
+ if (!wave) {
404
+ try {
405
+ const { entries, headerRewrites } = await preValidateNativeBatch(parsed, basePath);
406
+ wave = { parsed, entries, headerRewrites };
407
+ } catch (err) {
408
+ failed = { displayPath: unit.displayPath, error: `Error: ${err?.message || String(err)}` };
409
+ failedIndex = i;
410
+ continue;
411
+ }
293
412
  }
294
413
  const res = await applyParsedWave(wave, basePath, waveOpts);
295
414
  backend = res.backend;
@@ -473,7 +592,11 @@ async function apply_patch(args, cwd, options = {}) {
473
592
  let v4aRenamePlan = null;
474
593
  if (isV4APatchInput(patchStr, requestedFormat)) {
475
594
  try {
476
- const allSections = parseV4APatch(patchStr);
595
+ const allSections = rewriteV4AReadRedirects(
596
+ parseV4APatch(patchStr),
597
+ basePath,
598
+ readStateScope,
599
+ );
477
600
  v4aRenamePlan = await planV4ARenameSections(allSections, basePath);
478
601
  inputPatchStr = await convertV4ASectionsToUnifiedPatch(v4aRenamePlan.remainingSections, basePath, v4aConvertOpts);
479
602
  if (v4aRenamePlan.renameSections.length > 0) {
@@ -486,7 +609,12 @@ async function apply_patch(args, cwd, options = {}) {
486
609
  }
487
610
  } else if (requestedFormat !== 'unified' && hasUnifiedBareV4AHunk(patchStr)) {
488
611
  try {
489
- inputPatchStr = await convertUnifiedBareV4AToUnifiedPatch(patchStr, basePath, v4aConvertOpts);
612
+ const sections = rewriteV4AReadRedirects(
613
+ parseUnifiedBareV4APatch(patchStr),
614
+ basePath,
615
+ readStateScope,
616
+ );
617
+ inputPatchStr = await convertV4ASectionsToUnifiedPatch(sections, basePath, v4aConvertOpts);
490
618
  } catch (err) {
491
619
  throw new Error(`apply_patch: bare @@ parse failed — ${err?.message || String(err)}`);
492
620
  }
@@ -502,7 +630,12 @@ async function apply_patch(args, cwd, options = {}) {
502
630
  throw new Error(`apply_patch: parse failed — ${err?.message || String(err)}; prefer V4A envelope for multi-hunk edits (no @@ line counts)`);
503
631
  }
504
632
  try {
505
- inputPatchStr = await convertUnifiedCountedToUnifiedPatchViaV4A(patchStr, basePath, v4aConvertOpts);
633
+ const sections = rewriteV4AReadRedirects(
634
+ parseUnifiedCountedAsV4APatch(patchStr),
635
+ basePath,
636
+ readStateScope,
637
+ );
638
+ inputPatchStr = await convertV4ASectionsToUnifiedPatch(sections, basePath, v4aConvertOpts);
506
639
  normalizedPatchStr = prepareInput(inputPatchStr);
507
640
  parsed = parsePatch(normalizedPatchStr);
508
641
  mutationPlan = {
@@ -514,6 +647,9 @@ async function apply_patch(args, cwd, options = {}) {
514
647
  throw new Error(`apply_patch: parse failed — ${err?.message || String(err)}; V4A fallback failed — ${fallbackErr?.message || String(fallbackErr)}`);
515
648
  }
516
649
  }
650
+ if (!v4aRenameOnly) {
651
+ parsed = rewriteParsedReadRedirects(parsed, basePath, readStateScope);
652
+ }
517
653
  if (!v4aRenameOnly && (!Array.isArray(parsed) || parsed.length === 0)) {
518
654
  return 'Error: patch contained no file sections';
519
655
  }
@@ -560,8 +696,7 @@ async function apply_patch(args, cwd, options = {}) {
560
696
  }),
561
697
  ];
562
698
 
563
- return withBuiltinPathLocks(_lockPaths, () =>
564
- withAdvisoryLocks(_lockPaths, async () => {
699
+ const runLegacyBulkBatch = async () => {
565
700
  let v4aRenameResults = [];
566
701
  if (v4aRenamePlan?.renameSections?.length) {
567
702
  v4aRenameResults = await applyV4ARenameSections(v4aRenamePlan.renameSections, basePath, v4aConvertOpts);
@@ -629,7 +764,44 @@ async function apply_patch(args, cwd, options = {}) {
629
764
  return wrapPatchMutationOutput(`${combined}\n${tail.join('\n')}`, mutationPlan, { backend });
630
765
  }
631
766
  return wrapPatchMutationOutput(combined, mutationPlan, { backend });
632
- }));
767
+ };
768
+
769
+ return withBuiltinPathLocks(_lockPaths, () =>
770
+ withAdvisoryLocks(_lockPaths, async () => {
771
+ // Legacy bulk/atomic mode applies every wave in one shot, and a wave that
772
+ // mixes native (in-base) with JS (out-of-base) entries commits the native
773
+ // writes before the JS entries run. Snapshot every touched path up front
774
+ // and restore it whenever the batch fails — by returned Error text OR by
775
+ // a thrown error (V4A rename, persistence) — so mode:"atomic" really is
776
+ // all-or-nothing instead of leaving an earlier commit in place.
777
+ let rollbackSnapshots = [];
778
+ if (!dryRun) {
779
+ try {
780
+ rollbackSnapshots = capturePatchRollbackState(_lockPaths);
781
+ } catch (err) {
782
+ return `Error: ${err?.message || String(err)}`;
783
+ }
784
+ }
785
+ // Restoration errors are never swallowed: an incomplete rollback is
786
+ // reported verbatim so the caller never reads a false all-or-nothing.
787
+ const withRollback = (outcome) => {
788
+ const rollbackErrors = restorePatchRollbackState(rollbackSnapshots, readStateScope);
789
+ return rollbackErrors.length === 0
790
+ ? `${outcome}\n--- rolled back: every touched path was restored to its pre-patch state ---`
791
+ : [outcome, '--- rollback incomplete ---', ...rollbackErrors].join('\n');
792
+ };
793
+ let outcome;
794
+ try {
795
+ outcome = await runLegacyBulkBatch();
796
+ } catch (err) {
797
+ // A thrown failure (e.g. V4A rename) took the same path to disk as a
798
+ // returned one, so it takes the same path back out.
799
+ if (dryRun) throw err;
800
+ return withRollback(`Error: ${err?.message || String(err)}`);
801
+ }
802
+ if (dryRun || !isPatchErrorText(outcome)) return outcome;
803
+ return withRollback(outcome);
804
+ }));
633
805
  }
634
806
 
635
807
  export async function executePatchTool(name, args, cwd, options = {}) {
@@ -24,6 +24,16 @@ export function stripDiffPrefix(name) {
24
24
  }
25
25
 
26
26
  export function resolveEntryPath(basePath, rawName) {
27
+ // A parsed entry without a header path (jsdiff parses arbitrary non-diff
28
+ // text into a headerless entry) must fail as a patch-format error, not as a
29
+ // raw `The "path" argument must be of type string` TypeError from node:path.
30
+ if (rawName == null || rawName === '') {
31
+ throw new Error(
32
+ 'apply_patch: a file section header could not be parsed (no target path) — the patch body is not a valid diff. '
33
+ + 'Each section must start with `*** Update File: <path>` / `*** Add File: <path>` / `*** Delete File: <path>` '
34
+ + '(V4A, wrapped in `*** Begin Patch` / `*** End Patch`), or a `--- a/<path>` + `+++ b/<path>` pair (unified).',
35
+ );
36
+ }
27
37
  const stripped = stripDiffPrefix(rawName);
28
38
  const norm = normalizeInputPath(stripped);
29
39
  return isAbsolute(norm) ? pathResolve(norm) : resolveAgainstCwd(norm, basePath);
@@ -156,6 +166,18 @@ function unifiedRange(start, lines) {
156
166
  return `${s},${n}`;
157
167
  }
158
168
 
169
+ // jsdiff's parser normalizes the unified-diff "zero count" quirk by bumping the
170
+ // start by one (`parse.js`: `if (hunk.oldLines === 0) hunk.oldStart += 1`), so a
171
+ // re-render of a parsed hunk must undo it. Without this, an insert-only hunk
172
+ // `@@ -N,0 @@` reaches the native engine as `-(N+1),0` and is anchored one line
173
+ // too late — or rejected outright when N is already the last line (the engine
174
+ // treats N as a 0-based "insert after line N" skip count, valid 0..=line_count).
175
+ function unifiedRangeFromParsed(start, lines) {
176
+ const n = Math.max(0, Number(lines) || 0);
177
+ const s = Math.max(0, Number(start) || 0);
178
+ return unifiedRange(n === 0 ? Math.max(0, s - 1) : s, n);
179
+ }
180
+
159
181
  export function renderParsedUnifiedPatch(parsed) {
160
182
  const out = [];
161
183
  for (const entry of parsed || []) {
@@ -163,7 +185,7 @@ export function renderParsedUnifiedPatch(parsed) {
163
185
  out.push(`+++ ${entry.newFileName || '/dev/null'}`);
164
186
  for (const hunk of entry.hunks || []) {
165
187
  const section = hunk.section ? ` ${hunk.section}` : '';
166
- out.push(`@@ -${unifiedRange(hunk.oldStart, hunk.oldLines)} +${unifiedRange(hunk.newStart, hunk.newLines)} @@${section}`);
188
+ out.push(`@@ -${unifiedRangeFromParsed(hunk.oldStart, hunk.oldLines)} +${unifiedRangeFromParsed(hunk.newStart, hunk.newLines)} @@${section}`);
167
189
  for (const line of hunk.lines || []) out.push(line);
168
190
  }
169
191
  }
@@ -34,6 +34,7 @@ import {
34
34
  formatPatchSourceExcerpt,
35
35
  compactPatchPreviewLine,
36
36
  decodeValidUtf8OrNull,
37
+ assertSafeReplacementPlan,
37
38
  } from './matcher.mjs';
38
39
 
39
40
  function v4AHunkLineStats(hunk) {
@@ -154,6 +155,15 @@ function cloneTextLinesForPatch(sourceLines) {
154
155
  return lines;
155
156
  }
156
157
 
158
+ // Codex parity (codex-rs/apply-patch/src/lib.rs compute_replacements): a chunk
159
+ // with no old lines is a pure addition and is inserted at end-of-file — before
160
+ // the trailing empty line when the file ends with a blank line — never at the
161
+ // current anchor cursor.
162
+ function eofInsertionIndex(sourceLines) {
163
+ const len = (sourceLines || []).length;
164
+ return len > 0 && sourceLines[len - 1] === '' ? len - 1 : len;
165
+ }
166
+
157
167
  function resolveV4AHunkPosition(sourceLines, hunk, nextSearchLine, options = {}) {
158
168
  const stats = v4AHunkLineStats(hunk);
159
169
  if (stats.oldCount === 0 && stats.newCount === 0) return { skip: true };
@@ -170,7 +180,7 @@ function resolveV4AHunkPosition(sourceLines, hunk, nextSearchLine, options = {})
170
180
  let trimmedTrailing = 0;
171
181
  let trimmedTrailingNew = 0;
172
182
  if (stats.oldCount === 0) {
173
- oldStartIdx = eof ? sourceLines.length : anchorLine;
183
+ oldStartIdx = eofInsertionIndex(sourceLines);
174
184
  } else {
175
185
  const searchFrom = Math.max(0, anchorLine - 1);
176
186
  oldStartIdx = findLineSequence(
@@ -180,20 +190,32 @@ function resolveV4AHunkPosition(sourceLines, hunk, nextSearchLine, options = {})
180
190
  searchFrom,
181
191
  { fuzzy, eof },
182
192
  );
183
- if (eof && oldStartIdx < 0 && oldLinesPattern.length > 0 && oldLinesPattern[oldLinesPattern.length - 1] === '') {
193
+ // Codex parity (compute_replacements): when the first seek fails and the
194
+ // pattern's last line is the empty string that stands in for the region's
195
+ // terminating newline, retry without it. Codex runs this retry for EVERY
196
+ // chunk, not only `*** End of File` ones, so a hunk whose trailing blank
197
+ // context line sits at end-of-file resolves instead of reporting a context
198
+ // miss. When the retry empties the pattern (the old side was that single
199
+ // blank line) Codex's seek returns the cursor, i.e. the hunk becomes a
200
+ // zero-length insertion at the anchor cursor; `anchorLine` IS that cursor
201
+ // (Codex `line_index`), so use it directly instead of the seek's
202
+ // preferred-line fallback.
203
+ if (oldStartIdx < 0 && oldLinesPattern.length > 0 && oldLinesPattern[oldLinesPattern.length - 1] === '') {
184
204
  oldLinesPattern = oldLinesPattern.slice(0, -1);
185
205
  trimmedTrailing = 1;
186
206
  if (newLinesPattern.length > 0 && newLinesPattern[newLinesPattern.length - 1] === '') {
187
207
  newLinesPattern = newLinesPattern.slice(0, -1);
188
208
  trimmedTrailingNew = 1;
189
209
  }
190
- oldStartIdx = findLineSequence(
191
- sourceLines,
192
- oldLinesPattern,
193
- searchFrom,
194
- searchFrom,
195
- { fuzzy, eof },
196
- );
210
+ oldStartIdx = oldLinesPattern.length === 0
211
+ ? anchorLine
212
+ : findLineSequence(
213
+ sourceLines,
214
+ oldLinesPattern,
215
+ searchFrom,
216
+ searchFrom,
217
+ { fuzzy, eof },
218
+ );
197
219
  }
198
220
  }
199
221
  // Escape-equivalence fallback (fuzzy, non-EOF only): accept a window where each old
@@ -229,7 +251,11 @@ function resolveV4AHunkPosition(sourceLines, hunk, nextSearchLine, options = {})
229
251
  oldStartIdx,
230
252
  matchLen,
231
253
  newLines: newLinesPattern,
232
- nextSearchLine: oldStartIdx + Math.max(1, matchLen),
254
+ // Codex parity: a zero-length replacement never advances the cursor
255
+ // (compute_replacements `continue`s on the pure-addition branch), so a
256
+ // following hunk can still resolve against lines before the insertion
257
+ // point instead of being pushed past end-of-file.
258
+ nextSearchLine: matchLen === 0 ? Math.max(0, nextSearchLine || 0) : oldStartIdx + matchLen,
233
259
  trimmedTrailing,
234
260
  trimmedTrailingNew,
235
261
  };
@@ -251,6 +277,13 @@ export function applyV4AHunksToLines(sourceLines, hunks, options = {}) {
251
277
  });
252
278
  nextSearchLine = loc.nextSearchLine;
253
279
  }
280
+ // Codex parity (compute_replacements sorts by start index before
281
+ // apply_replacements walks them in reverse): descending application is only
282
+ // position-safe when the list is ordered by start index. Resolution order is
283
+ // normally already ascending; the sort is stable, so equal starts keep their
284
+ // resolution order.
285
+ replacements.sort((a, b) => a.oldStartIdx - b.oldStartIdx);
286
+ assertSafeReplacementPlan(replacements.map((rep) => ({ start: rep.oldStartIdx, oldLen: rep.oldLen })));
254
287
  for (const rep of replacements.reverse()) {
255
288
  lines.splice(rep.oldStartIdx, rep.oldLen, ...rep.newLines);
256
289
  }
@@ -628,6 +661,12 @@ export async function convertV4ASectionsToUnifiedPatch(sections, basePath, optio
628
661
  throw new Error(`V4A update target unreadable: ${section.path} (${err?.code || err?.message || String(err)}).`);
629
662
  }
630
663
  const sectionHunks = [];
664
+ // Resolved-position ordering for the emitted unified hunks: the unique-
665
+ // verbatim fallback in findLineSequence can resolve a later-listed hunk
666
+ // BEFORE an earlier one, and the native engine applies unified hunks with
667
+ // a forward-only cursor — emission must therefore be sorted by resolved
668
+ // start (stable on ties) instead of resolution order.
669
+ const sectionHunkEntries = [];
631
670
  const orderedHunks = orderV4AHunksByFilePosition(sourceLines, section.hunks, fuzzy);
632
671
  let nextSearchLine = 0;
633
672
  for (const hunk of orderedHunks) {
@@ -641,12 +680,7 @@ export async function convertV4ASectionsToUnifiedPatch(sections, basePath, optio
641
680
  if (rejectedHunks) rejectedHunks.push({ file: section.path, hunk, reason: msg });
642
681
  continue;
643
682
  }
644
- const oldStart = stats.oldCount === 0 ? loc.oldStartIdx : loc.oldStartIdx + 1;
645
- const newStart = oldStart;
646
683
  const tail = (hunk.anchors || []).filter(Boolean).join(' ');
647
- const oldCount = stats.oldCount === 0 ? 0 : loc.matchLen;
648
- const newCount = stats.newCount - (loc.trimmedTrailingNew || 0);
649
- sectionHunks.push(`@@ -${oldStart},${oldCount} +${newStart},${newCount} @@${tail ? ` ${tail}` : ''}`);
650
684
  let dropOldAt = -1;
651
685
  let dropNewAt = -1;
652
686
  if (loc.trimmedTrailing) {
@@ -659,6 +693,13 @@ export async function convertV4ASectionsToUnifiedPatch(sections, basePath, optio
659
693
  if (dropOldAt >= 0 && (!loc.trimmedTrailingNew || dropNewAt >= 0)) break;
660
694
  }
661
695
  }
696
+ // Emit the body FIRST and derive the header counts from what was
697
+ // actually emitted: the trailing-newline sentinel drop can remove a line
698
+ // from one side only, so a header computed from the raw hunk stats can
699
+ // disagree with the body and make the emitted unified patch unparseable.
700
+ const bodyLines = [];
701
+ let emittedOld = 0;
702
+ let emittedNew = 0;
662
703
  let srcIdx = loc.oldStartIdx;
663
704
  const srcEnd = loc.oldStartIdx + loc.matchLen;
664
705
  for (let i = 0; i < hunk.lines.length; i++) {
@@ -666,20 +707,47 @@ export async function convertV4ASectionsToUnifiedPatch(sections, basePath, optio
666
707
  if (isV4AEndOfFileMarker(line)) continue;
667
708
  const prefix = line[0];
668
709
  if (prefix === ' ' || prefix === '-') {
669
- if (i === dropOldAt || i === dropNewAt) continue;
710
+ if (i === dropOldAt) {
711
+ // The sentinel line leaves the OLD side. When it is a context line
712
+ // that the new side still keeps (Codex only trims the new slice
713
+ // when it ends with the sentinel), it survives as an addition.
714
+ if (prefix === ' ' && i !== dropNewAt) {
715
+ bodyLines.push(`+${line.slice(1)}`);
716
+ emittedNew++;
717
+ }
718
+ continue;
719
+ }
720
+ if (i === dropNewAt) continue;
670
721
  if (srcIdx < srcEnd && srcIdx < sourceLines.length) {
671
- sectionHunks.push(prefix + sourceLines[srcIdx]);
722
+ bodyLines.push(prefix + sourceLines[srcIdx]);
672
723
  } else {
673
- sectionHunks.push(line);
724
+ bodyLines.push(line);
674
725
  }
726
+ emittedOld++;
727
+ if (prefix === ' ') emittedNew++;
675
728
  srcIdx++;
676
729
  } else {
677
730
  if (i === dropNewAt) continue;
678
- sectionHunks.push(line);
731
+ bodyLines.push(line);
732
+ emittedNew++;
679
733
  }
680
734
  }
735
+ // A zero-length old range is an insertion: its header start is the 0-based
736
+ // insertion index (`-N,0` inserts after source line N), while a real
737
+ // replacement starts at the 1-based first replaced line.
738
+ const oldStart = emittedOld === 0 ? loc.oldStartIdx : loc.oldStartIdx + 1;
739
+ sectionHunkEntries.push({
740
+ start: loc.oldStartIdx,
741
+ order: sectionHunkEntries.length,
742
+ lines: [
743
+ `@@ -${oldStart},${emittedOld} +${oldStart},${emittedNew} @@${tail ? ` ${tail}` : ''}`,
744
+ ...bodyLines,
745
+ ],
746
+ });
681
747
  nextSearchLine = loc.nextSearchLine;
682
748
  }
749
+ sectionHunkEntries.sort((a, b) => (a.start - b.start) || (a.order - b.order));
750
+ for (const entry of sectionHunkEntries) sectionHunks.push(...entry.lines);
683
751
  if (sectionHunks.length > 0) {
684
752
  out.push(`--- a/${displayPath}`);
685
753
  out.push(`+++ b/${displayPath}`);
@@ -1,26 +1,26 @@
1
1
  {
2
- "version": "0.7.21",
2
+ "version": "0.7.22",
3
3
  "_comment": "Rewritten by .github/workflows/patch-release.yml on each tagged release. assets maps platformKey (process.platform-process.arch, e.g. win32-x64, linux-x64, darwin-arm64) to { url, sha256 } of the mixdog-patch binary on the GitHub release. A local cargo build under native/mixdog-patch/target/release always takes precedence; otherwise the binary is fetched per this manifest into the data dir (apply is native-only — no JS apply engine).",
4
4
  "assets": {
5
5
  "darwin-arm64": {
6
- "url": "https://github.com/tribgames/mixdog/releases/download/patch-v0.7.21/mixdog-patch-darwin-arm64",
7
- "sha256": "f52db5c65ffdc28ace93a6dd54d7d1c8e76f3daba45a74e63676513fcee4b51b"
6
+ "url": "https://github.com/tribgames/mixdog/releases/download/patch-v0.7.22/mixdog-patch-darwin-arm64",
7
+ "sha256": "6990ee4442aa4f5ec4c07926b2f22e8fcbc17b668a1be005d71551bfcf61fc73"
8
8
  },
9
9
  "darwin-x64": {
10
- "url": "https://github.com/tribgames/mixdog/releases/download/patch-v0.7.21/mixdog-patch-darwin-x64",
11
- "sha256": "024bacaaead16a368fa4a2a148f66f3892ee03be78803c40dda9acd59f3dd2c1"
10
+ "url": "https://github.com/tribgames/mixdog/releases/download/patch-v0.7.22/mixdog-patch-darwin-x64",
11
+ "sha256": "0118298483df38af7e0463bede8412c2267c8df96b586ce98120c9139ad22977"
12
12
  },
13
13
  "linux-arm64": {
14
- "url": "https://github.com/tribgames/mixdog/releases/download/patch-v0.7.21/mixdog-patch-linux-arm64",
15
- "sha256": "c197931e005fb89211b60b66008bc0c3b42200393483e0ae4a251e2111a06d44"
14
+ "url": "https://github.com/tribgames/mixdog/releases/download/patch-v0.7.22/mixdog-patch-linux-arm64",
15
+ "sha256": "91330967bf089b943e6175f9ade45fbac2d4e6d4449c9ea28853c1acf0f472db"
16
16
  },
17
17
  "linux-x64": {
18
- "url": "https://github.com/tribgames/mixdog/releases/download/patch-v0.7.21/mixdog-patch-linux-x64",
19
- "sha256": "8d958f9315ec82a17fa2e21560630f66a4e55a8a56ad92d69ba9f3f5ff61c15c"
18
+ "url": "https://github.com/tribgames/mixdog/releases/download/patch-v0.7.22/mixdog-patch-linux-x64",
19
+ "sha256": "399e7c68a42f6e15fca28270ea3e216b8d34e48b53702bbefa2354cf0cc956f5"
20
20
  },
21
21
  "win32-x64": {
22
- "url": "https://github.com/tribgames/mixdog/releases/download/patch-v0.7.21/mixdog-patch-win32-x64.exe",
23
- "sha256": "cc927c7bc6f09ecae591c058d0053cd5bc65ee17c56c9bce6871487be1d2baa2"
22
+ "url": "https://github.com/tribgames/mixdog/releases/download/patch-v0.7.22/mixdog-patch-win32-x64.exe",
23
+ "sha256": "13e2373bceb6c6f9cafe7b815b5d20bffcb20414d8c9dfbb287dd7fd110a811e"
24
24
  }
25
25
  }
26
26
  }