mixdog 0.7.17 → 0.8.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 (836) hide show
  1. package/README.md +37 -331
  2. package/package.json +67 -99
  3. package/scripts/boot-smoke.mjs +94 -0
  4. package/scripts/build-tui.mjs +52 -0
  5. package/scripts/compact-smoke.mjs +199 -0
  6. package/scripts/lead-workflow-smoke.mjs +598 -0
  7. package/scripts/live-worker-smoke.mjs +239 -0
  8. package/scripts/output-style-smoke.mjs +101 -0
  9. package/scripts/smoke-loop-report.mjs +221 -0
  10. package/scripts/smoke-loop.mjs +201 -0
  11. package/scripts/smoke.mjs +113 -0
  12. package/scripts/tool-failures.mjs +143 -0
  13. package/scripts/tool-smoke.mjs +456 -0
  14. package/src/agents/debugger/AGENT.md +3 -0
  15. package/src/agents/debugger/agent.json +6 -0
  16. package/src/agents/explore/AGENT.md +4 -0
  17. package/src/agents/explore/agent.json +6 -0
  18. package/src/agents/heavy-worker/AGENT.md +3 -0
  19. package/src/agents/heavy-worker/agent.json +6 -0
  20. package/src/agents/maintainer/AGENT.md +3 -0
  21. package/src/agents/maintainer/agent.json +6 -0
  22. package/src/agents/reviewer/AGENT.md +3 -0
  23. package/src/agents/reviewer/agent.json +6 -0
  24. package/src/agents/scheduler-task.md +3 -0
  25. package/src/agents/web-researcher/AGENT.md +3 -0
  26. package/src/agents/web-researcher/agent.json +6 -0
  27. package/src/agents/webhook-handler.md +3 -0
  28. package/src/agents/worker/AGENT.md +3 -0
  29. package/src/agents/worker/agent.json +6 -0
  30. package/src/app.mjs +90 -0
  31. package/src/cli.mjs +11 -0
  32. package/src/defaults/hidden-roles.json +72 -0
  33. package/src/defaults/mixdog-config.template.json +15 -0
  34. package/src/hooks/lib/permission-evaluator.cjs +488 -0
  35. package/src/hooks/lib/settings-loader.cjs +112 -0
  36. package/src/lib/keychain-cjs.cjs +332 -0
  37. package/src/lib/plugin-paths.cjs +28 -0
  38. package/src/lib/rules-builder.cjs +315 -0
  39. package/src/mixdog-session-runtime.mjs +3704 -0
  40. package/src/output-styles/default.md +38 -0
  41. package/src/output-styles/extreme-simple.md +17 -0
  42. package/src/output-styles/simple.md +17 -0
  43. package/src/repl.mjs +322 -0
  44. package/src/rules/bridge/00-common.md +5 -0
  45. package/src/rules/bridge/20-skip-protocol.md +11 -0
  46. package/src/rules/bridge/30-explorer.md +4 -0
  47. package/src/rules/bridge/40-cycle1-agent.md +28 -0
  48. package/src/rules/bridge/41-cycle2-agent.md +59 -0
  49. package/src/rules/lead/00-tool-lead.md +5 -0
  50. package/src/rules/lead/01-general.md +5 -0
  51. package/src/rules/lead/02-channels.md +3 -0
  52. package/src/rules/lead/04-workflow.md +12 -0
  53. package/src/rules/shared/00-language.md +3 -0
  54. package/src/rules/shared/01-tool.md +3 -0
  55. package/src/runtime/agent/orchestrator/bridge-trace.mjs +814 -0
  56. package/src/runtime/agent/orchestrator/cache-mtime.mjs +60 -0
  57. package/src/runtime/agent/orchestrator/config.mjs +446 -0
  58. package/src/runtime/agent/orchestrator/context/collect.mjs +796 -0
  59. package/src/runtime/agent/orchestrator/dispatch-persist.mjs +417 -0
  60. package/src/runtime/agent/orchestrator/internal-roles.mjs +188 -0
  61. package/src/runtime/agent/orchestrator/internal-tools.mjs +88 -0
  62. package/src/runtime/agent/orchestrator/mcp/client.mjs +345 -0
  63. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +2104 -0
  64. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +784 -0
  65. package/src/runtime/agent/orchestrator/providers/api-usage.mjs +341 -0
  66. package/src/runtime/agent/orchestrator/providers/gemini.mjs +1679 -0
  67. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +959 -0
  68. package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +213 -0
  69. package/src/runtime/agent/orchestrator/providers/model-cache.mjs +38 -0
  70. package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +471 -0
  71. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +615 -0
  72. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +808 -0
  73. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +1719 -0
  74. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +2587 -0
  75. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +1953 -0
  76. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +136 -0
  77. package/src/runtime/agent/orchestrator/providers/opencode-go-usage.mjs +317 -0
  78. package/src/runtime/agent/orchestrator/providers/opencode-go.mjs +109 -0
  79. package/src/runtime/agent/orchestrator/providers/registry.mjs +247 -0
  80. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +332 -0
  81. package/src/runtime/agent/orchestrator/providers/statusline-route-meta.mjs +11 -0
  82. package/src/runtime/agent/orchestrator/providers/trace-utils.mjs +50 -0
  83. package/src/runtime/agent/orchestrator/session/cache/prefetch-cache.mjs +142 -0
  84. package/src/runtime/agent/orchestrator/session/cache/read-cache.mjs +318 -0
  85. package/src/runtime/agent/orchestrator/session/cache/scoped-cache.mjs +367 -0
  86. package/src/runtime/agent/orchestrator/session/compact.mjs +882 -0
  87. package/src/runtime/agent/orchestrator/session/context-utils.mjs +233 -0
  88. package/src/runtime/agent/orchestrator/session/loop.mjs +2320 -0
  89. package/src/runtime/agent/orchestrator/session/manager.mjs +2960 -0
  90. package/src/runtime/agent/orchestrator/session/result-classification.mjs +65 -0
  91. package/src/runtime/agent/orchestrator/session/store.mjs +663 -0
  92. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +166 -0
  93. package/src/runtime/agent/orchestrator/smart-bridge/bridge-llm.mjs +339 -0
  94. package/src/runtime/agent/orchestrator/smart-bridge/cache-strategy.mjs +419 -0
  95. package/src/runtime/agent/orchestrator/stall-policy.mjs +227 -0
  96. package/src/runtime/agent/orchestrator/tool-loop-guard.mjs +235 -0
  97. package/src/runtime/agent/orchestrator/tools/bash-session.mjs +723 -0
  98. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +389 -0
  99. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +637 -0
  100. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +165 -0
  101. package/src/runtime/agent/orchestrator/tools/builtin/fuzzy-match.mjs +104 -0
  102. package/src/runtime/agent/orchestrator/tools/builtin/glob-walk.mjs +194 -0
  103. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +596 -0
  104. package/src/runtime/agent/orchestrator/tools/builtin/native-edit-runner.mjs +110 -0
  105. package/src/runtime/agent/orchestrator/tools/builtin/path-diagnostics.mjs +153 -0
  106. package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +118 -0
  107. package/src/runtime/agent/orchestrator/tools/builtin/read-open.mjs +189 -0
  108. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +731 -0
  109. package/src/runtime/agent/orchestrator/tools/builtin/read-snapshot-runtime.mjs +168 -0
  110. package/src/runtime/agent/orchestrator/tools/builtin/read-streaming.mjs +602 -0
  111. package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +465 -0
  112. package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +160 -0
  113. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +982 -0
  114. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +1087 -0
  115. package/src/runtime/agent/orchestrator/tools/builtin/shell-runtime.mjs +231 -0
  116. package/src/runtime/agent/orchestrator/tools/builtin/snapshot-store.mjs +223 -0
  117. package/src/runtime/agent/orchestrator/tools/builtin.mjs +478 -0
  118. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +24 -0
  119. package/src/runtime/agent/orchestrator/tools/code-graph.mjs +4102 -0
  120. package/src/runtime/agent/orchestrator/tools/destructive-warning.mjs +323 -0
  121. package/src/runtime/agent/orchestrator/tools/graph-binary-fetcher.mjs +154 -0
  122. package/src/runtime/agent/orchestrator/tools/graph-manifest.json +26 -0
  123. package/src/runtime/agent/orchestrator/tools/patch-binary-fetcher.mjs +143 -0
  124. package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +18 -0
  125. package/src/runtime/agent/orchestrator/tools/patch.mjs +2772 -0
  126. package/src/runtime/agent/orchestrator/tools/progress-message.mjs +114 -0
  127. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +880 -0
  128. package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +312 -0
  129. package/src/runtime/channels/backends/discord.mjs +781 -0
  130. package/src/runtime/channels/data/voice-runtime-manifest.json +138 -0
  131. package/src/runtime/channels/index.mjs +3309 -0
  132. package/src/runtime/channels/lib/config.mjs +285 -0
  133. package/src/runtime/channels/lib/drop-trace.mjs +71 -0
  134. package/src/runtime/channels/lib/event-pipeline.mjs +81 -0
  135. package/src/runtime/channels/lib/holidays.mjs +138 -0
  136. package/src/runtime/channels/lib/hook-pipe-server.mjs +671 -0
  137. package/src/runtime/channels/lib/output-forwarder.mjs +765 -0
  138. package/src/runtime/channels/lib/runtime-paths.mjs +497 -0
  139. package/src/runtime/channels/lib/scheduler.mjs +710 -0
  140. package/src/runtime/channels/lib/session-discovery.mjs +102 -0
  141. package/src/runtime/channels/lib/state-file.mjs +68 -0
  142. package/src/runtime/channels/lib/status-snapshot.mjs +224 -0
  143. package/src/runtime/channels/lib/tool-format.mjs +124 -0
  144. package/src/runtime/channels/lib/transcript-discovery.mjs +195 -0
  145. package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +734 -0
  146. package/src/runtime/channels/lib/webhook.mjs +1288 -0
  147. package/src/runtime/channels/tool-defs.mjs +177 -0
  148. package/src/runtime/lib/keychain-cjs.cjs +289 -0
  149. package/src/runtime/memory/data/runtime-manifest.json +40 -0
  150. package/src/runtime/memory/index.mjs +3600 -0
  151. package/src/runtime/memory/lib/core-memory-store.mjs +336 -0
  152. package/src/runtime/memory/lib/embedding-provider.mjs +275 -0
  153. package/src/runtime/memory/lib/embedding-worker.mjs +331 -0
  154. package/src/runtime/memory/lib/memory-cycle-requests.mjs +276 -0
  155. package/src/runtime/memory/lib/memory-cycle1.mjs +783 -0
  156. package/src/runtime/memory/lib/memory-cycle2.mjs +1389 -0
  157. package/src/runtime/memory/lib/memory-cycle3.mjs +646 -0
  158. package/src/runtime/memory/lib/memory-embed.mjs +300 -0
  159. package/src/runtime/memory/lib/memory-ops-policy.mjs +149 -0
  160. package/src/runtime/memory/lib/memory-recall-store.mjs +644 -0
  161. package/src/runtime/memory/lib/memory.mjs +418 -0
  162. package/src/runtime/memory/lib/pg/adapter.mjs +314 -0
  163. package/src/runtime/memory/lib/pg/process.mjs +366 -0
  164. package/src/runtime/memory/lib/pg/supervisor.mjs +495 -0
  165. package/src/runtime/memory/lib/runtime-fetcher.mjs +464 -0
  166. package/src/runtime/memory/lib/trace-store.mjs +734 -0
  167. package/src/runtime/memory/tool-defs.mjs +79 -0
  168. package/src/runtime/search/index.mjs +925 -0
  169. package/src/runtime/search/lib/config.mjs +61 -0
  170. package/src/runtime/search/lib/web-tools.mjs +1278 -0
  171. package/src/runtime/search/tool-defs.mjs +64 -0
  172. package/src/runtime/shared/atomic-file.mjs +435 -0
  173. package/src/runtime/shared/background-tasks.mjs +376 -0
  174. package/src/runtime/shared/child-guardian.mjs +98 -0
  175. package/src/runtime/shared/config.mjs +393 -0
  176. package/src/runtime/shared/err-text.mjs +121 -0
  177. package/src/runtime/shared/launcher-control.mjs +259 -0
  178. package/src/runtime/shared/llm/cost.mjs +66 -0
  179. package/src/runtime/shared/llm/http-agent.mjs +129 -0
  180. package/src/runtime/shared/open-url.mjs +37 -0
  181. package/src/runtime/shared/plugin-paths.mjs +25 -0
  182. package/src/runtime/shared/process-shutdown.mjs +147 -0
  183. package/src/runtime/shared/schedules-store.mjs +70 -0
  184. package/src/runtime/shared/tool-execution-contract.mjs +104 -0
  185. package/src/runtime/shared/tool-surface.mjs +950 -0
  186. package/src/runtime/shared/user-cwd.mjs +221 -0
  187. package/src/runtime/shared/user-data-guard.mjs +232 -0
  188. package/src/runtime/shared/workspace-router.mjs +259 -0
  189. package/src/standalone/bridge-tool.mjs +1414 -0
  190. package/src/standalone/channel-admin.mjs +366 -0
  191. package/src/standalone/channel-worker-preload.cjs +3 -0
  192. package/src/standalone/channel-worker.mjs +353 -0
  193. package/src/standalone/explore-tool.mjs +233 -0
  194. package/src/standalone/hook-bus.mjs +246 -0
  195. package/src/standalone/plugin-admin.mjs +247 -0
  196. package/src/standalone/provider-admin.mjs +338 -0
  197. package/src/standalone/seeds.mjs +94 -0
  198. package/src/standalone/usage-dashboard.mjs +510 -0
  199. package/src/tui/App.jsx +5438 -0
  200. package/src/tui/components/AnsiText.jsx +199 -0
  201. package/src/tui/components/ContextPanel.jsx +217 -0
  202. package/src/tui/components/Markdown.jsx +205 -0
  203. package/src/tui/components/MarkdownTable.jsx +204 -0
  204. package/src/tui/components/Message.jsx +103 -0
  205. package/src/tui/components/Picker.jsx +317 -0
  206. package/src/tui/components/PromptInput.jsx +584 -0
  207. package/src/tui/components/QueuedCommands.jsx +47 -0
  208. package/src/tui/components/SlashCommandPalette.jsx +114 -0
  209. package/src/tui/components/Spinner.jsx +317 -0
  210. package/src/tui/components/StatusLine.jsx +87 -0
  211. package/src/tui/components/TextEntryPanel.jsx +323 -0
  212. package/src/tui/components/ToolExecution.jsx +772 -0
  213. package/src/tui/components/TurnDone.jsx +78 -0
  214. package/src/tui/components/UsagePanel.jsx +331 -0
  215. package/src/tui/dist/index.mjs +12359 -0
  216. package/src/tui/engine.mjs +2410 -0
  217. package/src/tui/figures.mjs +50 -0
  218. package/src/tui/hooks/useEngine.mjs +16 -0
  219. package/src/tui/index.jsx +254 -0
  220. package/src/tui/input-editing.mjs +242 -0
  221. package/src/tui/markdown/format-token.mjs +194 -0
  222. package/src/tui/paste-attachments.mjs +198 -0
  223. package/src/tui/runtime/shared/process-shutdown.mjs +1 -0
  224. package/src/tui/spinner-verbs.mjs +45 -0
  225. package/src/tui/theme.mjs +67 -0
  226. package/src/tui/time-format.mjs +53 -0
  227. package/src/ui/ansi.mjs +115 -0
  228. package/src/ui/markdown.mjs +195 -0
  229. package/src/ui/statusline.mjs +730 -0
  230. package/src/ui/tool-card.mjs +101 -0
  231. package/src/vendor/statusline/bin/statusline-lib.mjs +805 -0
  232. package/src/vendor/statusline/bin/statusline-route.mjs +596 -0
  233. package/src/vendor/statusline/scripts/lib/gateway-settings.mjs +285 -0
  234. package/src/vendor/statusline/src/gateway/claude-current.mjs +320 -0
  235. package/src/vendor/statusline/src/gateway/route-meta.mjs +753 -0
  236. package/src/vendor/statusline/src/gateway/session-routes.mjs +244 -0
  237. package/src/workflows/default/WORKFLOW.md +7 -0
  238. package/src/workflows/default/workflow.json +14 -0
  239. package/vendor/ink/build/ansi-tokenizer.d.ts +38 -0
  240. package/vendor/ink/build/ansi-tokenizer.js +316 -0
  241. package/vendor/ink/build/ansi-tokenizer.js.map +1 -0
  242. package/vendor/ink/build/colorize.d.ts +3 -0
  243. package/vendor/ink/build/colorize.js +48 -0
  244. package/vendor/ink/build/colorize.js.map +1 -0
  245. package/vendor/ink/build/components/AccessibilityContext.d.ts +3 -0
  246. package/vendor/ink/build/components/AccessibilityContext.js +5 -0
  247. package/vendor/ink/build/components/AccessibilityContext.js.map +1 -0
  248. package/vendor/ink/build/components/AnimationContext.d.ts +9 -0
  249. package/vendor/ink/build/components/AnimationContext.js +13 -0
  250. package/vendor/ink/build/components/AnimationContext.js.map +1 -0
  251. package/vendor/ink/build/components/App.d.ts +24 -0
  252. package/vendor/ink/build/components/App.js +554 -0
  253. package/vendor/ink/build/components/App.js.map +1 -0
  254. package/vendor/ink/build/components/AppContext.d.ts +80 -0
  255. package/vendor/ink/build/components/AppContext.js +25 -0
  256. package/vendor/ink/build/components/AppContext.js.map +1 -0
  257. package/vendor/ink/build/components/BackgroundContext.d.ts +4 -0
  258. package/vendor/ink/build/components/BackgroundContext.js +3 -0
  259. package/vendor/ink/build/components/BackgroundContext.js.map +1 -0
  260. package/vendor/ink/build/components/Box.d.ts +130 -0
  261. package/vendor/ink/build/components/Box.js +34 -0
  262. package/vendor/ink/build/components/Box.js.map +1 -0
  263. package/vendor/ink/build/components/CursorContext.d.ts +11 -0
  264. package/vendor/ink/build/components/CursorContext.js +8 -0
  265. package/vendor/ink/build/components/CursorContext.js.map +1 -0
  266. package/vendor/ink/build/components/ErrorBoundary.d.ts +18 -0
  267. package/vendor/ink/build/components/ErrorBoundary.js +23 -0
  268. package/vendor/ink/build/components/ErrorBoundary.js.map +1 -0
  269. package/vendor/ink/build/components/ErrorOverview.d.ts +6 -0
  270. package/vendor/ink/build/components/ErrorOverview.js +90 -0
  271. package/vendor/ink/build/components/ErrorOverview.js.map +1 -0
  272. package/vendor/ink/build/components/FocusContext.d.ts +16 -0
  273. package/vendor/ink/build/components/FocusContext.js +17 -0
  274. package/vendor/ink/build/components/FocusContext.js.map +1 -0
  275. package/vendor/ink/build/components/Newline.d.ts +13 -0
  276. package/vendor/ink/build/components/Newline.js +8 -0
  277. package/vendor/ink/build/components/Newline.js.map +1 -0
  278. package/vendor/ink/build/components/Spacer.d.ts +7 -0
  279. package/vendor/ink/build/components/Spacer.js +11 -0
  280. package/vendor/ink/build/components/Spacer.js.map +1 -0
  281. package/vendor/ink/build/components/Static.d.ts +24 -0
  282. package/vendor/ink/build/components/Static.js +28 -0
  283. package/vendor/ink/build/components/Static.js.map +1 -0
  284. package/vendor/ink/build/components/StderrContext.d.ts +15 -0
  285. package/vendor/ink/build/components/StderrContext.js +13 -0
  286. package/vendor/ink/build/components/StderrContext.js.map +1 -0
  287. package/vendor/ink/build/components/StdinContext.d.ts +28 -0
  288. package/vendor/ink/build/components/StdinContext.js +20 -0
  289. package/vendor/ink/build/components/StdinContext.js.map +1 -0
  290. package/vendor/ink/build/components/StdoutContext.d.ts +15 -0
  291. package/vendor/ink/build/components/StdoutContext.js +13 -0
  292. package/vendor/ink/build/components/StdoutContext.js.map +1 -0
  293. package/vendor/ink/build/components/Text.d.ts +55 -0
  294. package/vendor/ink/build/components/Text.js +50 -0
  295. package/vendor/ink/build/components/Text.js.map +1 -0
  296. package/vendor/ink/build/components/Transform.d.ts +16 -0
  297. package/vendor/ink/build/components/Transform.js +15 -0
  298. package/vendor/ink/build/components/Transform.js.map +1 -0
  299. package/vendor/ink/build/cursor-helpers.d.ts +39 -0
  300. package/vendor/ink/build/cursor-helpers.js +62 -0
  301. package/vendor/ink/build/cursor-helpers.js.map +1 -0
  302. package/vendor/ink/build/devtools-window-polyfill.d.ts +1 -0
  303. package/vendor/ink/build/devtools-window-polyfill.js +68 -0
  304. package/vendor/ink/build/devtools-window-polyfill.js.map +1 -0
  305. package/vendor/ink/build/devtools.d.ts +1 -0
  306. package/vendor/ink/build/devtools.js +36 -0
  307. package/vendor/ink/build/devtools.js.map +1 -0
  308. package/vendor/ink/build/dom.d.ts +62 -0
  309. package/vendor/ink/build/dom.js +143 -0
  310. package/vendor/ink/build/dom.js.map +1 -0
  311. package/vendor/ink/build/get-max-width.d.ts +3 -0
  312. package/vendor/ink/build/get-max-width.js +10 -0
  313. package/vendor/ink/build/get-max-width.js.map +1 -0
  314. package/vendor/ink/build/hooks/use-animation.d.ts +49 -0
  315. package/vendor/ink/build/hooks/use-animation.js +87 -0
  316. package/vendor/ink/build/hooks/use-animation.js.map +1 -0
  317. package/vendor/ink/build/hooks/use-app.d.ts +5 -0
  318. package/vendor/ink/build/hooks/use-app.js +8 -0
  319. package/vendor/ink/build/hooks/use-app.js.map +1 -0
  320. package/vendor/ink/build/hooks/use-box-metrics.d.ts +59 -0
  321. package/vendor/ink/build/hooks/use-box-metrics.js +81 -0
  322. package/vendor/ink/build/hooks/use-box-metrics.js.map +1 -0
  323. package/vendor/ink/build/hooks/use-cursor.d.ts +12 -0
  324. package/vendor/ink/build/hooks/use-cursor.js +29 -0
  325. package/vendor/ink/build/hooks/use-cursor.js.map +1 -0
  326. package/vendor/ink/build/hooks/use-focus-manager.d.ts +43 -0
  327. package/vendor/ink/build/hooks/use-focus-manager.js +18 -0
  328. package/vendor/ink/build/hooks/use-focus-manager.js.map +1 -0
  329. package/vendor/ink/build/hooks/use-focus.d.ts +30 -0
  330. package/vendor/ink/build/hooks/use-focus.js +43 -0
  331. package/vendor/ink/build/hooks/use-focus.js.map +1 -0
  332. package/vendor/ink/build/hooks/use-input.d.ts +132 -0
  333. package/vendor/ink/build/hooks/use-input.js +126 -0
  334. package/vendor/ink/build/hooks/use-input.js.map +1 -0
  335. package/vendor/ink/build/hooks/use-is-screen-reader-enabled.d.ts +6 -0
  336. package/vendor/ink/build/hooks/use-is-screen-reader-enabled.js +12 -0
  337. package/vendor/ink/build/hooks/use-is-screen-reader-enabled.js.map +1 -0
  338. package/vendor/ink/build/hooks/use-paste.d.ts +35 -0
  339. package/vendor/ink/build/hooks/use-paste.js +62 -0
  340. package/vendor/ink/build/hooks/use-paste.js.map +1 -0
  341. package/vendor/ink/build/hooks/use-stderr.d.ts +5 -0
  342. package/vendor/ink/build/hooks/use-stderr.js +8 -0
  343. package/vendor/ink/build/hooks/use-stderr.js.map +1 -0
  344. package/vendor/ink/build/hooks/use-stdin.d.ts +7 -0
  345. package/vendor/ink/build/hooks/use-stdin.js +9 -0
  346. package/vendor/ink/build/hooks/use-stdin.js.map +1 -0
  347. package/vendor/ink/build/hooks/use-stdout.d.ts +5 -0
  348. package/vendor/ink/build/hooks/use-stdout.js +8 -0
  349. package/vendor/ink/build/hooks/use-stdout.js.map +1 -0
  350. package/vendor/ink/build/hooks/use-window-size.d.ts +18 -0
  351. package/vendor/ink/build/hooks/use-window-size.js +22 -0
  352. package/vendor/ink/build/hooks/use-window-size.js.map +1 -0
  353. package/vendor/ink/build/index.d.ts +42 -0
  354. package/vendor/ink/build/index.js +24 -0
  355. package/vendor/ink/build/index.js.map +1 -0
  356. package/vendor/ink/build/ink.d.ts +146 -0
  357. package/vendor/ink/build/ink.js +1022 -0
  358. package/vendor/ink/build/ink.js.map +1 -0
  359. package/vendor/ink/build/input-parser.d.ts +10 -0
  360. package/vendor/ink/build/input-parser.js +194 -0
  361. package/vendor/ink/build/input-parser.js.map +1 -0
  362. package/vendor/ink/build/instances.d.ts +3 -0
  363. package/vendor/ink/build/instances.js +8 -0
  364. package/vendor/ink/build/instances.js.map +1 -0
  365. package/vendor/ink/build/kitty-keyboard.d.ts +23 -0
  366. package/vendor/ink/build/kitty-keyboard.js +32 -0
  367. package/vendor/ink/build/kitty-keyboard.js.map +1 -0
  368. package/vendor/ink/build/log-update.d.ts +20 -0
  369. package/vendor/ink/build/log-update.js +261 -0
  370. package/vendor/ink/build/log-update.js.map +1 -0
  371. package/vendor/ink/build/measure-element.d.ts +20 -0
  372. package/vendor/ink/build/measure-element.js +13 -0
  373. package/vendor/ink/build/measure-element.js.map +1 -0
  374. package/vendor/ink/build/measure-text.d.ts +6 -0
  375. package/vendor/ink/build/measure-text.js +21 -0
  376. package/vendor/ink/build/measure-text.js.map +1 -0
  377. package/vendor/ink/build/output.d.ts +35 -0
  378. package/vendor/ink/build/output.js +328 -0
  379. package/vendor/ink/build/output.js.map +1 -0
  380. package/vendor/ink/build/parse-keypress.d.ts +20 -0
  381. package/vendor/ink/build/parse-keypress.js +495 -0
  382. package/vendor/ink/build/parse-keypress.js.map +1 -0
  383. package/vendor/ink/build/reconciler.d.ts +4 -0
  384. package/vendor/ink/build/reconciler.js +306 -0
  385. package/vendor/ink/build/reconciler.js.map +1 -0
  386. package/vendor/ink/build/render-background.d.ts +4 -0
  387. package/vendor/ink/build/render-background.js +25 -0
  388. package/vendor/ink/build/render-background.js.map +1 -0
  389. package/vendor/ink/build/render-border.d.ts +4 -0
  390. package/vendor/ink/build/render-border.js +84 -0
  391. package/vendor/ink/build/render-border.js.map +1 -0
  392. package/vendor/ink/build/render-node-to-output.d.ts +14 -0
  393. package/vendor/ink/build/render-node-to-output.js +162 -0
  394. package/vendor/ink/build/render-node-to-output.js.map +1 -0
  395. package/vendor/ink/build/render-to-string.d.ts +38 -0
  396. package/vendor/ink/build/render-to-string.js +116 -0
  397. package/vendor/ink/build/render-to-string.js.map +1 -0
  398. package/vendor/ink/build/render.d.ts +176 -0
  399. package/vendor/ink/build/render.js +71 -0
  400. package/vendor/ink/build/render.js.map +1 -0
  401. package/vendor/ink/build/renderer.d.ts +8 -0
  402. package/vendor/ink/build/renderer.js +64 -0
  403. package/vendor/ink/build/renderer.js.map +1 -0
  404. package/vendor/ink/build/sanitize-ansi.d.ts +2 -0
  405. package/vendor/ink/build/sanitize-ansi.js +27 -0
  406. package/vendor/ink/build/sanitize-ansi.js.map +1 -0
  407. package/vendor/ink/build/squash-text-nodes.d.ts +3 -0
  408. package/vendor/ink/build/squash-text-nodes.js +36 -0
  409. package/vendor/ink/build/squash-text-nodes.js.map +1 -0
  410. package/vendor/ink/build/styles.d.ts +302 -0
  411. package/vendor/ink/build/styles.js +303 -0
  412. package/vendor/ink/build/styles.js.map +1 -0
  413. package/vendor/ink/build/utils.d.ts +9 -0
  414. package/vendor/ink/build/utils.js +19 -0
  415. package/vendor/ink/build/utils.js.map +1 -0
  416. package/vendor/ink/build/wrap-text.d.ts +3 -0
  417. package/vendor/ink/build/wrap-text.js +38 -0
  418. package/vendor/ink/build/wrap-text.js.map +1 -0
  419. package/vendor/ink/build/write-synchronized.d.ts +4 -0
  420. package/vendor/ink/build/write-synchronized.js +9 -0
  421. package/vendor/ink/build/write-synchronized.js.map +1 -0
  422. package/vendor/ink/license +10 -0
  423. package/vendor/ink/package.json +137 -0
  424. package/.claude-plugin/marketplace.json +0 -34
  425. package/.claude-plugin/plugin.json +0 -20
  426. package/.gitattributes +0 -34
  427. package/.mcp.json +0 -14
  428. package/ARCHITECTURE.md +0 -77
  429. package/CHANGELOG.md +0 -30
  430. package/CONTRIBUTING.md +0 -45
  431. package/DATA-FLOW.md +0 -79
  432. package/LICENSE +0 -21
  433. package/SECURITY.md +0 -138
  434. package/UNINSTALL.md +0 -112
  435. package/agents/maintenance.md +0 -5
  436. package/agents/memory-classification.md +0 -30
  437. package/agents/scheduler-task.md +0 -18
  438. package/agents/webhook-handler.md +0 -27
  439. package/agents/worker.md +0 -24
  440. package/bin/bridge +0 -133
  441. package/bin/statusline-launcher.mjs +0 -82
  442. package/bin/statusline-lib.mjs +0 -558
  443. package/bin/statusline.mjs +0 -615
  444. package/bun.lock +0 -927
  445. package/commands/config.md +0 -16
  446. package/commands/doctor.md +0 -13
  447. package/commands/setup.md +0 -17
  448. package/defaults/hidden-roles.json +0 -68
  449. package/defaults/memory-chunk-prompt.md +0 -63
  450. package/defaults/mixdog-config.template.json +0 -27
  451. package/defaults/user-workflow.json +0 -8
  452. package/defaults/user-workflow.md +0 -17
  453. package/hooks/hooks.json +0 -73
  454. package/hooks/lib/active-instance.cjs +0 -77
  455. package/hooks/lib/permission-evaluator.cjs +0 -411
  456. package/hooks/lib/permission-route.cjs +0 -63
  457. package/hooks/lib/settings-loader.cjs +0 -117
  458. package/hooks/post-tool-use.cjs +0 -84
  459. package/hooks/pre-mcp-sandbox.cjs +0 -158
  460. package/hooks/pre-tool-subagent.cjs +0 -258
  461. package/hooks/session-start.cjs +0 -1479
  462. package/hooks/shim-launcher.cjs +0 -65
  463. package/hooks/turn-timer.cjs +0 -82
  464. package/lib/claude-md-writer.cjs +0 -386
  465. package/lib/keychain-cjs.cjs +0 -290
  466. package/lib/plugin-paths.cjs +0 -69
  467. package/lib/rules-builder.cjs +0 -241
  468. package/native/README.md +0 -117
  469. package/native/prebuilt/linux-aarch64/mixdog-shim +0 -0
  470. package/native/prebuilt/linux-x86_64/mixdog-shim +0 -0
  471. package/native/prebuilt/macos-aarch64/mixdog-shim +0 -0
  472. package/native/prebuilt/macos-x86_64/mixdog-shim +0 -0
  473. package/native/prebuilt/windows-x86_64/mixdog-shim.exe +0 -0
  474. package/prompts/code-review.txt +0 -16
  475. package/prompts/security-audit.txt +0 -17
  476. package/rules/bridge/00-common.md +0 -39
  477. package/rules/bridge/20-skip-protocol.md +0 -18
  478. package/rules/bridge/30-explorer.md +0 -33
  479. package/rules/bridge/40-cycle1-agent.md +0 -52
  480. package/rules/bridge/41-cycle2-agent.md +0 -62
  481. package/rules/lead/00-tool-lead.md +0 -61
  482. package/rules/lead/01-general.md +0 -23
  483. package/rules/lead/02-channels.md +0 -49
  484. package/rules/lead/03-team.md +0 -27
  485. package/rules/lead/04-workflow.md +0 -20
  486. package/rules/shared/00-language.md +0 -14
  487. package/rules/shared/01-tool.md +0 -138
  488. package/scripts/bootstrap.mjs +0 -130
  489. package/scripts/bridge-unify-smoke.mjs +0 -308
  490. package/scripts/build-runtime-linux.sh +0 -348
  491. package/scripts/build-runtime-macos.sh +0 -217
  492. package/scripts/build-runtime-windows.ps1 +0 -242
  493. package/scripts/builtin-utils-smoke.mjs +0 -398
  494. package/scripts/bump.mjs +0 -80
  495. package/scripts/check-json.mjs +0 -45
  496. package/scripts/check-syntax-changed.mjs +0 -102
  497. package/scripts/check-syntax.mjs +0 -58
  498. package/scripts/code-graph-batch.test.mjs +0 -33
  499. package/scripts/config-preserve-smoke.mjs +0 -180
  500. package/scripts/doctor.mjs +0 -489
  501. package/scripts/edit-normalize-fuzz.mjs +0 -130
  502. package/scripts/edit-normalize-smoke.mjs +0 -401
  503. package/scripts/edit-operation-smoke.mjs +0 -369
  504. package/scripts/edit2-smoke.mjs +0 -63
  505. package/scripts/ensure-deps.mjs +0 -259
  506. package/scripts/fuzzy-e2e.mjs +0 -28
  507. package/scripts/fuzzy-smoke.mjs +0 -26
  508. package/scripts/generate-runtime-manifest.mjs +0 -166
  509. package/scripts/guard-smoke.mjs +0 -66
  510. package/scripts/hidden-role-schema-smoke.mjs +0 -162
  511. package/scripts/hook-routing-smoke.mjs +0 -29
  512. package/scripts/inject-input.ps1 +0 -204
  513. package/scripts/io-complex-smoke.mjs +0 -667
  514. package/scripts/io-explore-bench.mjs +0 -424
  515. package/scripts/io-guardrails-smoke.mjs +0 -205
  516. package/scripts/io-mini-bench-baseline.json +0 -11
  517. package/scripts/io-mini-bench.mjs +0 -216
  518. package/scripts/io-route-harness.mjs +0 -933
  519. package/scripts/io-telemetry-report.mjs +0 -691
  520. package/scripts/mutation-bench.mjs +0 -564
  521. package/scripts/mutation-io-smoke.mjs +0 -1097
  522. package/scripts/native-patch-bridge-smoke.mjs +0 -288
  523. package/scripts/native-patch-smoke.mjs +0 -304
  524. package/scripts/openai-oauth-catalog-smoke.mjs +0 -53
  525. package/scripts/patch-interior-context-smoke.mjs +0 -49
  526. package/scripts/patch-newline-utf8-smoke.mjs +0 -157
  527. package/scripts/perf-hook-smoke.mjs +0 -71
  528. package/scripts/permission-eval-smoke.mjs +0 -443
  529. package/scripts/prep-patch.mjs +0 -53
  530. package/scripts/prep-shim.mjs +0 -96
  531. package/scripts/provider-cache-smoke.mjs +0 -687
  532. package/scripts/report-runtime-health.mjs +0 -132
  533. package/scripts/resolve-bun.mjs +0 -60
  534. package/scripts/run-mcp.mjs +0 -1448
  535. package/scripts/salvage-v4a-shatter.test.mjs +0 -58
  536. package/scripts/scoped-cache-io-smoke.mjs +0 -103
  537. package/scripts/shell-policy-round3-smoke.mjs +0 -46
  538. package/scripts/smoke-runtime-negative.ps1 +0 -100
  539. package/scripts/smoke-runtime-negative.sh +0 -95
  540. package/scripts/stall-policy-smoke.mjs +0 -50
  541. package/scripts/start-memory-worker.mjs +0 -23
  542. package/scripts/statusline-launcher-smoke.mjs +0 -82
  543. package/scripts/stress-atomic-write.mjs +0 -1028
  544. package/scripts/test-fault-inject.mjs +0 -164
  545. package/scripts/test-large-file.mjs +0 -174
  546. package/scripts/tool-edge-smoke.mjs +0 -209
  547. package/scripts/uninstall.mjs +0 -201
  548. package/scripts/webhook-selfheal-smoke.mjs +0 -27
  549. package/scripts/write-overwrite-guard-smoke.mjs +0 -56
  550. package/server-main.mjs +0 -3109
  551. package/server.mjs +0 -468
  552. package/setup/config-merge.mjs +0 -246
  553. package/setup/install.mjs +0 -574
  554. package/setup/launch-core.mjs +0 -617
  555. package/setup/launch.mjs +0 -101
  556. package/setup/locate-claude.mjs +0 -56
  557. package/setup/mixdog-cli.mjs +0 -122
  558. package/setup/setup-server.mjs +0 -3305
  559. package/setup/setup.html +0 -3740
  560. package/setup/tui.mjs +0 -325
  561. package/skills/retro-skill-proposer/SKILL.md +0 -92
  562. package/skills/schedule-add/SKILL.md +0 -77
  563. package/skills/setup/SKILL.md +0 -356
  564. package/skills/webhook-add/SKILL.md +0 -81
  565. package/src/agent/bridge-stall-watchdog.mjs +0 -337
  566. package/src/agent/index.mjs +0 -2138
  567. package/src/agent/orchestrator/ai-wrapped-dispatch.mjs +0 -1010
  568. package/src/agent/orchestrator/bridge-retry.mjs +0 -220
  569. package/src/agent/orchestrator/bridge-trace.mjs +0 -583
  570. package/src/agent/orchestrator/cache-mtime.mjs +0 -58
  571. package/src/agent/orchestrator/config.mjs +0 -405
  572. package/src/agent/orchestrator/context/collect.mjs +0 -651
  573. package/src/agent/orchestrator/dispatch-persist.mjs +0 -549
  574. package/src/agent/orchestrator/drain-registry.mjs +0 -50
  575. package/src/agent/orchestrator/explore-validator.mjs +0 -8
  576. package/src/agent/orchestrator/internal-roles.mjs +0 -118
  577. package/src/agent/orchestrator/internal-tools.mjs +0 -88
  578. package/src/agent/orchestrator/jobs.mjs +0 -116
  579. package/src/agent/orchestrator/mcp/client.mjs +0 -364
  580. package/src/agent/orchestrator/providers/anthropic-oauth.mjs +0 -1881
  581. package/src/agent/orchestrator/providers/anthropic.mjs +0 -594
  582. package/src/agent/orchestrator/providers/gemini.mjs +0 -1530
  583. package/src/agent/orchestrator/providers/grok-oauth.mjs +0 -779
  584. package/src/agent/orchestrator/providers/model-catalog.mjs +0 -374
  585. package/src/agent/orchestrator/providers/openai-compat-stream.mjs +0 -366
  586. package/src/agent/orchestrator/providers/openai-compat.mjs +0 -1511
  587. package/src/agent/orchestrator/providers/openai-oauth-ws.mjs +0 -1891
  588. package/src/agent/orchestrator/providers/openai-oauth.mjs +0 -1456
  589. package/src/agent/orchestrator/providers/openai-ws.mjs +0 -127
  590. package/src/agent/orchestrator/providers/registry.mjs +0 -192
  591. package/src/agent/orchestrator/providers/retry-classifier.mjs +0 -325
  592. package/src/agent/orchestrator/session/cache/prefetch-cache.mjs +0 -142
  593. package/src/agent/orchestrator/session/cache/read-cache.mjs +0 -319
  594. package/src/agent/orchestrator/session/cache/scoped-cache.mjs +0 -361
  595. package/src/agent/orchestrator/session/loop.mjs +0 -1478
  596. package/src/agent/orchestrator/session/manager.mjs +0 -1991
  597. package/src/agent/orchestrator/session/result-classification.mjs +0 -65
  598. package/src/agent/orchestrator/session/store.mjs +0 -632
  599. package/src/agent/orchestrator/session/stream-watchdog.mjs +0 -130
  600. package/src/agent/orchestrator/session/tool-result-offload.mjs +0 -166
  601. package/src/agent/orchestrator/session/trim.mjs +0 -491
  602. package/src/agent/orchestrator/smart-bridge/CACHE-SHARD.md +0 -115
  603. package/src/agent/orchestrator/smart-bridge/bridge-llm.mjs +0 -331
  604. package/src/agent/orchestrator/smart-bridge/cache-obs.mjs +0 -150
  605. package/src/agent/orchestrator/smart-bridge/cache-strategy.mjs +0 -228
  606. package/src/agent/orchestrator/smart-bridge/index.mjs +0 -215
  607. package/src/agent/orchestrator/smart-bridge/profiles.mjs +0 -37
  608. package/src/agent/orchestrator/smart-bridge/registry.mjs +0 -348
  609. package/src/agent/orchestrator/stall-policy.mjs +0 -201
  610. package/src/agent/orchestrator/tool-loop-guard.mjs +0 -75
  611. package/src/agent/orchestrator/tools/bash-session.mjs +0 -722
  612. package/src/agent/orchestrator/tools/builtin/arg-guard.mjs +0 -455
  613. package/src/agent/orchestrator/tools/builtin/bash-tool.mjs +0 -480
  614. package/src/agent/orchestrator/tools/builtin/builtin-tools.mjs +0 -256
  615. package/src/agent/orchestrator/tools/builtin/edit-base-guard.mjs +0 -58
  616. package/src/agent/orchestrator/tools/builtin/edit-byte-plan.mjs +0 -240
  617. package/src/agent/orchestrator/tools/builtin/edit-byte-utils.mjs +0 -113
  618. package/src/agent/orchestrator/tools/builtin/edit-commit.mjs +0 -74
  619. package/src/agent/orchestrator/tools/builtin/edit-context-utils.mjs +0 -242
  620. package/src/agent/orchestrator/tools/builtin/edit-diagnostics.mjs +0 -211
  621. package/src/agent/orchestrator/tools/builtin/edit-engine.mjs +0 -1364
  622. package/src/agent/orchestrator/tools/builtin/edit-failure-context.mjs +0 -126
  623. package/src/agent/orchestrator/tools/builtin/edit-hint.mjs +0 -141
  624. package/src/agent/orchestrator/tools/builtin/edit-match-utils.mjs +0 -194
  625. package/src/agent/orchestrator/tools/builtin/edit-partial-write.mjs +0 -60
  626. package/src/agent/orchestrator/tools/builtin/edit-stale-refresh.mjs +0 -168
  627. package/src/agent/orchestrator/tools/builtin/edit-tool.mjs +0 -173
  628. package/src/agent/orchestrator/tools/builtin/edit-utf8-guard.mjs +0 -48
  629. package/src/agent/orchestrator/tools/builtin/fuzzy-match.mjs +0 -99
  630. package/src/agent/orchestrator/tools/builtin/glob-walk.mjs +0 -193
  631. package/src/agent/orchestrator/tools/builtin/list-tool.mjs +0 -597
  632. package/src/agent/orchestrator/tools/builtin/native-edit-runner.mjs +0 -110
  633. package/src/agent/orchestrator/tools/builtin/notebook-edit-tool.mjs +0 -300
  634. package/src/agent/orchestrator/tools/builtin/path-diagnostics.mjs +0 -152
  635. package/src/agent/orchestrator/tools/builtin/read-formatting.mjs +0 -118
  636. package/src/agent/orchestrator/tools/builtin/read-open.mjs +0 -190
  637. package/src/agent/orchestrator/tools/builtin/read-single-tool.mjs +0 -728
  638. package/src/agent/orchestrator/tools/builtin/read-snapshot-runtime.mjs +0 -173
  639. package/src/agent/orchestrator/tools/builtin/read-streaming.mjs +0 -602
  640. package/src/agent/orchestrator/tools/builtin/rename-tool.mjs +0 -196
  641. package/src/agent/orchestrator/tools/builtin/rg-runner.mjs +0 -422
  642. package/src/agent/orchestrator/tools/builtin/search-builders.mjs +0 -158
  643. package/src/agent/orchestrator/tools/builtin/search-tool.mjs +0 -869
  644. package/src/agent/orchestrator/tools/builtin/shell-jobs.mjs +0 -962
  645. package/src/agent/orchestrator/tools/builtin/shell-runtime.mjs +0 -223
  646. package/src/agent/orchestrator/tools/builtin/snapshot-store.mjs +0 -206
  647. package/src/agent/orchestrator/tools/builtin/write-tool.mjs +0 -401
  648. package/src/agent/orchestrator/tools/builtin.mjs +0 -503
  649. package/src/agent/orchestrator/tools/code-graph-tool-defs.mjs +0 -24
  650. package/src/agent/orchestrator/tools/code-graph.mjs +0 -4095
  651. package/src/agent/orchestrator/tools/cwd-tool.mjs +0 -298
  652. package/src/agent/orchestrator/tools/destructive-warning.mjs +0 -323
  653. package/src/agent/orchestrator/tools/edit-normalize.mjs +0 -603
  654. package/src/agent/orchestrator/tools/graph-binary-fetcher.mjs +0 -154
  655. package/src/agent/orchestrator/tools/graph-manifest.json +0 -26
  656. package/src/agent/orchestrator/tools/host-input.mjs +0 -204
  657. package/src/agent/orchestrator/tools/patch-binary-fetcher.mjs +0 -143
  658. package/src/agent/orchestrator/tools/patch-manifest.json +0 -26
  659. package/src/agent/orchestrator/tools/patch-tool-defs.mjs +0 -20
  660. package/src/agent/orchestrator/tools/patch.mjs +0 -2754
  661. package/src/agent/orchestrator/tools/progress-message.mjs +0 -118
  662. package/src/agent/orchestrator/tools/shell-command.mjs +0 -865
  663. package/src/agent/orchestrator/tools/shell-policy-imports.mjs +0 -7
  664. package/src/agent/orchestrator/tools/shell-snapshot.mjs +0 -313
  665. package/src/agent/orchestrator/workflow-store.mjs +0 -93
  666. package/src/agent/tool-defs.mjs +0 -103
  667. package/src/channels/backends/discord.mjs +0 -784
  668. package/src/channels/data/voice-runtime-manifest.json +0 -138
  669. package/src/channels/index.mjs +0 -3268
  670. package/src/channels/lib/config.mjs +0 -292
  671. package/src/channels/lib/drop-trace.mjs +0 -71
  672. package/src/channels/lib/event-pipeline.mjs +0 -81
  673. package/src/channels/lib/holidays.mjs +0 -138
  674. package/src/channels/lib/hook-pipe-server.mjs +0 -822
  675. package/src/channels/lib/output-forwarder.mjs +0 -765
  676. package/src/channels/lib/runtime-paths.mjs +0 -517
  677. package/src/channels/lib/scheduler.mjs +0 -723
  678. package/src/channels/lib/session-discovery.mjs +0 -103
  679. package/src/channels/lib/state-file.mjs +0 -68
  680. package/src/channels/lib/status-snapshot.mjs +0 -219
  681. package/src/channels/lib/tool-format.mjs +0 -140
  682. package/src/channels/lib/transcript-discovery.mjs +0 -195
  683. package/src/channels/lib/voice-runtime-fetcher.mjs +0 -734
  684. package/src/channels/lib/webhook.mjs +0 -1318
  685. package/src/channels/tool-defs.mjs +0 -170
  686. package/src/daemon/host.mjs +0 -118
  687. package/src/daemon/mcp-transport.mjs +0 -47
  688. package/src/daemon/session.mjs +0 -100
  689. package/src/daemon/thin-client.mjs +0 -71
  690. package/src/daemon/transport.mjs +0 -163
  691. package/src/memory/data/runtime-manifest.json +0 -40
  692. package/src/memory/index.mjs +0 -3332
  693. package/src/memory/lib/core-memory-store.mjs +0 -330
  694. package/src/memory/lib/embedding-provider.mjs +0 -269
  695. package/src/memory/lib/embedding-worker.mjs +0 -323
  696. package/src/memory/lib/memory-cycle1.mjs +0 -645
  697. package/src/memory/lib/memory-cycle2.mjs +0 -1284
  698. package/src/memory/lib/memory-cycle3.mjs +0 -540
  699. package/src/memory/lib/memory-embed.mjs +0 -299
  700. package/src/memory/lib/memory-ops-policy.mjs +0 -190
  701. package/src/memory/lib/memory-recall-store.mjs +0 -638
  702. package/src/memory/lib/memory.mjs +0 -412
  703. package/src/memory/lib/pg/adapter.mjs +0 -308
  704. package/src/memory/lib/pg/process.mjs +0 -360
  705. package/src/memory/lib/pg/supervisor.mjs +0 -396
  706. package/src/memory/lib/runtime-fetcher.mjs +0 -458
  707. package/src/memory/lib/trace-store.mjs +0 -728
  708. package/src/memory/tool-defs.mjs +0 -79
  709. package/src/search/index.mjs +0 -1173
  710. package/src/search/lib/backends/anthropic-oauth.mjs +0 -98
  711. package/src/search/lib/backends/exa.mjs +0 -50
  712. package/src/search/lib/backends/firecrawl.mjs +0 -61
  713. package/src/search/lib/backends/gemini-api.mjs +0 -83
  714. package/src/search/lib/backends/grok-oauth.mjs +0 -86
  715. package/src/search/lib/backends/index.mjs +0 -150
  716. package/src/search/lib/backends/openai-api.mjs +0 -144
  717. package/src/search/lib/backends/openai-oauth.mjs +0 -102
  718. package/src/search/lib/backends/openai-web-search.mjs +0 -76
  719. package/src/search/lib/backends/tavily.mjs +0 -55
  720. package/src/search/lib/backends/xai-api.mjs +0 -113
  721. package/src/search/lib/config.mjs +0 -192
  722. package/src/search/lib/provider-usage.mjs +0 -67
  723. package/src/search/lib/providers.mjs +0 -47
  724. package/src/search/lib/search-intent.mjs +0 -109
  725. package/src/search/lib/setup-handler.mjs +0 -261
  726. package/src/search/lib/web-tools.mjs +0 -1219
  727. package/src/search/tool-defs.mjs +0 -83
  728. package/src/setup/defender-exclusion.mjs +0 -183
  729. package/src/shared/atomic-file.mjs +0 -436
  730. package/src/shared/config.mjs +0 -372
  731. package/src/shared/daemon-recycle.mjs +0 -108
  732. package/src/shared/disable-claude-builtins.mjs +0 -91
  733. package/src/shared/err-text.mjs +0 -12
  734. package/src/shared/llm/cost.mjs +0 -66
  735. package/src/shared/llm/http-agent.mjs +0 -123
  736. package/src/shared/open-url.mjs +0 -62
  737. package/src/shared/plugin-paths.mjs +0 -58
  738. package/src/shared/schedules-store.mjs +0 -70
  739. package/src/shared/seed.mjs +0 -136
  740. package/src/shared/user-cwd.mjs +0 -225
  741. package/src/shared/user-data-guard.mjs +0 -244
  742. package/src/status/aggregator.mjs +0 -584
  743. package/src/status/server.mjs +0 -413
  744. package/tools.json +0 -1653
  745. /package/{defaults → src/defaults}/cycle3-review-prompt.md +0 -0
  746. /package/{defaults → src/defaults}/memory-promote-prompt.md +0 -0
  747. /package/{hooks → src/hooks}/lib/permission-rules.cjs +0 -0
  748. /package/{lib → src/lib}/config-cjs.cjs +0 -0
  749. /package/{lib → src/lib}/hook-pipe-path.cjs +0 -0
  750. /package/{lib → src/lib}/mixdog-debug.cjs +0 -0
  751. /package/{lib → src/lib}/text-utils.cjs +0 -0
  752. /package/{rules → src/rules}/bridge/42-cycle3-agent.md +0 -0
  753. /package/src/{agent → runtime/agent}/orchestrator/activity-bus.mjs +0 -0
  754. /package/src/{agent → runtime/agent}/orchestrator/providers/anthropic-betas.mjs +0 -0
  755. /package/src/{agent → runtime/agent}/orchestrator/session/abort-lookup.mjs +0 -0
  756. /package/src/{agent → runtime/agent}/orchestrator/session/cache/post-edit-marks.mjs +0 -0
  757. /package/src/{agent → runtime/agent}/orchestrator/session/cache/scoped-cache-outcome.mjs +0 -0
  758. /package/src/{agent → runtime/agent}/orchestrator/session/cache/util.mjs +0 -0
  759. /package/src/{agent → runtime/agent}/orchestrator/session/read-dedup.mjs +0 -0
  760. /package/src/{agent → runtime/agent}/orchestrator/session/save-session-worker.mjs +0 -0
  761. /package/src/{agent → runtime/agent}/orchestrator/smart-bridge/session-builder.mjs +0 -0
  762. /package/src/{agent → runtime/agent}/orchestrator/tools/bash-policy-scan.mjs +0 -0
  763. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/advisory-lock.mjs +0 -0
  764. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/atomic-write.mjs +0 -0
  765. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/binary-file.mjs +0 -0
  766. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/cache-layers.mjs +0 -0
  767. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/cwd-utils.mjs +0 -0
  768. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/device-paths.mjs +0 -0
  769. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/diagnostics-tool.mjs +0 -0
  770. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/diff-utils.mjs +0 -0
  771. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/fs-reachability.mjs +0 -0
  772. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/grep-formatting.mjs +0 -0
  773. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/hash-utils.mjs +0 -0
  774. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/list-formatting.mjs +0 -0
  775. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/open-config-tool.mjs +0 -0
  776. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/path-locks.mjs +0 -0
  777. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/path-utils.mjs +0 -0
  778. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/read-args.mjs +0 -0
  779. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/read-batch.mjs +0 -0
  780. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/read-constants.mjs +0 -0
  781. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/read-image-resize.mjs +0 -0
  782. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/read-image.mjs +0 -0
  783. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/read-lines.mjs +0 -0
  784. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/read-mode-tool.mjs +0 -0
  785. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/read-range-index.mjs +0 -0
  786. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/read-ranges.mjs +0 -0
  787. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/read-special-files.mjs +0 -0
  788. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/read-tool.mjs +0 -0
  789. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/read-windows.mjs +0 -0
  790. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/shell-analysis.mjs +0 -0
  791. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/shell-output.mjs +0 -0
  792. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/snapshot-helpers.mjs +0 -0
  793. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/snapshot-validation.mjs +0 -0
  794. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/text-stats.mjs +0 -0
  795. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/windows-roots.mjs +0 -0
  796. /package/src/{agent → runtime/agent}/orchestrator/tools/code-graph-prewarm-worker.mjs +0 -0
  797. /package/src/{agent → runtime/agent}/orchestrator/tools/env-scrub.mjs +0 -0
  798. /package/src/{agent → runtime/agent}/orchestrator/tools/mutation-content-cache.mjs +0 -0
  799. /package/src/{agent → runtime/agent}/orchestrator/tools/mutation-planner.mjs +0 -0
  800. /package/src/{agent → runtime/agent}/orchestrator/tools/next-call-utils.mjs +0 -0
  801. /package/src/{agent → runtime/agent}/orchestrator/tools/result-compression.mjs +0 -0
  802. /package/src/{agent → runtime/agent}/orchestrator/tools/shell-exec-policy.mjs +0 -0
  803. /package/src/{agent → runtime/agent}/orchestrator/tools/shell-policy-danger-target.mjs +0 -0
  804. /package/src/{agent → runtime/agent}/orchestrator/tools/shell-policy.mjs +0 -0
  805. /package/src/{channels → runtime/channels}/lib/cli-worker-host.mjs +0 -0
  806. /package/src/{channels → runtime/channels}/lib/config-lock.mjs +0 -0
  807. /package/src/{channels → runtime/channels}/lib/event-queue.mjs +0 -0
  808. /package/src/{channels → runtime/channels}/lib/executor.mjs +0 -0
  809. /package/src/{channels → runtime/channels}/lib/format.mjs +0 -0
  810. /package/src/{channels → runtime/channels}/lib/interaction-workflows.mjs +0 -0
  811. /package/src/{channels → runtime/channels}/lib/memory-client.mjs +0 -0
  812. /package/src/{channels → runtime/channels}/lib/session-control.mjs +0 -0
  813. /package/src/{channels → runtime/channels}/lib/settings.mjs +0 -0
  814. /package/src/{channels → runtime/channels}/lib/whisper-server.mjs +0 -0
  815. /package/src/{memory → runtime/memory}/lib/agent-ipc.mjs +0 -0
  816. /package/src/{memory → runtime/memory}/lib/bridge-trace-queries.mjs +0 -0
  817. /package/src/{memory → runtime/memory}/lib/llm-worker-host.mjs +0 -0
  818. /package/src/{memory → runtime/memory}/lib/memory-cycle.mjs +0 -0
  819. /package/src/{memory → runtime/memory}/lib/memory-extraction.mjs +0 -0
  820. /package/src/{memory → runtime/memory}/lib/memory-maintenance-store.mjs +0 -0
  821. /package/src/{memory → runtime/memory}/lib/memory-recall-id-patch.mjs +0 -0
  822. /package/src/{memory → runtime/memory}/lib/memory-recall-read-query.mjs +0 -0
  823. /package/src/{memory → runtime/memory}/lib/memory-recall-scope-filter.mjs +0 -0
  824. /package/src/{memory → runtime/memory}/lib/memory-retrievers.mjs +0 -0
  825. /package/src/{memory → runtime/memory}/lib/memory-score.mjs +0 -0
  826. /package/src/{memory → runtime/memory}/lib/memory-text-utils.mjs +0 -0
  827. /package/src/{memory → runtime/memory}/lib/model-profile.mjs +0 -0
  828. /package/src/{memory → runtime/memory}/lib/project-id-resolver.mjs +0 -0
  829. /package/src/{search → runtime/search}/lib/cache.mjs +0 -0
  830. /package/src/{search → runtime/search}/lib/formatter.mjs +0 -0
  831. /package/src/{search → runtime/search}/lib/state.mjs +0 -0
  832. /package/src/{shared → runtime/shared}/abort-controller.mjs +0 -0
  833. /package/src/{shared → runtime/shared}/llm/index.mjs +0 -0
  834. /package/src/{shared → runtime/shared}/llm/pid-cleanup.mjs +0 -0
  835. /package/src/{shared → runtime/shared}/llm/usage-log.mjs +0 -0
  836. /package/src/{shared → runtime/shared}/wsl.mjs +0 -0
@@ -0,0 +1,2772 @@
1
+ // apply_patch — one-turn multi-file edits from a unified diff.
2
+ //
3
+ // Typical Lead workflow without this tool is `read` → `edit` per file, which
4
+ // costs N+1 turns for an N-file refactor. A unified diff already encodes
5
+ // every hunk's surrounding context, so we can apply the whole patch
6
+ // server-side and skip the read round-trips entirely.
7
+ //
8
+ // Backend: NATIVE-ONLY. Every supported case is dispatched to the
9
+ // mixdog-patch Rust engine via the persistent stdio server. There is NO
10
+ // JS apply fallback: unsupported / unsafe input returns a clean Error
11
+ // string ("Error: …"), never silently degrades to a different engine.
12
+ // `parsePatch(str)` splits a multi-file diff into one object per file
13
+ // with `{oldFileName, newFileName, hunks}`; the parsed entries are
14
+ // consulted only to derive display path/lines stats and to pre-validate
15
+ // path-escape safety before dispatch.
16
+ //
17
+ // Safety model:
18
+ // - No separate read gate. Hunk context lines are themselves the
19
+ // match proof — if they don't match, the native engine
20
+ // rejects the hunk and nothing is written.
21
+ // - Path-escape pre-validator throws on out-of-base `..` segments and
22
+ // symlink/junction escapes (realpath verifies the target stays inside
23
+ // the basePath even when an intermediate symlink points outside).
24
+ // - `reject_partial:true` (default) — file-batch atomic. Native engine
25
+ // errors out before touching disk.
26
+ // - `reject_partial:false` — file-level isolation. Native engine
27
+ // responds with OK_PARTIAL plus a hex-encoded failures payload that
28
+ // the JS side surfaces per-entry.
29
+
30
+ import { existsSync, readFileSync, realpathSync, statSync, mkdirSync } from 'node:fs';
31
+ import { unlink } from 'node:fs/promises';
32
+ import { spawn } from 'node:child_process';
33
+ import { createInterface } from 'node:readline';
34
+ import { fileURLToPath } from 'node:url';
35
+ import { resolve as pathResolve, relative as pathRelative, isAbsolute, dirname as pathDirname, join as pathJoin } from 'node:path';
36
+ import { performance } from 'node:perf_hooks';
37
+ import { parsePatch } from 'diff';
38
+ import { getAbortSignalForSession } from '../session/abort-lookup.mjs';
39
+ import { startChildGuardian } from '../../../shared/child-guardian.mjs';
40
+ import {
41
+ normalizeInputPath,
42
+ normalizeOutputPath,
43
+ resolveAgainstCwd,
44
+ invalidateBuiltinResultCache,
45
+ recordReadSnapshotForPath,
46
+ clearReadSnapshotForPath,
47
+ withBuiltinPathLocks,
48
+ } from './builtin.mjs';
49
+ import { withAdvisoryLocks } from './builtin/advisory-lock.mjs';
50
+ import { markCodeGraphDirtyPaths } from './code-graph.mjs';
51
+ import { wrapMutationRouteOutput } from './mutation-planner.mjs';
52
+ import { getPluginData } from '../config.mjs';
53
+ import { ensurePatchBinary, findCachedPatchBinary } from './patch-binary-fetcher.mjs';
54
+ import {
55
+ rawContentCacheGet,
56
+ rawContentCacheSet,
57
+ } from './builtin/cache-layers.mjs';
58
+ import { atomicWrite } from './builtin/atomic-write.mjs';
59
+ import { assertPathReachable, assertPathsReachable } from './builtin/fs-reachability.mjs';
60
+ export { PATCH_TOOL_DEFS } from './patch-tool-defs.mjs';
61
+
62
+ const DEV_NULL = /^\/dev\/null$/;
63
+ const V4A_EOF_MARKER = '*** End of File';
64
+ const V4A_MOVE_TO_PREFIX = '*** Move to:';
65
+ const PLUGIN_ROOT = process.env.MIXDOG_ROOT
66
+ || pathResolve(pathDirname(fileURLToPath(import.meta.url)), '../../../..');
67
+ const NATIVE_PATCH_DEFAULT_BIN = pathJoin(
68
+ PLUGIN_ROOT,
69
+ 'native/mixdog-patch/target/release',
70
+ process.platform === 'win32' ? 'mixdog-patch.exe' : 'mixdog-patch',
71
+ );
72
+ let _nativePatchServer = null;
73
+ let _nativePatchPrewarmTimer = null;
74
+ let _nativeEditServer = null;
75
+
76
+ function markNativePatchRuntimeTouched() {
77
+ try { globalThis.__mixdogNativePatchRuntimeTouched = true; } catch {}
78
+ }
79
+
80
+ function nativePatchMode() {
81
+ return String(process.env.MIXDOG_PATCH_NATIVE || 'auto').toLowerCase();
82
+ }
83
+
84
+ function nativePatchEnabled() {
85
+ return !/^(0|false|no|off|js|legacy)$/i.test(nativePatchMode());
86
+ }
87
+
88
+ function nativePatchTraceEnabled() {
89
+ return /^(1|true|yes)$/i.test(process.env.MIXDOG_PATCH_NATIVE_TRACE || '');
90
+ }
91
+
92
+ function ioTraceEnabled() {
93
+ return /^(1|true|yes|on)$/i.test(String(process.env.MIXDOG_IO_TRACE || ''));
94
+ }
95
+
96
+ function ioTrace(event, fields = {}) {
97
+ if (!ioTraceEnabled()) return;
98
+ try {
99
+ process.stderr.write(`[io-trace] ${JSON.stringify({ event, ts: Date.now(), ...fields })}\n`);
100
+ } catch {}
101
+ }
102
+
103
+ function patchTraceEnabled() {
104
+ return ioTraceEnabled()
105
+ || nativePatchTraceEnabled()
106
+ || /^(1|true|yes|on)$/i.test(String(process.env.MIXDOG_PATCH_TRACE || ''));
107
+ }
108
+
109
+ function nativePatchPrewarmEnabled() {
110
+ if (!nativePatchEnabled()) return false;
111
+ if (process.env.MIXDOG_PATCH_NATIVE_BIN && !existsSync(nativePatchBinPath())) return false;
112
+ return !/^(0|false|no)$/i.test(process.env.MIXDOG_PATCH_NATIVE_PREWARM || '');
113
+ }
114
+
115
+ function nativePatchPersistent() {
116
+ return /^(1|true|yes|server|persistent)$/i.test(nativePatchMode());
117
+ }
118
+
119
+ function nativePatchBinPath() {
120
+ if (process.env.MIXDOG_PATCH_NATIVE_BIN) return process.env.MIXDOG_PATCH_NATIVE_BIN;
121
+ // Local cargo build first, then a fetched/cached prebuilt; absence is
122
+ // a hard error at dispatch (no JS fallback in native-only mode).
123
+ if (existsSync(NATIVE_PATCH_DEFAULT_BIN)) return NATIVE_PATCH_DEFAULT_BIN;
124
+ return findCachedPatchBinary(getPluginData()) || NATIVE_PATCH_DEFAULT_BIN;
125
+ }
126
+
127
+ async function ensureNativePatchBinaryAvailable() {
128
+ if (!nativePatchEnabled()) {
129
+ throw new Error('apply_patch: native engine disabled via MIXDOG_PATCH_NATIVE; set it to "auto" or "1" to apply patches.');
130
+ }
131
+ const current = nativePatchBinPath();
132
+ if (existsSync(current)) return current;
133
+ if (process.env.MIXDOG_PATCH_NATIVE_BIN) {
134
+ throw new Error(`apply_patch: native patch binary not found at MIXDOG_PATCH_NATIVE_BIN=${current}.`);
135
+ }
136
+ try {
137
+ const fetched = await ensurePatchBinary(getPluginData());
138
+ if (fetched && existsSync(fetched)) return fetched;
139
+ } catch (err) {
140
+ throw new Error(`apply_patch: native patch binary unavailable — ${err?.message || String(err)}`);
141
+ }
142
+ const resolved = nativePatchBinPath();
143
+ if (existsSync(resolved)) return resolved;
144
+ throw new Error(`apply_patch: native patch binary not found at ${resolved}.`);
145
+ }
146
+
147
+ // Decode the hex-encoded failures payload that accompanies OK_PARTIAL:
148
+ // the Rust side emits utf-8 bytes (`<path>\t<reason>` records joined by
149
+ // `\n`) hex-encoded so they can ride the tab-separated response line
150
+ // without escaping. An empty / unparseable payload becomes an empty list
151
+ // so a missing field never crashes the caller.
152
+ function decodeNativeFailures(hexPayload) {
153
+ if (typeof hexPayload !== 'string' || hexPayload.length === 0) return [];
154
+ if (!/^[0-9a-fA-F]+$/.test(hexPayload) || hexPayload.length % 2 !== 0) return [];
155
+ let text = '';
156
+ try { text = Buffer.from(hexPayload, 'hex').toString('utf-8'); }
157
+ catch { return []; }
158
+ const out = [];
159
+ for (const raw of text.split('\n')) {
160
+ if (!raw) continue;
161
+ const tab = raw.indexOf('\t');
162
+ if (tab === -1) out.push({ path: '', reason: raw });
163
+ else out.push({ path: raw.slice(0, tab), reason: raw.slice(tab + 1) });
164
+ }
165
+ return out;
166
+ }
167
+
168
+ class NativePatchServer {
169
+ constructor(binPath) {
170
+ this.binPath = binPath;
171
+ // windowsHide: mixdog-patch.exe is a console binary; without this each spawn
172
+ // flashes an empty console window on Windows. Especially visible now that the
173
+ // idle watchdog exits the server and it respawns on the next request.
174
+ this.child = spawn(binPath, ['--server'], { stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true });
175
+ startChildGuardian({ childPid: this.child.pid, label: 'native-patch-server', orphanGraceMs: 5000, forceGraceMs: 2000 });
176
+ this.stderr = '';
177
+ this.lines = [];
178
+ this.waiters = [];
179
+ this.exited = false;
180
+ this.child.stderr.setEncoding('utf8');
181
+ this.child.stderr.on('data', (chunk) => { this.stderr += chunk; });
182
+ this.rl = createInterface({ input: this.child.stdout });
183
+ this.rl.on('line', (line) => {
184
+ const waiter = this.waiters.shift();
185
+ if (waiter) waiter.resolve(line);
186
+ else this.lines.push(line);
187
+ });
188
+ this.child.on('exit', (code, signal) => {
189
+ this.exited = true;
190
+ const err = new Error(`native patch server exited code=${code} signal=${signal} stderr=${this.stderr}`);
191
+ for (const waiter of this.waiters.splice(0)) waiter.reject(err);
192
+ try { this.rl.close(); } catch {}
193
+ });
194
+ }
195
+
196
+ abort(signal) {
197
+ const err = new Error(signal?.reason?.message || signal?.reason || 'native patch aborted');
198
+ err.name = 'AbortError';
199
+ if (_nativePatchServer === this) _nativePatchServer = null;
200
+ for (const waiter of this.waiters.splice(0)) waiter.reject(err);
201
+ try { this.child.kill('SIGTERM'); } catch {}
202
+ return err;
203
+ }
204
+
205
+ nextLine() {
206
+ if (this.lines.length > 0) return Promise.resolve(this.lines.shift());
207
+ if (this.exited) return Promise.reject(new Error(`native patch server already exited: ${this.stderr}`));
208
+ return new Promise((resolve, reject) => this.waiters.push({ resolve, reject }));
209
+ }
210
+
211
+ ref() {
212
+ try { this.child.ref(); } catch {}
213
+ try { this.child.stdin.ref?.(); } catch {}
214
+ try { this.child.stdout.ref?.(); } catch {}
215
+ try { this.child.stderr.ref?.(); } catch {}
216
+ }
217
+
218
+ unref() {
219
+ try { this.child.unref(); } catch {}
220
+ try { this.child.stdin.unref?.(); } catch {}
221
+ try { this.child.stdout.unref?.(); } catch {}
222
+ try { this.child.stderr.unref?.(); } catch {}
223
+ }
224
+
225
+ async ping() {
226
+ this.ref();
227
+ const linePromise = this.nextLine();
228
+ this.child.stdin.write('PING\n');
229
+ const line = await linePromise;
230
+ if (line !== 'OK\tPONG') {
231
+ throw new Error(`native patch server ping failed: ${line || 'no native response'}`);
232
+ }
233
+ }
234
+
235
+ async apply(basePath, patchText, { fuzz = 2, rejectPartial = true, dryRun = false, signal = null } = {}) {
236
+ this.ref();
237
+ if (signal?.aborted) {
238
+ const err = new Error(signal.reason?.message || signal.reason || 'native patch aborted');
239
+ err.name = 'AbortError';
240
+ throw err;
241
+ }
242
+ const started = performance.now();
243
+ const baseBuf = Buffer.from(basePath, 'utf8');
244
+ const patchBuf = Buffer.from(patchText, 'utf8');
245
+ const linePromise = this.nextLine();
246
+ if (signal) linePromise.catch(() => {});
247
+ let abortListener = null;
248
+ const abortPromise = signal ? new Promise((_, reject) => {
249
+ abortListener = () => {
250
+ reject(this.abort(signal));
251
+ };
252
+ signal.addEventListener('abort', abortListener, { once: true });
253
+ }) : null;
254
+ // 7-token APPLY protocol: APPLY <base_len> <patch_len> <timing> <dry_run> <fuzz> <reject_partial>
255
+ // - timing=1 keeps the server emitting per-phase ms fields
256
+ // - dry_run=1 validates without writing; useful for tests and explicit callers
257
+ // - fuzz=0 means strict context match; fuzz=2 absorbs minor outer-context drift and context trailing spaces/tabs
258
+ // - reject_partial=0 unlocks file-level isolation (OK_PARTIAL response)
259
+ const fuzzTok = Number.isFinite(fuzz) && fuzz >= 0 ? Math.floor(fuzz) : 2;
260
+ const rpTok = rejectPartial ? 1 : 0;
261
+ const dryTok = dryRun ? 1 : 0;
262
+ this.child.stdin.write(`APPLY ${baseBuf.length} ${patchBuf.length} 1 ${dryTok} ${fuzzTok} ${rpTok}\n`);
263
+ this.child.stdin.write(baseBuf);
264
+ this.child.stdin.write(patchBuf);
265
+ let line;
266
+ try {
267
+ line = abortPromise ? await Promise.race([linePromise, abortPromise]) : await linePromise;
268
+ } finally {
269
+ if (abortListener) {
270
+ try { signal.removeEventListener('abort', abortListener); } catch {}
271
+ }
272
+ }
273
+ if (!line) throw new Error('no native response');
274
+ if (line.startsWith('ERR\t')) throw new Error(line.slice(4));
275
+ const okFull = line.startsWith('OK\t');
276
+ const okPartial = line.startsWith('OK_PARTIAL\t');
277
+ if (!okFull && !okPartial) throw new Error(line);
278
+ const fields = line.split('\t');
279
+ // fields[0] = "OK" | "OK_PARTIAL".
280
+ // OK layout: <files> <readMs> <applyMs> <writeMs> <totalMs> <hashMs> <contentHashes>
281
+ // OK_PARTIAL layout: <files> <failed> <readMs> <applyMs> <writeMs> <totalMs> <hashMs> <contentHashes> <hexFailures>
282
+ // The OK_PARTIAL line carries an extra <failed> count between <files>
283
+ // and the timing block, plus a trailing <hexFailures> column — keep
284
+ // the two decodes separate so SKIP failure counts stay accurate.
285
+ let files; let readMs; let applyMs; let writeMs; let totalMs; let hashMs;
286
+ let contentHashesRaw; let hexFailures;
287
+ if (okPartial) {
288
+ files = fields[1];
289
+ // fields[2] = <failed> count; the JS layer already derives a failure
290
+ // count from decodeNativeFailures(hexFailures), so skip the raw cell.
291
+ readMs = fields[3];
292
+ applyMs = fields[4];
293
+ writeMs = fields[5];
294
+ totalMs = fields[6];
295
+ hashMs = fields[7];
296
+ contentHashesRaw = fields[8];
297
+ hexFailures = fields[9];
298
+ } else {
299
+ files = fields[1];
300
+ readMs = fields[2];
301
+ applyMs = fields[3];
302
+ writeMs = fields[4];
303
+ totalMs = fields[5];
304
+ hashMs = fields[6];
305
+ contentHashesRaw = fields[7];
306
+ }
307
+ const contentHashes = String(contentHashesRaw || '')
308
+ .split(',')
309
+ .filter((value) => value.length > 0)
310
+ .map((value) => (/^[a-f0-9]{64}$/i.test(value) ? value.toLowerCase() : null));
311
+ const failures = okPartial ? decodeNativeFailures(hexFailures) : [];
312
+ return {
313
+ partial: okPartial,
314
+ files: Number(files) || 0,
315
+ readMs: Number(readMs) || 0,
316
+ applyMs: Number(applyMs) || 0,
317
+ writeMs: Number(writeMs) || 0,
318
+ hashMs: Number(hashMs) || 0,
319
+ totalMs: Number(totalMs) || 0,
320
+ contentHashes,
321
+ contentHash: contentHashes.length === 1 ? contentHashes[0] : null,
322
+ failures,
323
+ roundtripMs: performance.now() - started,
324
+ };
325
+ }
326
+
327
+ // EDIT protocol client: invariant-safe char-indexed edit. Mirrors apply()'s
328
+ // abort/await-line handling. EDIT <path_len> <old_len> <new_len> <replace_all>
329
+ // <dry_run> then path+old+new bytes; response is the 8-field OK line with the
330
+ // matched tier.
331
+ async edit(fullPath, oldBuf, newBuf, { replaceAll = false, dryRun = false, signal = null } = {}) {
332
+ this.ref();
333
+ if (signal?.aborted) {
334
+ const err = new Error(signal.reason?.message || signal.reason || 'native edit aborted');
335
+ err.name = 'AbortError';
336
+ throw err;
337
+ }
338
+ const started = performance.now();
339
+ const pathBuf = Buffer.from(fullPath, 'utf8');
340
+ const linePromise = this.nextLine();
341
+ if (signal) linePromise.catch(() => {});
342
+ let abortListener = null;
343
+ const abortPromise = signal ? new Promise((_, reject) => {
344
+ abortListener = () => { reject(this.abort(signal)); };
345
+ signal.addEventListener('abort', abortListener, { once: true });
346
+ }) : null;
347
+ this.child.stdin.write(
348
+ `EDIT ${pathBuf.length} ${oldBuf.length} ${newBuf.length} ${replaceAll ? 1 : 0} ${dryRun ? 1 : 0}\n`,
349
+ );
350
+ this.child.stdin.write(pathBuf);
351
+ this.child.stdin.write(oldBuf);
352
+ this.child.stdin.write(newBuf);
353
+ let line;
354
+ try {
355
+ line = abortPromise ? await Promise.race([linePromise, abortPromise]) : await linePromise;
356
+ } finally {
357
+ if (abortListener) {
358
+ try { signal.removeEventListener('abort', abortListener); } catch {}
359
+ }
360
+ }
361
+ if (!line) throw new Error('no native response');
362
+ if (line.startsWith('ERR\t')) throw new Error(line.slice(4));
363
+ if (!line.startsWith('OK\t')) throw new Error(line);
364
+ const f = line.split('\t');
365
+ // OK \t replacements \t readMs \t applyMs \t writeMs \t totalMs \t tier \t hash
366
+ return {
367
+ replacements: Number(f[1]) || 0,
368
+ readMs: Number(f[2]) || 0,
369
+ applyMs: Number(f[3]) || 0,
370
+ writeMs: Number(f[4]) || 0,
371
+ totalMs: Number(f[5]) || 0,
372
+ tier: f[6] || 'exact',
373
+ contentHash: /^[a-f0-9]{64}$/i.test(f[7] || '') ? f[7].toLowerCase() : null,
374
+ roundtripMs: performance.now() - started,
375
+ };
376
+ }
377
+
378
+ async close(options = {}) {
379
+ if (this.exited) return;
380
+ const waitForExit = options?.waitForExit !== false;
381
+ if (!waitForExit) {
382
+ try { this.child.stdin.end('QUIT\n'); } catch {}
383
+ this.unref();
384
+ return;
385
+ }
386
+ this.ref();
387
+ try { this.child.stdin.end('QUIT\n'); } catch {}
388
+ await new Promise((resolve) => this.child.once('exit', resolve));
389
+ try { this.rl.close(); } catch {}
390
+ }
391
+ }
392
+
393
+ function getNativePatchServer() {
394
+ markNativePatchRuntimeTouched();
395
+ const binPath = nativePatchBinPath();
396
+ if (!existsSync(binPath)) {
397
+ throw new Error(`native patch binary not found: ${binPath}`);
398
+ }
399
+ if (!_nativePatchServer || _nativePatchServer.exited || _nativePatchServer.binPath !== binPath) {
400
+ _nativePatchServer = new NativePatchServer(binPath);
401
+ }
402
+ return _nativePatchServer;
403
+ }
404
+
405
+ function getNativeEditServer() {
406
+ markNativePatchRuntimeTouched();
407
+ // Honor MIXDOG_EDIT_NATIVE_BIN (the same override the edit gating checks) so
408
+ // the gated binary and the spawned server binary cannot diverge.
409
+ const binPath = process.env.MIXDOG_EDIT_NATIVE_BIN || nativePatchBinPath();
410
+ if (!existsSync(binPath)) {
411
+ throw new Error(`native patch binary not found: ${binPath}`);
412
+ }
413
+ if (!_nativeEditServer || _nativeEditServer.exited || _nativeEditServer.binPath !== binPath) {
414
+ _nativeEditServer = new NativePatchServer(binPath);
415
+ }
416
+ return _nativeEditServer;
417
+ }
418
+
419
+ // Invariant-safe char-indexed edit over the persistent server (B2). Shares the
420
+ // NativePatchServer transport but runs on a DEDICATED instance so edit and
421
+ // patch requests never interleave their stdin framing on one stdout stream.
422
+ export async function runServerEdit({ fullPath, oldBuf, newBuf, replaceAll = false, dryRun = false, signal = null }) {
423
+ const server = getNativeEditServer();
424
+ return server.edit(fullPath, oldBuf, newBuf, { replaceAll, dryRun, signal });
425
+ }
426
+
427
+ function scheduleNativePatchPrewarm() {
428
+ if (!nativePatchPrewarmEnabled() || _nativePatchPrewarmTimer || _nativePatchServer) return;
429
+ _nativePatchPrewarmTimer = setImmediate(() => {
430
+ void (async () => {
431
+ _nativePatchPrewarmTimer = null;
432
+ const started = performance.now();
433
+ try {
434
+ // Ensure the native binary is present (local build or fetched
435
+ // prebuilt) before starting the server. Best-effort: failures
436
+ // surface as a hard error at dispatch (no JS fallback in the
437
+ // native-only path).
438
+ if (!existsSync(nativePatchBinPath())) {
439
+ try { await ensurePatchBinary(getPluginData()); } catch { /* surfaces at dispatch */ }
440
+ }
441
+ await getNativePatchServer().ping();
442
+ if (!nativePatchPersistent() && (_nativePatchServer?.waiters?.length || 0) === 0) {
443
+ _nativePatchServer?.unref();
444
+ }
445
+ if (nativePatchTraceEnabled()) {
446
+ process.stderr.write(`[patch-native-trace] prewarm_ms=${(performance.now() - started).toFixed(3)}\n`);
447
+ }
448
+ } catch (err) {
449
+ if (nativePatchTraceEnabled()) {
450
+ process.stderr.write(`[patch-native-trace] prewarm_failed=${err?.message || String(err)}\n`);
451
+ }
452
+ }
453
+ })();
454
+ });
455
+ if (_nativePatchPrewarmTimer?.unref) _nativePatchPrewarmTimer.unref();
456
+ }
457
+
458
+ scheduleNativePatchPrewarm();
459
+
460
+ function scheduleNativePatchIdleClose() {
461
+ if (nativePatchPersistent() || !_nativePatchServer) return;
462
+ if (process.versions?.bun) {
463
+ const server = _nativePatchServer;
464
+ _nativePatchServer = null;
465
+ void server?.close().catch(() => {});
466
+ return;
467
+ }
468
+ _nativePatchServer.unref();
469
+ }
470
+
471
+ export async function closeNativePatchServerForTests(options = {}) {
472
+ if (_nativePatchPrewarmTimer) {
473
+ try { clearImmediate(_nativePatchPrewarmTimer); } catch {}
474
+ _nativePatchPrewarmTimer = null;
475
+ }
476
+ const server = _nativePatchServer;
477
+ _nativePatchServer = null;
478
+ const editServer = _nativeEditServer;
479
+ _nativeEditServer = null;
480
+ await server?.close(options);
481
+ await editServer?.close(options);
482
+ }
483
+
484
+ try { globalThis.__mixdogCloseNativePatchServers = closeNativePatchServerForTests; } catch {}
485
+
486
+ // Strip the leading `a/` or `b/` prefix that `diff -u` / git emit by
487
+ // default, plus timestamp suffixes (`\t2024-...`) that some tools append
488
+ // to header lines. parsePatch already splits the name from the header
489
+ // so timestamps land in `oldHeader` / `newHeader`, but be defensive.
490
+ function stripDiffPrefix(name) {
491
+ if (!name) return name;
492
+ if (isAbsolute(name) || /^[A-Za-z]:[\\/]/.test(name)) return name;
493
+ const m = /^[ab]\/(.+)$/.exec(name);
494
+ return m ? m[1] : name;
495
+ }
496
+
497
+ function resolveEntryPath(basePath, rawName) {
498
+ const stripped = stripDiffPrefix(rawName);
499
+ const norm = normalizeInputPath(stripped);
500
+ return isAbsolute(norm) ? pathResolve(norm) : resolveAgainstCwd(norm, basePath);
501
+ }
502
+
503
+ // V4A section paths are real repository paths and never carry the unified
504
+ // diff `a/`·`b/` prefix, so resolution must NOT apply stripDiffPrefix — a
505
+ // legitimate top-level `a/` or `b/` directory would otherwise be silently
506
+ // rewritten to its child, reading/writing the wrong file.
507
+ function resolveV4AEntryPath(basePath, rawName) {
508
+ const norm = normalizeInputPath(rawName);
509
+ return isAbsolute(norm) ? pathResolve(norm) : resolveAgainstCwd(norm, basePath);
510
+ }
511
+
512
+ function resolveBasePath(cwd, basePath) {
513
+ if (!basePath) return cwd;
514
+ const norm = normalizeInputPath(basePath);
515
+ return isAbsolute(norm) ? pathResolve(norm) : resolveAgainstCwd(norm, cwd);
516
+ }
517
+
518
+ // Categorise the per-file entry. A unified diff can describe:
519
+ // - modify : both files named, oldFileName exists on disk
520
+ // - create : oldFileName === /dev/null (or file doesn't exist + hunks start at 0)
521
+ // - delete : newFileName === /dev/null
522
+ function classifyEntry(entry) {
523
+ const oldIsNull = DEV_NULL.test(entry.oldFileName || '');
524
+ const newIsNull = DEV_NULL.test(entry.newFileName || '');
525
+ if (oldIsNull && !newIsNull) return 'create';
526
+ if (!oldIsNull && newIsNull) return 'delete';
527
+ return 'modify';
528
+ }
529
+
530
+ function isPatchErrorText(text) {
531
+ return /^Error:/i.test(String(text ?? '').trimStart());
532
+ }
533
+
534
+ // Count how many source lines a hunk consumes vs produces so we can
535
+ // surface a concise `lines_changed` figure without re-diffing.
536
+ function countHunkChanges(hunks) {
537
+ let added = 0;
538
+ let removed = 0;
539
+ for (const h of hunks || []) {
540
+ for (const line of h.lines || []) {
541
+ if (line.startsWith('+')) added++;
542
+ else if (line.startsWith('-')) removed++;
543
+ }
544
+ }
545
+ return { added, removed };
546
+ }
547
+
548
+ // Header-shape pre-validator (lexical only): a missing header, a
549
+ // `..` segment, or an absolute path that does not resolve inside the
550
+ // basePath returns false. Caller wraps the negative result into a clean
551
+ // throw so unsupported headers surface as a clean error instead of
552
+ // silently degrading to a JS fallback.
553
+ function nativeHeaderSupported(entry, basePath) {
554
+ const kind = classifyEntry(entry);
555
+ const headerName = kind === 'create' ? entry.newFileName : entry.oldFileName;
556
+ if (!headerName || DEV_NULL.test(headerName)) return false;
557
+ // Reject any `..` segment on EITHER oldFileName or newFileName (every
558
+ // non-/dev/null header) — a modify whose newFileName traverses out of
559
+ // base must still be refused even when the resolved path lands inside.
560
+ for (const which of ['oldFileName', 'newFileName']) {
561
+ const raw = entry[which];
562
+ if (!raw || DEV_NULL.test(raw)) continue;
563
+ const segs = normalizeInputPath(stripDiffPrefix(raw)).split(/[\\/]+/);
564
+ if (segs.some((part) => part === '..')) return false;
565
+ }
566
+ const stripped = stripDiffPrefix(headerName);
567
+ const norm = normalizeInputPath(stripped);
568
+ if (isAbsolute(norm) || /^[A-Za-z]:[\\/]/.test(norm)) {
569
+ if (!basePath) return false;
570
+ const absHeader = pathResolve(norm);
571
+ const absBase = pathResolve(basePath);
572
+ const rel = pathRelative(absBase, absHeader);
573
+ if (!rel || rel.startsWith('..') || isAbsolute(rel)) return false;
574
+ if (rel.split(/[\\/]+/).some((part) => part === '..')) return false;
575
+ return true;
576
+ }
577
+ return true;
578
+ }
579
+
580
+ // Resolve `absPath` via fs.realpathSync, walking up to the nearest existing
581
+ // ancestor when the leaf does not yet exist (e.g. a create-mode target).
582
+ // Returns the resolved real path, or the lexically-resolved path if no
583
+ // ancestor can be realpath'd.
584
+ function realpathNearestExistingAncestor(absPath) {
585
+ let cur = pathResolve(absPath);
586
+ while (true) {
587
+ try {
588
+ return realpathSync(cur);
589
+ } catch {
590
+ const parent = pathDirname(cur);
591
+ if (!parent || parent === cur) return cur;
592
+ cur = parent;
593
+ }
594
+ }
595
+ }
596
+
597
+ // Pre-validator (throws): walks every parsed entry and enforces
598
+ // - native engine is enabled
599
+ // - native binary exists on disk
600
+ // - each header is shape-supported (no `..`, no out-of-base absolute)
601
+ // - realpath of each non-/dev/null header stays inside the real basePath
602
+ // (catches symlink/junction escapes that lexical checks miss)
603
+ // - no duplicate target paths in the patch (case-insensitive on win32)
604
+ // Returns the list of normalized entry rows the dispatcher uses to format
605
+ // output, plus the header-rewrite map for absolute-path normalization.
606
+ async function preValidateNativeBatch(parsed, basePath) {
607
+ if (!nativePatchEnabled()) {
608
+ throw new Error('apply_patch: native engine disabled via MIXDOG_PATCH_NATIVE; set it to "auto" or "1" to apply patches.');
609
+ }
610
+ const binPath = nativePatchBinPath();
611
+ if (!existsSync(binPath)) {
612
+ throw new Error(`apply_patch: native patch binary not found at ${binPath}; build native/mixdog-patch or fetch the prebuilt before invoking apply_patch.`);
613
+ }
614
+ if (!Array.isArray(parsed) || parsed.length === 0) {
615
+ throw new Error('apply_patch: patch contained no file sections');
616
+ }
617
+ await assertPathReachable(basePath);
618
+ const reachabilityPaths = [];
619
+ for (const entry of parsed) {
620
+ for (const which of ['oldFileName', 'newFileName']) {
621
+ const checkName = entry[which];
622
+ if (!checkName || DEV_NULL.test(checkName)) continue;
623
+ reachabilityPaths.push(resolveEntryPath(basePath, checkName));
624
+ }
625
+ }
626
+ await assertPathsReachable(reachabilityPaths);
627
+ let realBase;
628
+ try {
629
+ realBase = realpathSync(pathResolve(basePath));
630
+ } catch (err) {
631
+ throw new Error(`apply_patch: base_path unreadable (${err?.code || err?.message || String(err)}): ${basePath}`);
632
+ }
633
+ const entries = [];
634
+ const seenPaths = new Set();
635
+ const headerRewrites = [];
636
+ for (const entry of parsed) {
637
+ const kind = classifyEntry(entry);
638
+ const headerName = kind === 'create' ? entry.newFileName : entry.oldFileName;
639
+ if (!nativeHeaderSupported(entry, basePath)) {
640
+ const display = headerName ? normalizeOutputPath(stripDiffPrefix(headerName)) : '(unknown)';
641
+ throw new Error(`apply_patch: header ${display} is unsupported (path escapes base_path or contains \`..\`).`);
642
+ }
643
+ if (kind !== 'delete' && !(entry.hunks?.length > 0)) {
644
+ const display = headerName ? normalizeOutputPath(stripDiffPrefix(headerName)) : '(unknown)';
645
+ throw new Error(`apply_patch: entry ${display} has no hunks — patch header malformed (use \`@@ -A,B +C,D @@\` per hunk).`);
646
+ }
647
+ // Realpath each non-/dev/null header; nearest-existing-ancestor handles
648
+ // create-mode leaves that do not yet exist. A symlink/junction whose
649
+ // target escapes basePath fails here even when the lexical check above
650
+ // looked safe.
651
+ for (const which of ['oldFileName', 'newFileName']) {
652
+ const checkName = entry[which];
653
+ if (!checkName || DEV_NULL.test(checkName)) continue;
654
+ const checkFull = resolveEntryPath(basePath, checkName);
655
+ const checkReal = realpathNearestExistingAncestor(checkFull);
656
+ const checkRel = pathRelative(realBase, checkReal);
657
+ if (checkRel.split(/[\\/]+/).some((part) => part === '..') || isAbsolute(checkRel)) {
658
+ const display = normalizeOutputPath(stripDiffPrefix(checkName));
659
+ throw new Error(`apply_patch: ${display} resolves outside base_path via symlink/junction; refusing to apply.`);
660
+ }
661
+ }
662
+ const fullPath = resolveEntryPath(basePath, headerName);
663
+ const pathKey = process.platform === 'win32' ? fullPath.toLowerCase() : fullPath;
664
+ if (seenPaths.has(pathKey)) {
665
+ const display = normalizeOutputPath(stripDiffPrefix(headerName));
666
+ throw new Error(`apply_patch: duplicate target ${display} — patch lists the same path twice.`);
667
+ }
668
+ seenPaths.add(pathKey);
669
+ const displayPath = normalizeOutputPath(stripDiffPrefix(headerName));
670
+ const { added, removed } = countHunkChanges(entry.hunks);
671
+ entries.push({
672
+ kind,
673
+ fullPath,
674
+ displayPath,
675
+ added,
676
+ removed,
677
+ hunks: entry.hunks?.length || 0,
678
+ linesChanged: added + removed,
679
+ });
680
+ // Absolute-form headers must be rewritten to repo-relative before the
681
+ // native server, which joins headers to basePath, sees them.
682
+ for (const which of ['oldFileName', 'newFileName']) {
683
+ const raw = entry[which];
684
+ if (!raw || DEV_NULL.test(raw)) continue;
685
+ const stripped = stripDiffPrefix(raw);
686
+ const norm = normalizeInputPath(stripped);
687
+ if (!isAbsolute(norm) && !/^[A-Za-z]:[\\/]/.test(norm)) continue;
688
+ const rel = pathRelative(pathResolve(basePath), pathResolve(norm)).replace(/\\/g, '/');
689
+ if (!rel || rel.startsWith('..') || isAbsolute(rel)) {
690
+ const display = normalizeOutputPath(stripDiffPrefix(raw));
691
+ throw new Error(`apply_patch: absolute header ${display} does not resolve inside base_path.`);
692
+ }
693
+ headerRewrites.push({ from: raw, to: rel });
694
+ }
695
+ }
696
+ return { entries, headerRewrites };
697
+ }
698
+
699
+ // Rewrite ONLY the file-section header lines (`--- old`/`+++ new`) that
700
+ // precede each hunk so the native server, which joins headers to
701
+ // basePath, never sees an absolute header. A hunk DELETION line is `-`
702
+ // + content, so a deleted line whose body text is `-- C:/...` renders as
703
+ // `--- C:/...`; track hunk-body state by consuming each `@@ -a,b +c,d @@`
704
+ // header's declared line counts so only lines outside any hunk body are
705
+ // eligible for rewrite.
706
+ function rewriteHeaderPaths(patchStr, headerRewrites) {
707
+ if (!headerRewrites || headerRewrites.length === 0) return patchStr;
708
+ const lines = patchStr.split('\n');
709
+ let i = 0;
710
+ while (i < lines.length) {
711
+ const line = lines[i];
712
+ if (line.startsWith('@@ ')) {
713
+ const m = /^@@ -\d+(?:,(\d+))? \+\d+(?:,(\d+))? @@/.exec(line);
714
+ let oldRem = m && m[1] !== undefined ? Number(m[1]) : 1;
715
+ let newRem = m && m[2] !== undefined ? Number(m[2]) : 1;
716
+ i++;
717
+ while (i < lines.length && (oldRem > 0 || newRem > 0)) {
718
+ const body = lines[i];
719
+ const c = body.charAt(0);
720
+ if (c === ' ') { oldRem--; newRem--; }
721
+ else if (c === '-') { oldRem--; }
722
+ else if (c === '+') { newRem--; }
723
+ else if (c === '\\') { /* "" marker */ }
724
+ else break;
725
+ i++;
726
+ }
727
+ continue;
728
+ }
729
+ if (line.startsWith('--- ') || line.startsWith('+++ ')) {
730
+ const prefix = line.slice(0, 4);
731
+ const rest = line.slice(4);
732
+ const tabIdx = rest.indexOf('\t');
733
+ const pathPart = tabIdx === -1 ? rest : rest.slice(0, tabIdx);
734
+ const suffix = tabIdx === -1 ? '' : rest.slice(tabIdx);
735
+ for (const { from, to } of headerRewrites) {
736
+ if (pathPart === from) {
737
+ lines[i] = `${prefix}${to}${suffix}`;
738
+ break;
739
+ }
740
+ }
741
+ }
742
+ i++;
743
+ }
744
+ return lines.join('\n');
745
+ }
746
+
747
+ function collectUnifiedOldLines(hunk) {
748
+ const oldLines = [];
749
+ // Track whether the immediately preceding hunk line contributed an OLD
750
+ // (context/delete) line, so a following "" marker
751
+ // can flip the trailing-newline flag on the right entry. hasNewline is EXTRA
752
+ // metadata: the push condition stays IDENTICAL to before so old-line offsets
753
+ // and the change-band coordinates are unaffected.
754
+ let lastWasOld = false;
755
+ for (const raw of hunk?.lines || []) {
756
+ if (typeof raw !== 'string' || raw.length === 0) continue;
757
+ const tag = raw[0];
758
+ if (tag === '-' || tag === ' ') {
759
+ oldLines.push({ tag, line: raw.slice(1), hasNewline: true });
760
+ lastWasOld = true;
761
+ continue;
762
+ }
763
+ if (tag === '\\') {
764
+ // Unified "": the immediately preceding line
765
+ // has no trailing newline. Mirror native (main.rs:1500-1526), which flips
766
+ // the has_newline flag on the preceding line. Only apply to OLD lines —
767
+ // an add-line marker does not affect the context/delete old-line list.
768
+ if (lastWasOld && oldLines.length > 0) oldLines[oldLines.length - 1].hasNewline = false;
769
+ lastWasOld = false;
770
+ continue;
771
+ }
772
+ lastWasOld = false;
773
+ }
774
+ return oldLines;
775
+ }
776
+
777
+ // Ordered op sequence for a hunk (Context/Delete/Add), in source order. Mirrors
778
+ // native parts.ops: needed for the change-band computation because Add lines do
779
+ // not live in the old-line list yet still bound the interior context region.
780
+ // The push condition for context/delete is kept IDENTICAL to
781
+ // collectUnifiedOldLines so old-line offsets line up across both views.
782
+ function collectUnifiedOps(hunk) {
783
+ const ops = [];
784
+ for (const raw of hunk?.lines || []) {
785
+ if (typeof raw !== 'string' || raw.length === 0) continue;
786
+ const tag = raw[0];
787
+ if (tag === ' ') ops.push('context');
788
+ else if (tag === '-') ops.push('delete');
789
+ else if (tag === '+') ops.push('add');
790
+ }
791
+ return ops;
792
+ }
793
+
794
+ // Mirror native evaluate_fuzzy_candidate change-band (main.rs:1744-1766): map
795
+ // the ordered op sequence into a doubled+shifted old-index coordinate space.
796
+ // old_cursor counts consumed OLD lines; Delete at cursor o -> (o+1)*2, Add at
797
+ // cursor k -> k*2+1 (strictly between neighbouring old lines). first/last span
798
+ // over Adds AND Deletes. {first:null,last:null} means no changes at all.
799
+ function computeUnifiedChangeBand(ops) {
800
+ let first = null;
801
+ let last = null;
802
+ let oldCursor = 0;
803
+ for (const op of ops) {
804
+ if (op === 'context') {
805
+ oldCursor += 1;
806
+ } else if (op === 'delete') {
807
+ const pos = (oldCursor + 1) * 2;
808
+ first = first === null ? pos : Math.min(first, pos);
809
+ last = last === null ? pos : Math.max(last, pos);
810
+ oldCursor += 1;
811
+ } else {
812
+ const pos = oldCursor * 2 + 1;
813
+ first = first === null ? pos : Math.min(first, pos);
814
+ last = last === null ? pos : Math.max(last, pos);
815
+ }
816
+ }
817
+ return { first, last };
818
+ }
819
+
820
+ function firstMeaningfulUnifiedHunkLine(hunk) {
821
+ for (const { line } of collectUnifiedOldLines(hunk)) {
822
+ if (line.trim()) return { line, preferredLine: Math.max(0, (Number(hunk?.oldStart) || 1) - 1) };
823
+ }
824
+ return null;
825
+ }
826
+
827
+ // First FAILING old line within a hunk — not its first line. Anchor at the
828
+ // deepest-matching prefix (declared oldStart first, then any position where
829
+ // the first old line matches) and report the line where the match breaks.
830
+ // Without this the rejection message shows the hunk's FIRST context line,
831
+ // which often matches perfectly, next to a "nearest" source line identical
832
+ // to it — telling the caller nothing about the actual mismatch.
833
+ function firstFailingUnifiedHunkLineDetail(sourceLines, hunk) {
834
+ const oldLines = collectUnifiedOldLines(hunk);
835
+ if (!oldLines.length) return null;
836
+ const lineEq = (actual, expected) => {
837
+ const ab = toLineBytes(actual);
838
+ const eb = toLineBytes(expected);
839
+ return ab.equals(eb) || byteTrimPatchWhitespace(ab).equals(byteTrimPatchWhitespace(eb));
840
+ };
841
+ const prefixDepth = (start) => {
842
+ let d = 0;
843
+ while (d < oldLines.length && start + d < sourceLines.length && lineEq(sourceLines[start + d], oldLines[d].line)) d += 1;
844
+ return d;
845
+ };
846
+ const declared = Math.max(0, (Number(hunk?.oldStart) || 1) - 1);
847
+ const candidates = [];
848
+ if (declared < sourceLines.length) candidates.push(declared);
849
+ for (let i = 0; i < sourceLines.length && candidates.length < 8; i++) {
850
+ if (i !== declared && lineEq(sourceLines[i], oldLines[0].line)) candidates.push(i);
851
+ }
852
+ let best = null;
853
+ for (const start of candidates) {
854
+ const depth = prefixDepth(start);
855
+ if (depth >= oldLines.length) continue;
856
+ if (!best || depth > best.depth) best = { start, depth };
857
+ }
858
+ // depth 0 → the hunk's first line itself never anchored; the existing
859
+ // first-meaningful-line message is already the right diagnostic there.
860
+ if (!best || best.depth === 0) return null;
861
+ return {
862
+ line: oldLines[best.depth].line,
863
+ preferredLine: best.start + best.depth,
864
+ };
865
+ }
866
+
867
+ function firstMeaningfulUnifiedEntryLine(entry) {
868
+ const hunks = Array.isArray(entry?.hunks) ? entry.hunks : [];
869
+ for (const hunk of hunks) {
870
+ const expected = firstMeaningfulUnifiedHunkLine(hunk);
871
+ if (expected) return expected;
872
+ }
873
+ return null;
874
+ }
875
+
876
+ // --- Byte-level diagnostic-matcher substrate (diagnostics ONLY) ---------------
877
+ // The native engine compares RAW BYTES (main.rs:1867-1875 exact, 1891-1899 ws,
878
+ // 1936-1944 normalize). The JS diagnostic matcher mirrors that on Buffer line
879
+ // slices so invalid-UTF-8 source bytes are NOT pre-collapsed to U+FFFD. These
880
+ // helpers feed unifiedOldLinesMatchAt / findFirstFailingUnifiedHunk and nothing
881
+ // on the V4A conversion path.
882
+
883
+ // Coerce a matcher line value to bytes. Buffer (byte source view OR a raw-byte
884
+ // expected line injected by a test) is used as-is; a string (parsed-patch
885
+ // expected body, already valid UTF-8 — or a legacy/test plain source array) is
886
+ // encoded as UTF-8. Mirrors the fact that native holds both sides as bytes.
887
+ function toLineBytes(v) {
888
+ return Buffer.isBuffer(v) ? v : Buffer.from(String(v ?? ''), 'utf8');
889
+ }
890
+
891
+ // Byte version of trim_patch_ws (main.rs:1891-1899): strip ONLY 0x20/0x09 from
892
+ // BOTH ends, returning a (zero-copy) subview.
893
+ function byteTrimPatchWhitespace(buf) {
894
+ let start = 0;
895
+ let end = buf.length;
896
+ while (start < end && (buf[start] === 0x20 || buf[start] === 0x09)) start++;
897
+ while (end > start && (buf[end - 1] === 0x20 || buf[end - 1] === 0x09)) end--;
898
+ return buf.subarray(start, end);
899
+ }
900
+
901
+ // Reused fatal UTF-8 decoder: mirror native's `from_utf8` validity gate
902
+ // (main.rs:1936-1944). Returns the decoded string when the bytes are valid
903
+ // UTF-8, else null (the normalize tier is then refused for that line).
904
+ const __utf8FatalDecoder = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true });
905
+ function decodeValidUtf8OrNull(buf) {
906
+ try {
907
+ return __utf8FatalDecoder.decode(buf);
908
+ } catch {
909
+ return null;
910
+ }
911
+ }
912
+
913
+ function unifiedOldLinesMatchAt(sourceLines, oldLines, startIdx, fuzz, band) {
914
+ if (startIdx < 0 || startIdx + oldLines.length > sourceLines.length) return null;
915
+ let fuzzUsed = 0;
916
+ let normCount = 0;
917
+ // A source line has a trailing newline EXCEPT the final line when the file
918
+ // body did not end in '\n' (metadata stashed on the array by
919
+ // splitTextLinesForPatch). Default true when metadata is absent (callers that
920
+ // pass a plain literal array, e.g. tests/legacy) so behaviour is unchanged
921
+ // unless they opt in by setting hasFinalNewline.
922
+ const srcFinalNewline = sourceLines.hasFinalNewline !== false;
923
+ const lastSrcIdx = sourceLines.length - 1;
924
+ for (let offset = 0; offset < oldLines.length; offset++) {
925
+ const expected = oldLines[offset];
926
+ const actualIdx = startIdx + offset;
927
+ // Byte substrate (diagnostics-only): compare RAW BYTES like native instead
928
+ // of JS strings, so invalid-UTF-8 source bytes are not pre-collapsed to
929
+ // U+FFFD. `actualBytes` is a per-line Buffer slice when sourceLines is the
930
+ // byte view (formatNativeFailureContext) and an on-the-fly UTF-8 encode for
931
+ // a plain-string/legacy/test source array; `expectedBytes` is the UTF-8
932
+ // encoding of the parsed-patch old line (or a raw Buffer when a test injects
933
+ // invalid bytes directly).
934
+ const actualBytes = toLineBytes(sourceLines[actualIdx]);
935
+ const expectedBytes = toLineBytes(expected.line);
936
+ // Per-line newline guard (mirror native exact-newline guard main.rs:1867-1875
937
+ // and ws/normalize guards 1891-1899/1928-1947): a tier matches ONLY when the
938
+ // expected and actual trailing-newline state agree. expected.hasNewline is
939
+ // the unified old line's flag (default true, cleared by a "\ No newline"
940
+ // marker); the source line lacks a newline only when it is the final line of
941
+ // a no-trailing-newline file. When they differ, NO tier accepts this line —
942
+ // for a delete line this then falls through to reject (mirror native
943
+ // 1832-1835).
944
+ const expectedNL = expected.hasNewline !== false;
945
+ const actualNL = !(actualIdx === lastSrcIdx && !srcFinalNewline);
946
+ const newlineOk = expectedNL === actualNL;
947
+ // Exact tier (main.rs:1867-1875): raw byte equality.
948
+ if (newlineOk && actualBytes.equals(expectedBytes)) continue;
949
+ // Whitespace tier: mirror native (gate main.rs:1787-1790) which accepts a
950
+ // Context OR Delete line that matches after trimming space/tab (0x20/0x09)
951
+ // from BOTH ends (byte trim, main.rs:1891-1899). Costs no fuzz and does NOT
952
+ // increment normCount.
953
+ if (newlineOk && fuzz > 0 && (expected.tag === ' ' || expected.tag === '-') && byteTrimPatchWhitespace(actualBytes).equals(byteTrimPatchWhitespace(expectedBytes))) continue;
954
+ // Unicode-normalization tier: mirror the native engine, which accepts a
955
+ // context OR delete line that matches only after typographic normalization
956
+ // (dashes/quotes/NBSP) at zero fuzz cost (native gate: Context|Delete). The
957
+ // count of such normalization-only matches is tracked so the candidate
958
+ // selector can prefer a block that anchored without normalization. Without
959
+ // this the diagnostic matcher mislabels which hunk first failed when the
960
+ // source carries exotic code-points.
961
+ //
962
+ // UTF-8 validity guard (exact mirror of native main.rs:1936-1944, which
963
+ // normalizes ONLY when BOTH bodies are valid UTF-8): decode each side with a
964
+ // fatal TextDecoder; if EITHER side is invalid UTF-8 the normalize tier is
965
+ // refused (matching native's `from_utf8` gate). This replaces the old
966
+ // string-level U+FFFD heuristic — a VALID literal U+FFFD present in both
967
+ // sides now normalize-matches, exactly as native does.
968
+ if (
969
+ newlineOk
970
+ && fuzz > 0
971
+ && (expected.tag === ' ' || expected.tag === '-')
972
+ ) {
973
+ const actualStr = decodeValidUtf8OrNull(actualBytes);
974
+ const expectedStr = actualStr === null ? null : decodeValidUtf8OrNull(expectedBytes);
975
+ if (
976
+ actualStr !== null
977
+ && expectedStr !== null
978
+ && normalizeTypographic(actualStr) === normalizeTypographic(expectedStr)
979
+ ) {
980
+ normCount++;
981
+ continue;
982
+ }
983
+ }
984
+ if (fuzz > 0 && expected.tag === ' ') {
985
+ // Mirror native (main.rs:1808-1830): content drift on a context line is
986
+ // only fuzz-consumable when the line is OUTER context (before the first
987
+ // change or after the last). Interior context (strictly inside the change
988
+ // band) must match exactly/ws/normalize — content drift there means the
989
+ // hunk is binding to a different block, so reject. ctx_pos maps the old
990
+ // line at this offset into the band coordinate space: (offset + 1) * 2.
991
+ const ctxPos = (offset + 1) * 2;
992
+ const isOuter = (!band || band.first === null || band.last === null)
993
+ ? true
994
+ : (ctxPos < band.first || ctxPos > band.last);
995
+ if (!isOuter) return null;
996
+ fuzzUsed++;
997
+ if (fuzzUsed <= fuzz) continue;
998
+ }
999
+ return null;
1000
+ }
1001
+ return { fuzzUsed, normCount };
1002
+ }
1003
+
1004
+ function findUnifiedHunkMatch(sourceLines, hunk, minStartIdx, fuzz) {
1005
+ const oldLines = collectUnifiedOldLines(hunk);
1006
+ const band = computeUnifiedChangeBand(collectUnifiedOps(hunk));
1007
+ const oldStart = Math.max(0, (Number(hunk?.oldStart) || 1) - 1);
1008
+ if (oldLines.length === 0) {
1009
+ const insertIdx = Math.max(0, Number(hunk?.oldStart) || 0);
1010
+ return insertIdx >= minStartIdx && insertIdx <= sourceLines.length ? { start: insertIdx, end: insertIdx } : null;
1011
+ }
1012
+ if (oldStart >= minStartIdx && unifiedOldLinesMatchAt(sourceLines, oldLines, oldStart, 0, band) !== null) {
1013
+ return { start: oldStart, end: oldStart + oldLines.length };
1014
+ }
1015
+ if (fuzz <= 0) return null;
1016
+ let best = null;
1017
+ for (let start = minStartIdx; start <= sourceLines.length - oldLines.length; start++) {
1018
+ const matched = unifiedOldLinesMatchAt(sourceLines, oldLines, start, fuzz, band);
1019
+ if (matched === null) continue;
1020
+ const { fuzzUsed, normCount } = matched;
1021
+ const distance = Math.abs(start - oldStart);
1022
+ // Ordering mirrors native: lower fuzz, THEN fewer normalization-only
1023
+ // matches, THEN smaller distance. A block that anchored without
1024
+ // normalization always beats a nearer one that needed it.
1025
+ if (
1026
+ !best ||
1027
+ fuzzUsed < best.fuzzUsed ||
1028
+ (fuzzUsed === best.fuzzUsed && normCount < best.normCount) ||
1029
+ (fuzzUsed === best.fuzzUsed && normCount === best.normCount && distance < best.distance)
1030
+ ) {
1031
+ best = { start, distance, fuzzUsed, normCount };
1032
+ }
1033
+ }
1034
+ return best ? { start: best.start, end: best.start + oldLines.length } : null;
1035
+ }
1036
+
1037
+ function findFirstFailingUnifiedHunk(entry, sourceLines, fuzz) {
1038
+ const hunks = Array.isArray(entry?.hunks) ? entry.hunks : [];
1039
+ let minStartIdx = 0;
1040
+ for (const hunk of hunks) {
1041
+ const match = findUnifiedHunkMatch(sourceLines, hunk, minStartIdx, fuzz);
1042
+ if (!match) return hunk;
1043
+ minStartIdx = Math.max(minStartIdx, match.end);
1044
+ }
1045
+ return null;
1046
+ }
1047
+
1048
+ function nativeFailurePathCandidates(parsed) {
1049
+ const candidates = new Set();
1050
+ for (const entry of Array.isArray(parsed) ? parsed : []) {
1051
+ const kind = classifyEntry(entry);
1052
+ const headerName = kind === 'create' ? entry.newFileName : entry.oldFileName;
1053
+ if (!headerName) continue;
1054
+ const stripped = stripDiffPrefix(headerName);
1055
+ const display = normalizeOutputPath(stripped);
1056
+ candidates.add(headerName);
1057
+ candidates.add(stripped);
1058
+ candidates.add(display);
1059
+ if (display) {
1060
+ candidates.add(`a/${display}`);
1061
+ candidates.add(`b/${display}`);
1062
+ }
1063
+ }
1064
+ return [...candidates].filter(Boolean).sort((a, b) => b.length - a.length);
1065
+ }
1066
+
1067
+ function extractNativeFailurePath(message, parsed) {
1068
+ const text = String(message || '').trim();
1069
+ if (!text) return '';
1070
+ const candidates = nativeFailurePathCandidates(parsed);
1071
+ for (const candidate of candidates) {
1072
+ if (text.startsWith(`${candidate}:`)) return candidate;
1073
+ }
1074
+ const hunkMatch = /(?:^|\b)hunk rejected in (.+?)(?: \(|$)/i.exec(text);
1075
+ if (hunkMatch?.[1]) return hunkMatch[1].trim();
1076
+ for (const candidate of candidates) {
1077
+ if (text.includes(candidate)) return candidate;
1078
+ }
1079
+ return '';
1080
+ }
1081
+
1082
+ function nativeFailureMatchesEntry(entry, failedPath) {
1083
+ if (!failedPath) return true;
1084
+ const kind = classifyEntry(entry);
1085
+ const headerName = kind === 'create' ? entry.newFileName : entry.oldFileName;
1086
+ if (!headerName) return false;
1087
+ const failed = normalizeOutputPath(stripDiffPrefix(failedPath));
1088
+ const display = normalizeOutputPath(stripDiffPrefix(headerName));
1089
+ if (!failed || !display) return false;
1090
+ // Exact match: single-file / relative-header case.
1091
+ if (failed === display) return true;
1092
+ // Path-segment-aware containment, both directions:
1093
+ // - display.endsWith('/'+failed): an accepted ABSOLUTE header is rewritten
1094
+ // to a repo-relative path before native dispatch, so the native failure
1095
+ // reports the RELATIVE path while this parsed entry still carries its
1096
+ // ORIGINAL absolute (or longer) path — `.../src/b.js` matches `src/b.js`.
1097
+ // - failed.endsWith('/'+display): the inverse (native path longer than the
1098
+ // parsed header).
1099
+ // Anchoring on a leading `/` keeps this segment-boundary aware, so
1100
+ // `.../notsrc/app.js` never matches `src/app.js` via raw substring.
1101
+ return display.endsWith(`/${failed}`) || failed.endsWith(`/${display}`);
1102
+ }
1103
+
1104
+ function formatNativeFailureContext(parsed, basePath, failedPath = '', options = {}) {
1105
+ const entries = Array.isArray(parsed) ? parsed : [];
1106
+ const entry = entries.find((candidate) => classifyEntry(candidate) !== 'create' && nativeFailureMatchesEntry(candidate, failedPath))
1107
+ || entries.find((candidate) => classifyEntry(candidate) !== 'create');
1108
+ const headerName = entry?.oldFileName;
1109
+ const displayPath = headerName ? normalizeOutputPath(stripDiffPrefix(headerName)) : '';
1110
+ const fuzz = Number.isFinite(options?.fuzz) && options.fuzz > 0 ? Math.floor(options.fuzz) : 0;
1111
+ // Two SEPARATE views of the same source file (this is the ONLY shared-source
1112
+ // site): `sourceLines` stays a decoded UTF-8 string[] for the downstream hint
1113
+ // heuristics (nearestPatchLineHint, .trim(), normalizeTypographic below) which
1114
+ // are inherently string-based; `sourceByteLines` is an independent raw-byte
1115
+ // view (per-line Buffer slices) for the byte-parity diagnostic matcher so
1116
+ // invalid-UTF-8 bytes are not pre-collapsed to U+FFFD. The V4A conversion path
1117
+ // (splitTextLinesForPatch at ~1644/~1660 feeding findLineSequence) is NOT
1118
+ // touched — it keeps reading utf8 strings.
1119
+ let sourceLines = null;
1120
+ let sourceByteLines = null;
1121
+ try {
1122
+ const fullPath = resolveEntryPath(basePath, entry.oldFileName);
1123
+ const raw = readFileSync(fullPath); // Buffer — no 'utf8' decode
1124
+ sourceByteLines = splitBufferLinesForPatch(raw);
1125
+ sourceLines = splitTextLinesForPatch(raw.toString('utf8'));
1126
+ } catch {}
1127
+ const failingHunk = sourceByteLines ? findFirstFailingUnifiedHunk(entry, sourceByteLines, fuzz) : null;
1128
+ const failingDetail = (failingHunk && sourceByteLines)
1129
+ ? firstFailingUnifiedHunkLineDetail(sourceByteLines, failingHunk)
1130
+ : null;
1131
+ const expected = failingDetail || firstMeaningfulUnifiedHunkLine(failingHunk) || firstMeaningfulUnifiedEntryLine(entry);
1132
+ if (!entry || !expected?.line) return '';
1133
+ const expectedText = JSON.stringify(compactPatchPreviewLine(expected.line));
1134
+ let nearest = '';
1135
+ let normalizeHint = '';
1136
+ if (sourceLines) {
1137
+ nearest = nearestPatchLineHint(sourceLines, expected.line, expected.preferredLine);
1138
+ // Typographic-mismatch hint: if the expected context line matches a source
1139
+ // line ONLY after Unicode normalization, the source likely carries
1140
+ // typographic dashes/quotes/NBSP that an ASCII patch can't match exactly.
1141
+ const wantNorm = normalizeTypographic(expected.line);
1142
+ if (wantNorm) {
1143
+ for (let i = 0; i < sourceLines.length; i++) {
1144
+ if (sourceLines[i] === expected.line) break; // exact match exists; not a normalization issue
1145
+ // normalizeTypographic also .trim()s, so a pure trailing/leading
1146
+ // whitespace drift would otherwise fire this typographic hint. Require
1147
+ // a genuine code-point difference: the lines must still differ after a
1148
+ // plain trim, yet become equal after typographic normalization.
1149
+ if (
1150
+ sourceLines[i].trim() !== expected.line.trim()
1151
+ && normalizeTypographic(sourceLines[i]) === wantNorm
1152
+ ) {
1153
+ normalizeHint = `context matches after Unicode normalization at line ${i + 1} — source may contain typographic dashes/quotes/NBSP`;
1154
+ break;
1155
+ }
1156
+ }
1157
+ }
1158
+ }
1159
+ return ` expected first old/context line${displayPath ? ` in ${displayPath}` : ''}: ${expectedText}${nearest ? `; ${nearest}` : ''}${normalizeHint ? `; ${normalizeHint}` : ''}; use exact current lines, no stubs.`;
1160
+ }
1161
+
1162
+ // Dispatch the (already validated, header-rewritten) patch to the native
1163
+ // engine. Throws on any native error; on success returns the formatted
1164
+ // human-readable response string. Never silently falls back to JS — the
1165
+ // caller MUST surface throws as `Error: ...` strings.
1166
+ async function dispatchNativePatch({ entries, basePath, nativePatchStr, fuzz, rejectPartial, dryRun, readStateScope, signal, parsed }) {
1167
+ const nativeStart = performance.now();
1168
+ let stats;
1169
+ try {
1170
+ stats = await getNativePatchServer().apply(basePath, nativePatchStr, { fuzz, rejectPartial, dryRun, signal });
1171
+ } catch (err) {
1172
+ scheduleNativePatchIdleClose();
1173
+ const msg = err?.message || String(err);
1174
+ const failedPath = extractNativeFailurePath(msg, parsed);
1175
+ return `Error: native patch failed — ${msg}${formatNativeFailureContext(parsed, basePath, failedPath, { fuzz })}`;
1176
+ }
1177
+ const afterInvalidateStart = performance.now();
1178
+ // Only invalidate / snapshot entries that actually landed. In isolation
1179
+ // mode (OK_PARTIAL) skipped entries still have their original disk state
1180
+ // and must not be re-snapshotted.
1181
+ const failedDisplaySet = new Set();
1182
+ for (const f of stats.failures || []) {
1183
+ if (!f?.path) continue;
1184
+ failedDisplaySet.add(normalizeOutputPath(f.path));
1185
+ failedDisplaySet.add(normalizeOutputPath(stripDiffPrefix(f.path)));
1186
+ }
1187
+ const writtenEntries = entries.filter((entry) => !failedDisplaySet.has(entry.displayPath));
1188
+ const fullPaths = writtenEntries.map((entry) => entry.fullPath);
1189
+ if (!dryRun) invalidateBuiltinResultCache(fullPaths);
1190
+ const afterInvalidate = performance.now();
1191
+ if (!dryRun) markCodeGraphDirtyPaths(fullPaths);
1192
+ const afterDirty = performance.now();
1193
+ if (!dryRun) {
1194
+ for (let i = 0; i < writtenEntries.length; i++) {
1195
+ const entry = writtenEntries[i];
1196
+ if (entry.kind === 'delete') {
1197
+ clearReadSnapshotForPath(entry.fullPath, readStateScope);
1198
+ } else {
1199
+ const snapshotMeta = {
1200
+ source: 'apply_patch_native',
1201
+ isPartialView: false,
1202
+ };
1203
+ const contentHash = stats.contentHashes?.[i] || null;
1204
+ if (contentHash) snapshotMeta.contentHash = contentHash;
1205
+ recordReadSnapshotForPath(entry.fullPath, readStateScope, snapshotMeta);
1206
+ }
1207
+ }
1208
+ }
1209
+ const afterSnapshot = performance.now();
1210
+ ioTrace('apply_patch_native', {
1211
+ files: writtenEntries.length,
1212
+ dryRun,
1213
+ partial: stats.partial,
1214
+ failed: stats.failures.length,
1215
+ roundtripMs: Number(stats.roundtripMs.toFixed(3)),
1216
+ rustTotalMs: Number(stats.totalMs.toFixed(3)),
1217
+ invalidateMs: Number((afterInvalidate - afterInvalidateStart).toFixed(3)),
1218
+ dirtyMs: Number((afterDirty - afterInvalidate).toFixed(3)),
1219
+ snapshotMs: Number((afterSnapshot - afterDirty).toFixed(3)),
1220
+ contentHashes: (stats.contentHashes || []).filter(Boolean).length,
1221
+ });
1222
+ if (nativePatchTraceEnabled()) {
1223
+ process.stderr.write(
1224
+ `[patch-native-trace] files=${writtenEntries.length} partial=${stats.partial ? 1 : 0} failed=${stats.failures.length} roundtrip_ms=${stats.roundtripMs.toFixed(3)} rust_total_ms=${stats.totalMs.toFixed(3)} rust_hash_ms=${stats.hashMs.toFixed(3)} invalidate_ms=${(afterInvalidate - afterInvalidateStart).toFixed(3)} dirty_ms=${(afterDirty - afterInvalidate).toFixed(3)} snapshot_ms=${(afterSnapshot - afterDirty).toFixed(3)} total_js_ms=${(afterSnapshot - nativeStart).toFixed(3)} content_hashes=${(stats.contentHashes || []).filter(Boolean).length}\n`
1225
+ );
1226
+ }
1227
+ if (patchTraceEnabled()) {
1228
+ process.stderr.write(`[patch-native] applied files=${writtenEntries.length} partial=${stats.partial ? 1 : 0} ms=${stats.totalMs.toFixed(3)}\n`);
1229
+ }
1230
+ scheduleNativePatchIdleClose();
1231
+ const verb = dryRun ? 'checked' : 'applied';
1232
+ const verbLabel = dryRun ? 'Checked' : 'Applied';
1233
+ const countLabel = (count, singular, plural = `${singular}s`) => `${count} ${count === 1 ? singular : plural}`;
1234
+ const kindLabel = (kind) => {
1235
+ const text = String(kind || '').trim();
1236
+ return text ? `${text.charAt(0).toUpperCase()}${text.slice(1).toLowerCase()}` : 'Update';
1237
+ };
1238
+ const summary = stats.partial
1239
+ ? `Error: Patch Partially ${verbLabel} (${countLabel(writtenEntries.length, 'File')} ${verb} · ${countLabel(stats.failures.length, 'File')} Skipped) (Native)`
1240
+ : `${verbLabel} ${countLabel(writtenEntries.length, 'File')} (Native)${dryRun ? ' Dry Run' : ''}`;
1241
+ const lines = [summary];
1242
+ for (const entry of writtenEntries) {
1243
+ const detail = `+${countLabel(entry.added || 0, 'Line')} · -${countLabel(entry.removed || 0, 'Line')}`;
1244
+ lines.push(` OK ${kindLabel(entry.kind)} ${entry.displayPath} — ${detail}`);
1245
+ }
1246
+ for (const f of stats.failures || []) {
1247
+ lines.push(` SKIP ${f.path || '(unknown)'} — ${f.reason}${formatNativeFailureContext(parsed, basePath, f.path, { fuzz })}`);
1248
+ }
1249
+ return lines.join('\n');
1250
+ }
1251
+
1252
+ // Strip BOM + normalize CRLF→LF only. Idempotent and structural — no
1253
+ // hunk metadata is rewritten.
1254
+ function prepareInput(patchStr) {
1255
+ return String(patchStr).replace(/^\uFEFF/, '').replace(/\r\n/g, '\n');
1256
+ }
1257
+
1258
+ function isCodexApplyPatchEnvelope(patchStr) {
1259
+ const text = prepareInput(patchStr).trimStart();
1260
+ return text.startsWith('*** Begin Patch')
1261
+ || text.startsWith('*** Add File:')
1262
+ || text.startsWith('*** Update File:')
1263
+ || text.startsWith('*** Delete File:');
1264
+ }
1265
+
1266
+ function isV4APatchInput(patchStr, format) {
1267
+ return String(format || '').toLowerCase() === 'v4a'
1268
+ || isCodexApplyPatchEnvelope(patchStr);
1269
+ }
1270
+
1271
+ const UNIFIED_HUNK_HEADER_RE = /^@@ -\d+(?:,\d+)? \+\d+(?:,\d+)? @@/;
1272
+ const UNIFIED_HUNK_HEADER_CAPTURE_RE = /^@@ -\d+(?:,\d+)? \+\d+(?:,\d+)? @@(.*)$/;
1273
+
1274
+ function hasUnifiedBareV4AHunk(patchStr) {
1275
+ const text = prepareInput(patchStr);
1276
+ if (!/^--- /m.test(text) || !/^\+\+\+ /m.test(text)) return false;
1277
+ return text.split('\n').some((line) => line.startsWith('@@') && !UNIFIED_HUNK_HEADER_RE.test(line));
1278
+ }
1279
+
1280
+ function isUnifiedHunkCountError(err) {
1281
+ const message = String(err?.message || err || '');
1282
+ return /Hunk at line .*more lines than expected|Hunk at line .*less lines than expected|expected \d+ old lines|line count did not match/i.test(message);
1283
+ }
1284
+
1285
+ function canFallbackCountedUnified(patchStr, requestedFormat, err) {
1286
+ if (requestedFormat === 'unified') return false;
1287
+ if (isV4APatchInput(patchStr, requestedFormat)) return false;
1288
+ const text = prepareInput(patchStr);
1289
+ return /^--- /m.test(text)
1290
+ && /^\+\+\+ /m.test(text)
1291
+ && UNIFIED_HUNK_HEADER_RE.test(text.split('\n').find((line) => line.startsWith('@@')) || '')
1292
+ && isUnifiedHunkCountError(err);
1293
+ }
1294
+
1295
+ function planApplyPatchMutationRoute(args, patchStr, requestedFormat) {
1296
+ const v4aInput = isV4APatchInput(patchStr, requestedFormat)
1297
+ || (requestedFormat !== 'unified' && hasUnifiedBareV4AHunk(patchStr));
1298
+ return {
1299
+ sourceTool: 'apply_patch',
1300
+ engine: v4aInput ? 'v4a-patch' : 'unified-patch',
1301
+ reason: 'direct',
1302
+ };
1303
+ }
1304
+
1305
+ function wrapPatchMutationOutput(text, plan, extras = {}) {
1306
+ if (isPatchErrorText(text)) return text;
1307
+ return wrapMutationRouteOutput(text, plan, extras);
1308
+ }
1309
+
1310
+ function stripPatchPathMetadata(rawPath) {
1311
+ let text = String(rawPath || '').trim();
1312
+ if (!text) return '';
1313
+ const tabIdx = text.indexOf('\t');
1314
+ if (tabIdx !== -1) text = text.slice(0, tabIdx).trimEnd();
1315
+ const quote = text[0];
1316
+ if ((quote === '"' || quote === "'") && text.length > 1) {
1317
+ const end = text.indexOf(quote, 1);
1318
+ if (end > 0) text = text.slice(1, end);
1319
+ }
1320
+ return text;
1321
+ }
1322
+
1323
+ function stripV4APathHeader(line, prefix) {
1324
+ return stripPatchPathMetadata(String(line || '').slice(prefix.length));
1325
+ }
1326
+
1327
+ function normaliseV4APath(rawPath) {
1328
+ const p = stripPatchPathMetadata(rawPath);
1329
+ if (!p) return '';
1330
+ return p.replace(/^["']|["']$/g, '').replace(/\\/g, '/');
1331
+ }
1332
+
1333
+ function normaliseV4AAnchor(rawAnchor) {
1334
+ return String(rawAnchor || '').replace(/\s*@@\s*$/, '').trim();
1335
+ }
1336
+
1337
+ function stripV4AMovePathHeader(line) {
1338
+ return normaliseV4APath(String(line || '').slice(V4A_MOVE_TO_PREFIX.length));
1339
+ }
1340
+
1341
+ function isV4AEndOfFileMarker(rawLine) {
1342
+ return String(rawLine || '').trim() === V4A_EOF_MARKER;
1343
+ }
1344
+
1345
+ function v4aEnsureUpdateHunk(current, pendingAnchors) {
1346
+ return { anchors: pendingAnchors.slice(), lines: [] };
1347
+ }
1348
+
1349
+ function v4aPushBlankContextLine(currentHunk, pendingAnchors) {
1350
+ if (!currentHunk) currentHunk = v4aEnsureUpdateHunk(null, pendingAnchors);
1351
+ currentHunk.lines.push(' ');
1352
+ return currentHunk;
1353
+ }
1354
+
1355
+ function v4aMarkHunkEndOfFile(currentHunk, finishHunk) {
1356
+ if (!currentHunk || currentHunk.lines.length === 0) {
1357
+ throw new Error('V4A update hunk does not contain any lines before *** End of File');
1358
+ }
1359
+ currentHunk.isEndOfFile = true;
1360
+ finishHunk();
1361
+ }
1362
+
1363
+ // Split a patch string into lines, dropping the single trailing empty element
1364
+ // produced by the patch's terminal newline. Invariant: a well-formed patch
1365
+ // ends with "\n", so `"...\n".split("\n")` always yields a spurious final ""
1366
+ // that is a line *terminator*, not a content line. Absorbing it as a blank
1367
+ // context line corrupts the last hunk (phantom trailing "" in oldLines) and
1368
+ // breaks anchoring whenever the matched source region is not followed by a
1369
+ // blank line. A genuine trailing blank context line survives as the
1370
+ // second-to-last element, so only the terminator artifact is removed.
1371
+ function splitPatchLines(patchStr) {
1372
+ const lines = prepareInput(patchStr).split('\n');
1373
+ if (lines.length > 0 && lines[lines.length - 1] === '') lines.pop();
1374
+ return lines;
1375
+ }
1376
+
1377
+ function parseV4APatch(patchStr) {
1378
+ const lines = splitPatchLines(patchStr);
1379
+ const files = [];
1380
+ let current = null;
1381
+ let pendingAnchors = [];
1382
+ let currentHunk = null;
1383
+
1384
+ const finishHunk = () => {
1385
+ if (!current || !currentHunk) return;
1386
+ if (currentHunk.lines.length > 0) current.hunks.push(currentHunk);
1387
+ currentHunk = null;
1388
+ };
1389
+ const finishFile = () => {
1390
+ finishHunk();
1391
+ current = null;
1392
+ pendingAnchors = [];
1393
+ };
1394
+ const startFile = (kind, path) => {
1395
+ finishFile();
1396
+ current = { kind, path: normaliseV4APath(path), hunks: [], lines: [], movePath: null };
1397
+ files.push(current);
1398
+ };
1399
+
1400
+ for (const rawLine of lines) {
1401
+ if (rawLine === '*** Begin Patch' || rawLine === '*** End Patch') continue;
1402
+ if (rawLine.startsWith('*** Update File:')) {
1403
+ startFile('update', stripV4APathHeader(rawLine, '*** Update File:'));
1404
+ continue;
1405
+ }
1406
+ if (rawLine.startsWith('*** Add File:')) {
1407
+ startFile('add', stripV4APathHeader(rawLine, '*** Add File:'));
1408
+ continue;
1409
+ }
1410
+ if (rawLine.startsWith('*** Delete File:')) {
1411
+ startFile('delete', stripV4APathHeader(rawLine, '*** Delete File:'));
1412
+ continue;
1413
+ }
1414
+ if (!current) {
1415
+ throw new Error(`V4A patch line appears before a file header: ${rawLine}`);
1416
+ }
1417
+ if (current.kind === 'update' && rawLine.startsWith(V4A_MOVE_TO_PREFIX)) {
1418
+ if (current.movePath) {
1419
+ throw new Error(`V4A patch lists multiple ${V4A_MOVE_TO_PREFIX} directives for ${current.path}`);
1420
+ }
1421
+ const dest = stripV4AMovePathHeader(rawLine);
1422
+ if (!dest) throw new Error('V4A patch contains an empty move destination path');
1423
+ current.movePath = dest;
1424
+ continue;
1425
+ }
1426
+ if (current.kind === 'add') {
1427
+ current.lines.push(rawLine.startsWith('+') ? rawLine.slice(1) : rawLine);
1428
+ continue;
1429
+ }
1430
+ if (current.kind === 'delete') {
1431
+ continue;
1432
+ }
1433
+ if (rawLine === '') {
1434
+ if (currentHunk) currentHunk = v4aPushBlankContextLine(currentHunk, pendingAnchors);
1435
+ continue;
1436
+ }
1437
+ if (isV4AEndOfFileMarker(rawLine)) {
1438
+ v4aMarkHunkEndOfFile(currentHunk, finishHunk);
1439
+ currentHunk = null;
1440
+ continue;
1441
+ }
1442
+ if (rawLine.startsWith('@@')) {
1443
+ const anchor = normaliseV4AAnchor(rawLine.slice(2));
1444
+ if (currentHunk && currentHunk.lines.length > 0) finishHunk();
1445
+ pendingAnchors.push(anchor);
1446
+ currentHunk = { anchors: pendingAnchors.slice(), lines: [] };
1447
+ pendingAnchors = [];
1448
+ continue;
1449
+ }
1450
+ const tag = rawLine[0];
1451
+ if (tag !== ' ' && tag !== '-' && tag !== '+') {
1452
+ if (!currentHunk) currentHunk = v4aEnsureUpdateHunk(current, pendingAnchors);
1453
+ pendingAnchors = [];
1454
+ currentHunk.lines.push(` ${rawLine}`);
1455
+ continue;
1456
+ }
1457
+ if (!currentHunk) currentHunk = v4aEnsureUpdateHunk(current, pendingAnchors);
1458
+ currentHunk.lines.push(rawLine);
1459
+ }
1460
+ finishFile();
1461
+ const bad = files.find((file) => !file.path);
1462
+ if (bad) throw new Error('V4A patch contains an empty file path');
1463
+ if (files.length === 0) throw new Error('V4A patch contained no file sections');
1464
+ return files;
1465
+ }
1466
+
1467
+ function stripUnifiedV4APathHeader(line, prefix) {
1468
+ return stripDiffPrefix(normaliseV4APath(String(line || '').slice(prefix.length)));
1469
+ }
1470
+
1471
+ // Shared parser for unified-input -> V4A sections. The bare and counted
1472
+ // fallbacks are byte-identical except for (a) the error label and (b) how an
1473
+ // `@@` line yields its anchor: bare rejects counted headers and takes the raw
1474
+ // tail; counted requires a counted header and takes its capture group. The
1475
+ // difference is injected as `resolveAnchor`; everything else is shared.
1476
+ function parseUnifiedAsV4APatch(patchStr, { label, resolveAnchor }) {
1477
+ const lines = splitPatchLines(patchStr);
1478
+ const files = [];
1479
+ let current = null;
1480
+ let pendingAnchors = [];
1481
+ let currentHunk = null;
1482
+
1483
+ const finishHunk = () => {
1484
+ if (!current || !currentHunk) return;
1485
+ if (current.kind === 'update' && currentHunk.lines.length > 0) current.hunks.push(currentHunk);
1486
+ currentHunk = null;
1487
+ };
1488
+ const finishFile = () => {
1489
+ finishHunk();
1490
+ current = null;
1491
+ pendingAnchors = [];
1492
+ };
1493
+ const startFile = (oldPath, newPath) => {
1494
+ finishFile();
1495
+ const oldIsNull = DEV_NULL.test(oldPath || '');
1496
+ const newIsNull = DEV_NULL.test(newPath || '');
1497
+ const kind = oldIsNull ? 'add' : (newIsNull ? 'delete' : 'update');
1498
+ const path = kind === 'add' ? newPath : oldPath;
1499
+ current = { kind, path: normaliseV4APath(path), hunks: [], lines: [] };
1500
+ files.push(current);
1501
+ };
1502
+
1503
+ for (let i = 0; i < lines.length; i++) {
1504
+ const rawLine = lines[i];
1505
+ if (rawLine.startsWith('diff --git ') || rawLine.startsWith('index ') || rawLine.startsWith('new file mode ') || rawLine.startsWith('deleted file mode ')) {
1506
+ continue;
1507
+ }
1508
+ if (rawLine.startsWith('--- ')) {
1509
+ const next = lines[i + 1] || '';
1510
+ if (next.startsWith('+++ ')) {
1511
+ startFile(stripUnifiedV4APathHeader(rawLine, '--- '), stripUnifiedV4APathHeader(next, '+++ '));
1512
+ i++;
1513
+ continue;
1514
+ }
1515
+ // A real unified file header `--- X` is ALWAYS immediately followed by a
1516
+ // `+++ Y` line. Without the pair, outside a file this is a malformed
1517
+ // patch (keep the diagnostic); INSIDE a file it is a hunk-body deletion
1518
+ // line whose content starts with `-- ` (rawLine `--- foo`) — fall through
1519
+ // to body handling instead of misreading it as a file header.
1520
+ if (!current) throw new Error(`${label} missing +++ header after: ${rawLine}`);
1521
+ }
1522
+ if (!current) continue;
1523
+ if (rawLine === '') {
1524
+ if (current.kind !== 'update') continue;
1525
+ if (currentHunk) currentHunk = v4aPushBlankContextLine(currentHunk, pendingAnchors);
1526
+ continue;
1527
+ }
1528
+ if (current.kind === 'update' && isV4AEndOfFileMarker(rawLine)) {
1529
+ v4aMarkHunkEndOfFile(currentHunk, finishHunk);
1530
+ currentHunk = null;
1531
+ continue;
1532
+ }
1533
+ if (rawLine.startsWith('@@')) {
1534
+ const anchor = resolveAnchor(rawLine);
1535
+ if (currentHunk && currentHunk.lines.length > 0) finishHunk();
1536
+ pendingAnchors.push(anchor);
1537
+ currentHunk = { anchors: pendingAnchors.slice(), lines: [] };
1538
+ pendingAnchors = [];
1539
+ continue;
1540
+ }
1541
+ if (current.kind === 'add') {
1542
+ if (rawLine[0] === '+') current.lines.push(rawLine.slice(1));
1543
+ continue;
1544
+ }
1545
+ if (current.kind === 'delete') {
1546
+ continue;
1547
+ }
1548
+ const tag = rawLine[0];
1549
+ if (tag !== ' ' && tag !== '-' && tag !== '+') {
1550
+ if (!currentHunk) currentHunk = v4aEnsureUpdateHunk(current, pendingAnchors);
1551
+ pendingAnchors = [];
1552
+ currentHunk.lines.push(` ${rawLine}`);
1553
+ continue;
1554
+ }
1555
+ if (!currentHunk) currentHunk = v4aEnsureUpdateHunk(current, pendingAnchors);
1556
+ currentHunk.lines.push(rawLine);
1557
+ }
1558
+ finishFile();
1559
+ const bad = files.find((file) => !file.path);
1560
+ if (bad) throw new Error(`${label} contains an empty file path`);
1561
+ if (files.length === 0) throw new Error(`${label} contained no file sections`);
1562
+ return files;
1563
+ }
1564
+
1565
+ function parseUnifiedBareV4APatch(patchStr) {
1566
+ return parseUnifiedAsV4APatch(patchStr, {
1567
+ label: 'unified bare patch',
1568
+ resolveAnchor: (rawLine) => {
1569
+ if (UNIFIED_HUNK_HEADER_RE.test(rawLine)) {
1570
+ throw new Error('unified bare patch cannot mix counted unified hunks with bare @@ anchors');
1571
+ }
1572
+ return normaliseV4AAnchor(rawLine.slice(2));
1573
+ },
1574
+ });
1575
+ }
1576
+
1577
+ function parseUnifiedCountedAsV4APatch(patchStr) {
1578
+ return parseUnifiedAsV4APatch(patchStr, {
1579
+ label: 'unified fallback',
1580
+ resolveAnchor: (rawLine) => {
1581
+ const match = UNIFIED_HUNK_HEADER_CAPTURE_RE.exec(rawLine);
1582
+ if (!match) throw new Error(`unified fallback requires counted hunk header: ${rawLine}`);
1583
+ return normaliseV4AAnchor(match[1] || '');
1584
+ },
1585
+ });
1586
+ }
1587
+
1588
+ function splitTextLinesForPatch(text) {
1589
+ const body = String(text ?? '').replace(/\r\n/g, '\n');
1590
+ if (body.length === 0) {
1591
+ const empty = [];
1592
+ // No content -> no final source line that could lack a newline.
1593
+ empty.hasFinalNewline = true;
1594
+ return empty;
1595
+ }
1596
+ const lines = body.split('\n');
1597
+ // Per-line hasNewline tracking (mirror native): every line carries a trailing
1598
+ // newline EXCEPT possibly the final one. The metadata rides on the array as a
1599
+ // non-indexed `hasFinalNewline` property so the return value stays a plain
1600
+ // string[] for every existing exact/ws/normalize/LCS consumer; only the
1601
+ // newline-aware matcher reads it. When `body` ends in '\n', split yields a
1602
+ // trailing '' sentinel -> popped -> all real lines had newlines. Otherwise
1603
+ // the final element IS real content with no trailing newline.
1604
+ let hasFinalNewline = true;
1605
+ if (lines[lines.length - 1] === '') lines.pop();
1606
+ else hasFinalNewline = false;
1607
+ lines.hasFinalNewline = hasFinalNewline;
1608
+ return lines;
1609
+ }
1610
+
1611
+ // Byte-aware variant of splitTextLinesForPatch for the DIAGNOSTIC matcher only.
1612
+ // Takes a raw file Buffer and yields an array of per-line Buffer slices split on
1613
+ // 0x0A (LF), stripping a single trailing 0x0D (CR) per line so CRLF files behave
1614
+ // like the string splitter's \r\n -> \n normalization — all WITHOUT lossy UTF-8
1615
+ // decoding, so invalid bytes survive for native-parity raw byte compares. The
1616
+ // same non-indexed `hasFinalNewline` metadata rides on the array; values are
1617
+ // Buffers, which toLineBytes/unifiedOldLinesMatchAt consume directly.
1618
+ function splitBufferLinesForPatch(buf) {
1619
+ const empty = [];
1620
+ if (!buf || buf.length === 0) {
1621
+ empty.hasFinalNewline = true;
1622
+ return empty;
1623
+ }
1624
+ const lines = [];
1625
+ let start = 0;
1626
+ for (let i = 0; i < buf.length; i++) {
1627
+ if (buf[i] === 0x0a) {
1628
+ let end = i;
1629
+ if (end > start && buf[end - 1] === 0x0d) end--; // strip CR of CRLF
1630
+ lines.push(buf.subarray(start, end));
1631
+ start = i + 1;
1632
+ }
1633
+ }
1634
+ let hasFinalNewline;
1635
+ if (start === buf.length) {
1636
+ // Buffer ended exactly on a newline -> no dangling final line.
1637
+ hasFinalNewline = true;
1638
+ } else {
1639
+ // Native strips \r ONLY when immediately before \n (CRLF). The FINAL
1640
+ // unterminated line has no \n, so a bare trailing \r is KEPT in the
1641
+ // compared body to mirror native's exact byte compare.
1642
+ lines.push(buf.subarray(start, buf.length));
1643
+ hasFinalNewline = false;
1644
+ }
1645
+ lines.hasFinalNewline = hasFinalNewline;
1646
+ return lines;
1647
+ }
1648
+
1649
+ function v4AHunkLineStats(hunk) {
1650
+ let oldCount = 0;
1651
+ let newCount = 0;
1652
+ const oldLines = [];
1653
+ const newLines = [];
1654
+ for (const raw of hunk.lines || []) {
1655
+ if (!raw) continue;
1656
+ const tag = raw[0];
1657
+ const body = raw.slice(1);
1658
+ if (tag === ' ') {
1659
+ oldCount++;
1660
+ newCount++;
1661
+ oldLines.push(body);
1662
+ newLines.push(body);
1663
+ } else if (tag === '-') {
1664
+ oldCount++;
1665
+ oldLines.push(body);
1666
+ } else if (tag === '+') {
1667
+ newCount++;
1668
+ newLines.push(body);
1669
+ }
1670
+ }
1671
+ return { oldCount, newCount, oldLines, newLines };
1672
+ }
1673
+
1674
+ function findAnchorLine(lines, anchors, fromLine) {
1675
+ let cursor = Math.max(0, fromLine || 0);
1676
+ for (const anchorRaw of anchors || []) {
1677
+ const anchor = String(anchorRaw || '').trim();
1678
+ if (!anchor) continue;
1679
+ const found = lines.findIndex((line, idx) => idx >= cursor && line.includes(anchor));
1680
+ if (found === -1) return -1;
1681
+ cursor = found + 1;
1682
+ }
1683
+ return cursor;
1684
+ }
1685
+
1686
+ // Length of the longest common (contiguous) substring of `a` and `b`.
1687
+ // Capped per side so a pathological multi-KB line cannot blow up the
1688
+ // O(N*M) inner loop; lines beyond `cap` are truncated for the LCS only.
1689
+ // Used by both the long-single-line context fallback in findLineSequence
1690
+ // and the nearest-line hint scorer.
1691
+ function longestCommonSubstringLen(a, b, cap = 4000) {
1692
+ if (!a || !b) return 0;
1693
+ const A = a.length > cap ? a.slice(0, cap) : a;
1694
+ const B = b.length > cap ? b.slice(0, cap) : b;
1695
+ const la = A.length;
1696
+ const lb = B.length;
1697
+ if (la === 0 || lb === 0) return 0;
1698
+ let prev = new Int32Array(lb + 1);
1699
+ let curr = new Int32Array(lb + 1);
1700
+ let best = 0;
1701
+ for (let i = 1; i <= la; i++) {
1702
+ const ca = A.charCodeAt(i - 1);
1703
+ for (let j = 1; j <= lb; j++) {
1704
+ curr[j] = ca === B.charCodeAt(j - 1) ? prev[j - 1] + 1 : 0;
1705
+ if (curr[j] > best) best = curr[j];
1706
+ }
1707
+ const tmp = prev; prev = curr; curr = tmp;
1708
+ curr.fill(0);
1709
+ }
1710
+ return best;
1711
+ }
1712
+
1713
+ // Normalize common typographic code-points to their ASCII equivalents, then
1714
+ // trim. Mirrors the Rust mixdog-patch normalize_typographic() and Codex
1715
+ // apply_patch's normalise() so V4A->unified anchor resolution stays consistent
1716
+ // across engines: an ASCII-authored patch can still anchor on source that
1717
+ // carries curly quotes, em/en dashes, NBSP and other exotic spaces.
1718
+ // Rust str::trim() strips Unicode White_Space at both ends. JS String.trim()
1719
+ // diverges: it trims U+FEFF (BOM/ZWNBSP) but NOT U+0085 (NEL). To stay
1720
+ // byte-for-byte consistent with native normalise(), trim EXACTLY the Rust
1721
+ // White_Space set here — include U+0085, exclude U+FEFF.
1722
+ const RUST_WS = '\\u0009\\u000A\\u000B\\u000C\\u000D\\u0020\\u0085\\u00A0\\u1680\\u2000-\\u200A\\u2028\\u2029\\u202F\\u205F\\u3000';
1723
+ const RUST_TRIM_RE = new RegExp(`^[${RUST_WS}]+|[${RUST_WS}]+$`, 'g');
1724
+ function rustTrim(s) {
1725
+ return s.replace(RUST_TRIM_RE, '');
1726
+ }
1727
+ function normalizeTypographic(s) {
1728
+ // Mirror Rust normalise() ORDER: trim FIRST, then apply the dash/quote/space
1729
+ // code-point map. Trimming before mapping matches `s.trim().chars().map(...)`.
1730
+ return rustTrim(String(s ?? ''))
1731
+ .replace(/[\u2010\u2011\u2012\u2013\u2014\u2015\u2212]/g, '-')
1732
+ .replace(/[\u2018\u2019\u201A\u201B]/g, "'")
1733
+ .replace(/[\u201C\u201D\u201E\u201F]/g, '"')
1734
+ .replace(/[\u00A0\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u202F\u205F\u3000]/g, ' ');
1735
+ }
1736
+
1737
+ function findLineSequence(lines, needle, fromLine, preferredLine = 0, options = {}) {
1738
+ if (!Array.isArray(needle) || needle.length === 0) return Math.max(0, preferredLine || fromLine || 0);
1739
+ const eof = options?.eof === true;
1740
+ let minStart = Math.max(0, fromLine || 0);
1741
+ if (eof && needle.length <= lines.length) {
1742
+ minStart = Math.max(minStart, lines.length - needle.length);
1743
+ }
1744
+ const preferred = Math.max(0, preferredLine || 0);
1745
+ const fuzzy = options && options.fuzzy === false ? false : true;
1746
+ const tiers = fuzzy
1747
+ ? [
1748
+ (a, b) => a === b,
1749
+ (a, b) => a.replace(/\s+$/, '') === b.replace(/\s+$/, ''),
1750
+ (a, b) => a.trim() === b.trim(),
1751
+ // Internal-whitespace-collapse tier: catches reformatted long lines
1752
+ // (e.g. re-indented JSON values) where exact bytes drift but the
1753
+ // semantic content matches. Runs strictly after stricter tiers so
1754
+ // exact / rstrip / trim still win when they match.
1755
+ (a, b) => a.replace(/\s+/g, ' ').trim() === b.replace(/\s+/g, ' ').trim(),
1756
+ // Unicode-normalization tier (LAST): typographic dashes/quotes/NBSP in
1757
+ // the source vs an ASCII-authored patch. Deterministic code-point map,
1758
+ // runs after every whitespace tier so stricter matches always win.
1759
+ (a, b) => normalizeTypographic(a) === normalizeTypographic(b),
1760
+ ]
1761
+ : [
1762
+ (a, b) => a === b,
1763
+ ];
1764
+ for (const eq of tiers) {
1765
+ const starts = [];
1766
+ for (let i = minStart; i <= lines.length - needle.length; i++) {
1767
+ let ok = true;
1768
+ for (let k = 0; k < needle.length; k++) {
1769
+ if (!eq(lines[i + k], needle[k])) { ok = false; break; }
1770
+ }
1771
+ if (ok) starts.push(i);
1772
+ }
1773
+ if (starts.length) {
1774
+ starts.sort((a, b) => Math.abs(a - preferred) - Math.abs(b - preferred) || a - b);
1775
+ return starts[0];
1776
+ }
1777
+ }
1778
+ // Long single-line context fallback. When the entire needle is one long
1779
+ // line (>=40 chars after whitespace-collapse) and every equality tier
1780
+ // failed, accept a UNIQUE source line whose longest-common-substring
1781
+ // with the needle is the file-wide maximum and covers at least half of
1782
+ // the needle. Uniqueness is the invariant: ambiguous best-matches
1783
+ // return -1, so real mismatches still surface as "context not found"
1784
+ // instead of silently anchoring on the wrong line.
1785
+ if (fuzzy && needle.length === 1) {
1786
+ const want = String(needle[0] ?? '').replace(/\s+/g, ' ').trim();
1787
+ if (want.length >= 40) {
1788
+ const minLcs = Math.max(40, Math.floor(want.length / 2));
1789
+ let bestIdx = -1;
1790
+ let bestLcs = 0;
1791
+ let bestTies = 0;
1792
+ for (let i = minStart; i < lines.length; i++) {
1793
+ const cand = String(lines[i] ?? '').replace(/\s+/g, ' ').trim();
1794
+ if (cand.length === 0) continue;
1795
+ const lcs = longestCommonSubstringLen(cand, want);
1796
+ if (lcs < minLcs) continue;
1797
+ if (lcs > bestLcs) { bestLcs = lcs; bestIdx = i; bestTies = 1; }
1798
+ else if (lcs === bestLcs) { bestTies++; }
1799
+ }
1800
+ if (bestIdx >= 0 && bestTies === 1) return bestIdx;
1801
+ }
1802
+ }
1803
+ return -1;
1804
+ }
1805
+
1806
+ function compactPatchPreviewLine(line, maxLen = 140) {
1807
+ const text = String(line ?? '').replace(/\t/g, '\\t');
1808
+ return text.length > maxLen ? `${text.slice(0, maxLen - 1)}…` : text;
1809
+ }
1810
+
1811
+ // Bundled/transpiled sources often store non-ASCII inside string literals as
1812
+ // literal `\uXXXX` escape sequences (6 ASCII chars). A patch authored with the
1813
+ // real character can then never match verbatim. These helpers let the V4A
1814
+ // locator accept a window where each patch line matches the source either
1815
+ // verbatim or via "patch's real char == file's \uXXXX escape of it".
1816
+ function escapeNonAsciiForPatch(line) {
1817
+ const s = String(line ?? '');
1818
+ let out = '';
1819
+ for (let i = 0; i < s.length; i++) {
1820
+ const code = s.charCodeAt(i);
1821
+ out += code > 0x7f
1822
+ ? String.fromCharCode(92) + 'u' + code.toString(16).padStart(4, '0')
1823
+ : s[i];
1824
+ }
1825
+ return out;
1826
+ }
1827
+
1828
+ function findLineSequenceEscapeEquiv(sourceLines, pattern, minStart, preferred) {
1829
+ if (!pattern || pattern.length === 0) return -1;
1830
+ const starts = [];
1831
+ const from = Math.max(0, Number.isFinite(minStart) ? minStart : 0);
1832
+ outer: for (let i = from; i + pattern.length <= sourceLines.length; i++) {
1833
+ let usedEquiv = false;
1834
+ for (let k = 0; k < pattern.length; k++) {
1835
+ const pat = pattern[k];
1836
+ const src = sourceLines[i + k];
1837
+ if (src === pat) continue;
1838
+ if (src === escapeNonAsciiForPatch(pat)) { usedEquiv = true; continue; }
1839
+ continue outer;
1840
+ }
1841
+ // Require at least one escape-equivalent line: an all-verbatim window
1842
+ // would have been found by the primary matcher already.
1843
+ if (usedEquiv) starts.push(i);
1844
+ }
1845
+ if (starts.length === 0) return -1;
1846
+ const pref = Number.isFinite(preferred) && preferred >= 0 ? preferred : 0;
1847
+ starts.sort((a, b) => Math.abs(a - pref) - Math.abs(b - pref) || a - b);
1848
+ return starts[0];
1849
+ }
1850
+
1851
+ function firstMeaningfulPatchLine(lines) {
1852
+ return (lines || []).find((line) => String(line ?? '').trim().length > 0) || '';
1853
+ }
1854
+
1855
+ function scoreSimilarPatchLine(candidate, target) {
1856
+ const cand = String(candidate ?? '').trim().replace(/\s+/g, ' ');
1857
+ const want = String(target ?? '').trim().replace(/\s+/g, ' ');
1858
+ if (!cand || !want) return 0;
1859
+ if (cand === want) return 100000;
1860
+ let score = 0;
1861
+ // Longest common substring drives similarity for long lines: weighting
1862
+ // shared-byte run length keeps the "nearest line" hint anchored on the
1863
+ // line that actually shares the most content, instead of a short line
1864
+ // that happens to embed in (or share a few tokens with) the long target.
1865
+ const lcs = longestCommonSubstringLen(cand, want);
1866
+ score += lcs * 20;
1867
+ if (cand.includes(want) || want.includes(cand)) score += 5000 + Math.min(cand.length, want.length);
1868
+ const words = new Set(want.split(/[^A-Za-z0-9_$]+/).filter((word) => word.length > 1));
1869
+ for (const word of words) {
1870
+ if (cand.includes(word)) score += Math.min(200, word.length * 12);
1871
+ }
1872
+ // Length-delta penalty only meaningful for short lines; a long line with
1873
+ // a large shared-byte run should not be crushed by a modest length gap.
1874
+ if (Math.max(cand.length, want.length) < 80) {
1875
+ score -= Math.abs(cand.length - want.length);
1876
+ }
1877
+ return score;
1878
+ }
1879
+
1880
+ function nearestPatchLineHint(sourceLines, expectedLine, preferredLine) {
1881
+ const expected = String(expectedLine || '');
1882
+ if (!expected.trim()) return '';
1883
+ let best = null;
1884
+ const preferred = Number.isFinite(preferredLine) && preferredLine >= 0 ? preferredLine : 0;
1885
+ for (let i = 0; i < sourceLines.length; i++) {
1886
+ const score = scoreSimilarPatchLine(sourceLines[i], expected) - (Math.abs(i - preferred) * 0.01);
1887
+ if (!best || score > best.score) best = { score, index: i, line: sourceLines[i] };
1888
+ }
1889
+ if (!best || best.score <= 0) return '';
1890
+ return `nearest line ${best.index + 1}: ${JSON.stringify(compactPatchPreviewLine(best.line))}`;
1891
+ }
1892
+
1893
+ function formatV4AHunkLocator(hunk) {
1894
+ return (hunk.anchors || []).filter(Boolean).join(' > ') || '(no anchor)';
1895
+ }
1896
+
1897
+ function formatV4AAnchorMissHint(sourceLines, hunk) {
1898
+ const anchors = (hunk?.anchors || []).filter(Boolean);
1899
+ const nearest = anchors.length > 0
1900
+ ? anchors.map((anchor) => nearestPatchLineHint(sourceLines, anchor, 0)).find(Boolean)
1901
+ : null;
1902
+ return anchors.length === 0
1903
+ ? ' use an existing @@ anchor from the current file or add exact context lines.'
1904
+ : ` use an existing @@ anchor from the current file or add exact context lines; no stubs.${nearest ? ` nearest anchor candidate: ${nearest}.` : ''}`;
1905
+ }
1906
+
1907
+ function formatV4AContextMissHint(sourceLines, stats, anchorLine) {
1908
+ const expected = firstMeaningfulPatchLine(stats.oldLines);
1909
+ const parts = [];
1910
+ if (expected) {
1911
+ const nearest = nearestPatchLineHint(sourceLines, expected, anchorLine);
1912
+ parts.push(`expected first old line: ${JSON.stringify(compactPatchPreviewLine(expected))}`);
1913
+ if (nearest) parts.push(nearest);
1914
+ const divergence = firstV4ADivergenceHint(sourceLines, stats.oldLines, anchorLine);
1915
+ if (divergence) parts.push(divergence);
1916
+ }
1917
+ parts.push('use exact current context or a broader @@ anchor; no stubs.');
1918
+ return ` ${parts.join('; ')}`;
1919
+ }
1920
+
1921
+ // When the FIRST old line does exist verbatim in the source, the real
1922
+ // mismatch is some later line of the block — name it, with both sides
1923
+ // JSON-escaped so invisible differences (real char vs literal \uXXXX
1924
+ // escape, tabs, trailing spaces) become visible in the error.
1925
+ function firstV4ADivergenceHint(sourceLines, oldLines, anchorLine) {
1926
+ const lines = oldLines || [];
1927
+ const firstIdx = lines.findIndex((l) => String(l ?? '').trim().length > 0);
1928
+ if (firstIdx < 0) return '';
1929
+ const first = lines[firstIdx];
1930
+ const starts = [];
1931
+ for (let i = 0; i < sourceLines.length; i++) {
1932
+ if (sourceLines[i] === first) starts.push(i - firstIdx);
1933
+ }
1934
+ const pref = Number.isFinite(anchorLine) && anchorLine >= 0 ? anchorLine : 0;
1935
+ const start = starts.filter((s) => s >= 0)
1936
+ .sort((a, b) => Math.abs(a - pref) - Math.abs(b - pref) || a - b)[0];
1937
+ if (start === undefined) return '';
1938
+ for (let k = 0; k < lines.length; k++) {
1939
+ const exp = lines[k];
1940
+ const act = sourceLines[start + k];
1941
+ if (act !== exp) {
1942
+ const actText = act === undefined ? '(past EOF)' : JSON.stringify(compactPatchPreviewLine(act));
1943
+ return `first divergent line: old[${k + 1}] expected ${JSON.stringify(compactPatchPreviewLine(exp))} vs file line ${start + k + 1} actual ${actText}`;
1944
+ }
1945
+ }
1946
+ return '';
1947
+ }
1948
+
1949
+ function joinTextLinesForPatch(lines) {
1950
+ const body = (lines || []).join('\n');
1951
+ return lines?.hasFinalNewline !== false ? `${body}\n` : body;
1952
+ }
1953
+
1954
+ function cloneTextLinesForPatch(sourceLines) {
1955
+ const lines = [...(sourceLines || [])];
1956
+ lines.hasFinalNewline = sourceLines?.hasFinalNewline !== false;
1957
+ return lines;
1958
+ }
1959
+
1960
+ function resolveV4AHunkPosition(sourceLines, hunk, nextSearchLine, options = {}) {
1961
+ const stats = v4AHunkLineStats(hunk);
1962
+ if (stats.oldCount === 0 && stats.newCount === 0) return { skip: true };
1963
+ const fuzzy = options.fuzzy !== false;
1964
+ const eof = hunk?.isEndOfFile === true;
1965
+ const anchorLine = findAnchorLine(sourceLines, hunk.anchors, nextSearchLine);
1966
+ if (anchorLine < 0) {
1967
+ const msg = `V4A hunk anchor not found: ${formatV4AHunkLocator(hunk)};${formatV4AAnchorMissHint(sourceLines, hunk)}`;
1968
+ return { error: msg };
1969
+ }
1970
+ let oldLinesPattern = stats.oldLines;
1971
+ let newLinesPattern = stats.newLines;
1972
+ let oldStartIdx;
1973
+ let trimmedTrailing = 0;
1974
+ let trimmedTrailingNew = 0;
1975
+ if (stats.oldCount === 0) {
1976
+ oldStartIdx = eof ? sourceLines.length : anchorLine;
1977
+ } else {
1978
+ const searchFrom = Math.max(0, anchorLine - 1);
1979
+ oldStartIdx = findLineSequence(
1980
+ sourceLines,
1981
+ oldLinesPattern,
1982
+ searchFrom,
1983
+ searchFrom,
1984
+ { fuzzy, eof },
1985
+ );
1986
+ if (eof && oldStartIdx < 0 && oldLinesPattern.length > 0 && oldLinesPattern[oldLinesPattern.length - 1] === '') {
1987
+ oldLinesPattern = oldLinesPattern.slice(0, -1);
1988
+ trimmedTrailing = 1;
1989
+ if (newLinesPattern.length > 0 && newLinesPattern[newLinesPattern.length - 1] === '') {
1990
+ newLinesPattern = newLinesPattern.slice(0, -1);
1991
+ trimmedTrailingNew = 1;
1992
+ }
1993
+ oldStartIdx = findLineSequence(
1994
+ sourceLines,
1995
+ oldLinesPattern,
1996
+ searchFrom,
1997
+ searchFrom,
1998
+ { fuzzy, eof },
1999
+ );
2000
+ }
2001
+ }
2002
+ // Escape-equivalence fallback (fuzzy, non-EOF only): accept a window where each old
2003
+ // line matches the source verbatim OR as the file's literal `\uXXXX` escape
2004
+ // of the patch's real character. On match, remap old/context lines to the
2005
+ // file's on-disk form so untouched context stays byte-identical and the
2006
+ // escape representation survives the edit.
2007
+ if (oldStartIdx < 0 && fuzzy && !eof && oldLinesPattern.length > 0) {
2008
+ const from = Math.max(0, anchorLine - 1);
2009
+ const alt = findLineSequenceEscapeEquiv(sourceLines, oldLinesPattern, from, from);
2010
+ if (alt >= 0) {
2011
+ const remapped = new Map();
2012
+ // Text-keyed remap is only safe when unambiguous: if the SAME patch
2013
+ // line text maps to DIFFERENT on-disk forms at different window
2014
+ // positions (one verbatim, one escaped), rewriting newLines by text
2015
+ // would corrupt an untouched context line — reject the match instead.
2016
+ let ambiguous = false;
2017
+ for (let k = 0; k < oldLinesPattern.length; k++) {
2018
+ const pat = oldLinesPattern[k];
2019
+ const src = sourceLines[alt + k];
2020
+ if (remapped.has(pat) && remapped.get(pat) !== src) { ambiguous = true; break; }
2021
+ remapped.set(pat, src);
2022
+ }
2023
+ if (!ambiguous) {
2024
+ newLinesPattern = newLinesPattern.map((l) => remapped.get(l) ?? l);
2025
+ oldLinesPattern = oldLinesPattern.map((_, k) => sourceLines[alt + k]);
2026
+ oldStartIdx = alt;
2027
+ }
2028
+ }
2029
+ }
2030
+ if (oldStartIdx < 0) {
2031
+ const msg = `V4A hunk context not found: ${formatV4AHunkLocator(hunk)};${formatV4AContextMissHint(sourceLines, stats, anchorLine)}`;
2032
+ return { error: msg };
2033
+ }
2034
+ const matchLen = stats.oldCount === 0 ? 0 : oldLinesPattern.length;
2035
+ return {
2036
+ oldStartIdx,
2037
+ matchLen,
2038
+ newLines: newLinesPattern,
2039
+ nextSearchLine: oldStartIdx + Math.max(1, matchLen),
2040
+ trimmedTrailing,
2041
+ trimmedTrailingNew,
2042
+ };
2043
+ }
2044
+
2045
+ function applyV4AHunksToLines(sourceLines, hunks, options = {}) {
2046
+ const lines = cloneTextLinesForPatch(sourceLines);
2047
+ const orderedHunks = orderV4AHunksByFilePosition(lines, hunks, options.fuzzy !== false);
2048
+ let nextSearchLine = 0;
2049
+ const replacements = [];
2050
+ for (const hunk of orderedHunks) {
2051
+ const loc = resolveV4AHunkPosition(lines, hunk, nextSearchLine, options);
2052
+ if (loc.skip) continue;
2053
+ if (loc.error) throw new Error(loc.error);
2054
+ replacements.push({
2055
+ oldStartIdx: loc.oldStartIdx,
2056
+ oldLen: loc.matchLen,
2057
+ newLines: loc.newLines,
2058
+ });
2059
+ nextSearchLine = loc.nextSearchLine;
2060
+ }
2061
+ for (const rep of replacements.reverse()) {
2062
+ lines.splice(rep.oldStartIdx, rep.oldLen, ...rep.newLines);
2063
+ }
2064
+ return lines;
2065
+ }
2066
+
2067
+ // Order-independent hunk ordering for the V4A apply / V4A→unified conversion.
2068
+ // V4A hunks carry no line numbers and may be authored out of file order (a
2069
+ // later edit's hunk listed before an earlier one) or against pre-shift line
2070
+ // numbers. The cursor loops that consume hunks locate each one with a
2071
+ // forward-only `nextSearchLine`, which rejects an out-of-order hunk even when
2072
+ // its context is uniquely present ("context not found; nearest line N").
2073
+ //
2074
+ // Two-phase, semantics-preserving:
2075
+ // Phase 1 — replay the SAME forward-cursor over the input order. If every
2076
+ // hunk resolves, the existing cursor semantics are authoritative — they
2077
+ // own duplicate-context AND insert-only @@-anchor disambiguation (a later
2078
+ // hunk binds to the NEXT matching occurrence after the previous hunk), so
2079
+ // we return the hunks UNCHANGED. An already-in-order patch is a guaranteed
2080
+ // no-op; nothing about the existing behaviour shifts.
2081
+ // Phase 2 (invariant-based recovery) — only when the input order is NOT
2082
+ // forward-locatable (a hunk targets a position before a prior hunk).
2083
+ // Reorder a hunk ONLY when its old-block (context+delete body lines)
2084
+ // appears EXACTLY ONCE in the source as a literal line sequence — that
2085
+ // hunk then has a single order-independent position. If ANY hunk is
2086
+ // insert-only (no old body) or its old-block is absent / appears more than
2087
+ // once (cursor-sensitive), reordering is unsafe: return the input order
2088
+ // unchanged so the loop surfaces the original error instead of binding to
2089
+ // the wrong occurrence. Direct literal counting (NOT resolveV4AHunkPosition)
2090
+ // sidesteps the anchor/cursor/EOF off-by-one quirks of a re-probe.
2091
+ function orderV4AHunksByFilePosition(sourceLines, hunks, fuzzy) {
2092
+ const list = hunks || [];
2093
+ if (list.length <= 1) return list;
2094
+ // Phase 1: is the input order already forward-locatable? Mirror the
2095
+ // conversion/apply loop's `nextSearchLine` advance exactly.
2096
+ let nextSearchLine = 0;
2097
+ let inputOrderValid = true;
2098
+ for (const hunk of list) {
2099
+ const stats = v4AHunkLineStats(hunk);
2100
+ if (stats.oldCount === 0 && stats.newCount === 0) continue;
2101
+ let loc;
2102
+ try { loc = resolveV4AHunkPosition(sourceLines, hunk, nextSearchLine, { fuzzy }); }
2103
+ catch { loc = { error: true }; }
2104
+ if (!loc || loc.error || loc.skip || typeof loc.nextSearchLine !== 'number') {
2105
+ inputOrderValid = false;
2106
+ break;
2107
+ }
2108
+ nextSearchLine = loc.nextSearchLine;
2109
+ }
2110
+ if (inputOrderValid) return list;
2111
+ // Phase 2: reorder only hunks whose old-block is a UNIQUE literal sequence.
2112
+ const keyed = [];
2113
+ for (let idx = 0; idx < list.length; idx++) {
2114
+ const hunk = list[idx];
2115
+ const stats = v4AHunkLineStats(hunk);
2116
+ if (stats.oldCount === 0 && stats.newCount === 0) {
2117
+ keyed.push({ hunk, key: Number.MAX_SAFE_INTEGER, idx });
2118
+ continue;
2119
+ }
2120
+ // Old-block = context + delete body lines (prefix-stripped), excluding the
2121
+ // EOF marker. Empty = insert-only → no order-independent position.
2122
+ const seq = [];
2123
+ for (const ln of hunk.lines || []) {
2124
+ if (isV4AEndOfFileMarker(ln)) continue;
2125
+ const p = ln[0];
2126
+ if (p === ' ' || p === '-') seq.push(ln.slice(1));
2127
+ }
2128
+ if (seq.length === 0) return list;
2129
+ // Count exact file-wide occurrences (early-out at 2). Must be exactly one.
2130
+ let pos = -1;
2131
+ let count = 0;
2132
+ for (let i = 0; i + seq.length <= sourceLines.length; i++) {
2133
+ let match = true;
2134
+ for (let j = 0; j < seq.length; j++) {
2135
+ if (sourceLines[i + j] !== seq[j]) { match = false; break; }
2136
+ }
2137
+ if (match) {
2138
+ if (pos < 0) pos = i;
2139
+ count++;
2140
+ if (count >= 2) break;
2141
+ }
2142
+ }
2143
+ if (count !== 1) return list;
2144
+ keyed.push({ hunk, key: pos, idx });
2145
+ }
2146
+ keyed.sort((a, b) => (a.key - b.key) || (a.idx - b.idx));
2147
+ return keyed.map((e) => e.hunk);
2148
+ }
2149
+
2150
+ function isV4ARenameSection(section) {
2151
+ return section?.kind === 'update' && !!section?.movePath;
2152
+ }
2153
+
2154
+ function v4aRenamePathKey(absPath) {
2155
+ return process.platform === 'win32' ? String(absPath || '').toLowerCase() : String(absPath || '');
2156
+ }
2157
+
2158
+ function v4aRenamePathInsideRealBase(absPath, realBase) {
2159
+ const checkReal = realpathNearestExistingAncestor(absPath);
2160
+ const checkRel = pathRelative(realBase, checkReal);
2161
+ if (!checkRel || checkRel.startsWith('..') || isAbsolute(checkRel)) return false;
2162
+ return !checkRel.split(/[\\/]+/).some((part) => part === '..');
2163
+ }
2164
+
2165
+ function validateV4ARenameSection(section, basePath, seenDestKeys, realBase) {
2166
+ const escapeErr = v4aSectionPathEscapeError(section, basePath);
2167
+ if (escapeErr) return escapeErr;
2168
+ const escapeDest = v4aSectionPathEscapeError({ path: section.movePath }, basePath);
2169
+ if (escapeDest) return escapeDest;
2170
+ const srcFull = resolveV4AEntryPath(basePath, section.path);
2171
+ const destFull = resolveV4AEntryPath(basePath, section.movePath);
2172
+ if (realBase) {
2173
+ if (!v4aRenamePathInsideRealBase(srcFull, realBase)) {
2174
+ return `apply_patch: ${normalizeOutputPath(section.path)} resolves outside base_path via symlink/junction; refusing V4A rename.`;
2175
+ }
2176
+ if (!v4aRenamePathInsideRealBase(destFull, realBase)) {
2177
+ return `apply_patch: ${normalizeOutputPath(section.movePath)} resolves outside base_path via symlink/junction; refusing V4A rename.`;
2178
+ }
2179
+ }
2180
+ if (v4aRenamePathKey(srcFull) === v4aRenamePathKey(destFull)) {
2181
+ return `apply_patch: V4A rename source and destination are the same path (${normalizeOutputPath(section.path)})`;
2182
+ }
2183
+ const destKey = v4aRenamePathKey(destFull);
2184
+ if (seenDestKeys.has(destKey)) {
2185
+ return `apply_patch: duplicate V4A rename destination ${normalizeOutputPath(section.movePath)}`;
2186
+ }
2187
+ seenDestKeys.add(destKey);
2188
+ try {
2189
+ const st = statSync(srcFull);
2190
+ if (!st.isFile()) {
2191
+ return `apply_patch: V4A rename source is not a regular file: ${normalizeOutputPath(section.path)}`;
2192
+ }
2193
+ } catch (err) {
2194
+ return `apply_patch: V4A rename source missing or unreadable: ${normalizeOutputPath(section.path)} (${err?.code || err?.message || String(err)})`;
2195
+ }
2196
+ try {
2197
+ const destSt = statSync(destFull);
2198
+ if (destSt.isDirectory()) {
2199
+ return `apply_patch: V4A rename destination is a directory: ${normalizeOutputPath(section.movePath)}`;
2200
+ }
2201
+ if (!destSt.isFile()) {
2202
+ return `apply_patch: V4A rename destination is not a regular file: ${normalizeOutputPath(section.movePath)}`;
2203
+ }
2204
+ } catch (err) {
2205
+ if (err?.code !== 'ENOENT') {
2206
+ return `apply_patch: V4A rename destination unreadable: ${normalizeOutputPath(section.movePath)} (${err?.code || err?.message || String(err)})`;
2207
+ }
2208
+ }
2209
+ if (!section.hunks?.length) {
2210
+ return `apply_patch: V4A rename for ${normalizeOutputPath(section.path)} has no update hunks`;
2211
+ }
2212
+ return null;
2213
+ }
2214
+
2215
+ async function applyV4ARenameSection(section, basePath, options = {}) {
2216
+ const srcFull = resolveV4AEntryPath(basePath, section.path);
2217
+ const destFull = resolveV4AEntryPath(basePath, section.movePath);
2218
+ const displaySrc = normalizeOutputPath(section.path);
2219
+ const displayDest = normalizeOutputPath(section.movePath);
2220
+ let sourceLines;
2221
+ try {
2222
+ sourceLines = v4aConversionSourceLines(srcFull, options.linesCache || new Map());
2223
+ } catch (err) {
2224
+ throw new Error(`apply_patch: V4A rename source unreadable: ${displaySrc} (${err?.code || err?.message || String(err)})`);
2225
+ }
2226
+ let updatedLines;
2227
+ try {
2228
+ updatedLines = applyV4AHunksToLines(sourceLines, section.hunks, options);
2229
+ } catch (err) {
2230
+ throw err;
2231
+ }
2232
+ const newContent = joinTextLinesForPatch(updatedLines);
2233
+ if (options.dryRun) {
2234
+ return {
2235
+ ok: true,
2236
+ dryRun: true,
2237
+ displayPath: displayDest,
2238
+ linesChanged: section.hunks.reduce((n, h) => n + (h.lines?.length || 0), 0),
2239
+ srcFull,
2240
+ destFull,
2241
+ };
2242
+ }
2243
+ const originalContent = readFileSync(srcFull);
2244
+ let destBefore = null;
2245
+ try {
2246
+ destBefore = readFileSync(destFull);
2247
+ } catch (err) {
2248
+ if (err?.code !== 'ENOENT') throw err;
2249
+ }
2250
+ mkdirSync(pathDirname(destFull), { recursive: true });
2251
+ try {
2252
+ await atomicWrite(destFull, newContent, { sessionId: options.readStateScope });
2253
+ await unlink(srcFull);
2254
+ } catch (err) {
2255
+ try {
2256
+ if (destBefore === null) {
2257
+ try { await unlink(destFull); } catch {}
2258
+ } else {
2259
+ await atomicWrite(destFull, destBefore, { sessionId: options.readStateScope });
2260
+ }
2261
+ } catch {}
2262
+ try {
2263
+ await atomicWrite(srcFull, originalContent, { sessionId: options.readStateScope });
2264
+ } catch {}
2265
+ throw new Error(`apply_patch: V4A rename failed for ${displaySrc} → ${displayDest} (${err?.message || String(err)})`);
2266
+ }
2267
+ invalidateBuiltinResultCache([srcFull, destFull]);
2268
+ markCodeGraphDirtyPaths([srcFull, destFull]);
2269
+ clearReadSnapshotForPath(srcFull, options.readStateScope);
2270
+ clearReadSnapshotForPath(destFull, options.readStateScope);
2271
+ return {
2272
+ ok: true,
2273
+ displayPath: displayDest,
2274
+ fromPath: displaySrc,
2275
+ linesChanged: section.hunks.reduce((n, h) => n + (h.lines?.length || 0), 0),
2276
+ srcFull,
2277
+ destFull,
2278
+ };
2279
+ }
2280
+
2281
+ function formatV4ARenameSuccessLines(results) {
2282
+ return (results || [])
2283
+ .filter((r) => r?.ok && !r.skipped)
2284
+ .map((r) => `OK ${r.displayPath} (renamed from ${r.fromPath}, ~${r.linesChanged} lines touched, engine=v4a-rename)`);
2285
+ }
2286
+
2287
+ async function planV4ARenameSections(sections, basePath) {
2288
+ const renameSections = (sections || []).filter(isV4ARenameSection);
2289
+ const remainingSections = (sections || []).filter((s) => !isV4ARenameSection(s));
2290
+ if (renameSections.length === 0) {
2291
+ return { renameSections: [], remainingSections };
2292
+ }
2293
+ if (renameSections.length > 1) {
2294
+ throw new Error('apply_patch: only one V4A rename (*** Move to:) per patch is supported; split into separate patches.');
2295
+ }
2296
+ if (remainingSections.length > 0) {
2297
+ throw new Error('apply_patch: V4A rename cannot be combined with other add/update/delete sections in the same patch; apply file edits in a separate patch first.');
2298
+ }
2299
+ await assertPathReachable(basePath);
2300
+ const renameReachPaths = renameSections.flatMap((section) => [
2301
+ resolveV4AEntryPath(basePath, section.path),
2302
+ resolveV4AEntryPath(basePath, section.movePath),
2303
+ ]);
2304
+ await assertPathsReachable(renameReachPaths);
2305
+ let realBase;
2306
+ try {
2307
+ realBase = realpathSync(pathResolve(basePath));
2308
+ } catch (err) {
2309
+ throw new Error(`apply_patch: base_path unreadable (${err?.code || err?.message || String(err)}): ${basePath}`);
2310
+ }
2311
+ const seenDestKeys = new Set();
2312
+ for (const section of renameSections) {
2313
+ const errText = validateV4ARenameSection(section, basePath, seenDestKeys, realBase);
2314
+ if (errText) throw new Error(errText);
2315
+ }
2316
+ return {
2317
+ renameSections,
2318
+ remainingSections,
2319
+ };
2320
+ }
2321
+
2322
+ async function applyV4ARenameSections(renameSections, basePath, options = {}) {
2323
+ const linesCache = new Map();
2324
+ const results = [];
2325
+ for (const section of renameSections || []) {
2326
+ results.push(await applyV4ARenameSection(section, basePath, { ...options, linesCache }));
2327
+ }
2328
+ return results;
2329
+ }
2330
+
2331
+ function convertUnifiedBareV4AToUnifiedPatch(patchStr, basePath, options = {}) {
2332
+ return convertV4ASectionsToUnifiedPatch(parseUnifiedBareV4APatch(patchStr), basePath, options);
2333
+ }
2334
+
2335
+ function convertUnifiedCountedToUnifiedPatchViaV4A(patchStr, basePath, options = {}) {
2336
+ return convertV4ASectionsToUnifiedPatch(parseUnifiedCountedAsV4APatch(patchStr), basePath, options);
2337
+ }
2338
+
2339
+ // Lexical path-escape guard for V4A section paths. Mirrors the check
2340
+ // `nativeHeaderSupported` runs on unified headers: a `..` segment or an
2341
+ // absolute path that does not resolve inside basePath is unsupported.
2342
+ // Run BEFORE the V4A readFileSync so an escape surfaces with a clear
2343
+ // reason instead of masquerading as an ENOENT "update target unreadable".
2344
+ function v4aSectionPathEscapeError(section, basePath) {
2345
+ const raw = section?.path;
2346
+ if (!raw) return null;
2347
+ const norm = normalizeInputPath(raw);
2348
+ const segs = norm.split(/[\\/]+/);
2349
+ if (segs.some((part) => part === '..')) {
2350
+ return `apply_patch: header ${normalizeOutputPath(raw)} is unsupported (path escapes base_path or contains \`..\`).`;
2351
+ }
2352
+ if (isAbsolute(norm) || /^[A-Za-z]:[\\/]/.test(norm)) {
2353
+ if (!basePath) return `apply_patch: header ${normalizeOutputPath(raw)} is unsupported (path escapes base_path or contains \`..\`).`;
2354
+ const absHeader = pathResolve(norm);
2355
+ const absBase = pathResolve(basePath);
2356
+ const rel = pathRelative(absBase, absHeader);
2357
+ if (!rel || rel.startsWith('..') || isAbsolute(rel) || rel.split(/[\\/]+/).some((part) => part === '..')) {
2358
+ return `apply_patch: header ${normalizeOutputPath(raw)} is unsupported (path escapes base_path or contains \`..\`).`;
2359
+ }
2360
+ }
2361
+ return null;
2362
+ }
2363
+
2364
+ function readRawBufForV4AConversion(fullPath) {
2365
+ // Fresh statSync (NOT the 5s STAT_CACHE) for raw-cache generation validation:
2366
+ // an external modify/delete that bypasses invalidateBuiltinResultCache could
2367
+ // otherwise let a stale STAT_CACHE entry match stale raw bytes, anchoring V4A
2368
+ // hunks on out-of-date source. Fresh stat is cheap and keeps the byte-read
2369
+ // savings on the unchanged common path while rejecting stale generations.
2370
+ const st = statSync(fullPath);
2371
+ const cached = rawContentCacheGet(fullPath, st);
2372
+ if (cached) return cached;
2373
+ const rawBuf = readFileSync(fullPath);
2374
+ const buf = Buffer.isBuffer(rawBuf) ? rawBuf : Buffer.from(rawBuf);
2375
+ rawContentCacheSet(fullPath, st, buf);
2376
+ return buf;
2377
+ }
2378
+
2379
+ function v4aConversionSourceLines(fullPath, linesCache) {
2380
+ if (linesCache.has(fullPath)) return linesCache.get(fullPath);
2381
+ const lines = splitTextLinesForPatch(readRawBufForV4AConversion(fullPath).toString('utf-8'));
2382
+ linesCache.set(fullPath, lines);
2383
+ return lines;
2384
+ }
2385
+
2386
+ // options.rejectPartial (default true)
2387
+ // true — anchor/context miss on any hunk throws and aborts the whole patch
2388
+ // false — hunk-level isolation in the V4A→unified conversion: a hunk
2389
+ // whose anchor/context cannot be located is dropped; the rest of
2390
+ // the file's hunks continue. A file section whose hunks all fail
2391
+ // emits no header so the downstream unified-diff parser does not
2392
+ // see an empty section. Dropped hunks are appended to
2393
+ // options.rejectedHunks for the caller to surface.
2394
+ async function convertV4ASectionsToUnifiedPatch(sections, basePath, options = {}) {
2395
+ // Reachability preflight for update/delete targets: v4aConversionSourceLines
2396
+ // does statSync/readFileSync on each non-add section's source file. A
2397
+ // dead-mounted target under a reachable basePath would freeze the event loop
2398
+ // here, before preValidateNativeBatch's guard. resolveEntryPath is FS-pure.
2399
+ {
2400
+ const reachPaths = [];
2401
+ const _seenReach = new Set();
2402
+ for (const s of (sections || [])) {
2403
+ if (!s || s.kind === 'add' || typeof s.path !== 'string' || !s.path) continue;
2404
+ const fp = resolveV4AEntryPath(basePath, s.path);
2405
+ if (_seenReach.has(fp)) continue;
2406
+ _seenReach.add(fp);
2407
+ reachPaths.push(fp);
2408
+ }
2409
+ await assertPathsReachable(reachPaths);
2410
+ }
2411
+ const rejectPartial = options.rejectPartial !== false;
2412
+ const rejectedHunks = Array.isArray(options.rejectedHunks) ? options.rejectedHunks : null;
2413
+ const fuzzy = options.fuzzy !== false;
2414
+ const out = [];
2415
+ const v4aLinesCache = new Map();
2416
+ for (const section of sections) {
2417
+ // Explicit path-escape guard runs BEFORE any readFileSync attempt so
2418
+ // a header containing `..` or an out-of-base absolute path surfaces
2419
+ // with a clear reason instead of being masked as ENOENT (the V4A
2420
+ // "update target unreadable" path) when the escaped target doesn't
2421
+ // happen to exist on disk.
2422
+ const escapeErr = v4aSectionPathEscapeError(section, basePath);
2423
+ if (escapeErr) throw new Error(escapeErr);
2424
+ const displayPath = section.path.replace(/\\/g, '/');
2425
+ if (section.kind === 'add') {
2426
+ out.push('--- /dev/null');
2427
+ out.push(`+++ b/${displayPath}`);
2428
+ out.push(`@@ -0,0 +1,${section.lines.length} @@`);
2429
+ for (const line of section.lines) out.push(`+${line}`);
2430
+ continue;
2431
+ }
2432
+ if (section.kind === 'delete') {
2433
+ const fullPath = resolveV4AEntryPath(basePath, section.path);
2434
+ let fileLines = [];
2435
+ try {
2436
+ // Non-UTF-8 targets (UTF-16 BOM / binary) cannot round-trip through
2437
+ // decoded `-` lines — the native byte compare rejects the hunk and the
2438
+ // file becomes UNDELETABLE through apply_patch. A delete needs no
2439
+ // content match; emit the header-only form (already the unreadable-
2440
+ // file shape below) and let the engine remove the file by intent.
2441
+ const _delRaw = readFileSync(fullPath);
2442
+ // decodeValidUtf8OrNull (fatal TextDecoder) instead of Buffer.isUtf8:
2443
+ // the daemon may run under a runtime where Buffer.isUtf8 is absent,
2444
+ // and a missing-API fallback of "assume UTF-8" silently re-enables
2445
+ // the content hunks this gate exists to suppress.
2446
+ if (decodeValidUtf8OrNull(_delRaw) !== null) {
2447
+ fileLines = v4aConversionSourceLines(fullPath, v4aLinesCache);
2448
+ }
2449
+ } catch {
2450
+ fileLines = [];
2451
+ }
2452
+ out.push(`--- a/${displayPath}`);
2453
+ out.push('+++ /dev/null');
2454
+ if (fileLines.length > 0) {
2455
+ out.push(`@@ -1,${fileLines.length} +0,0 @@`);
2456
+ for (const line of fileLines) out.push(`-${line}`);
2457
+ }
2458
+ continue;
2459
+ }
2460
+
2461
+ const fullPath = resolveV4AEntryPath(basePath, section.path);
2462
+ let sourceLines;
2463
+ try {
2464
+ sourceLines = v4aConversionSourceLines(fullPath, v4aLinesCache);
2465
+ } catch (err) {
2466
+ throw new Error(`V4A update target unreadable: ${section.path} (${err?.code || err?.message || String(err)}).`);
2467
+ }
2468
+ const sectionHunks = [];
2469
+ const orderedHunks = orderV4AHunksByFilePosition(sourceLines, section.hunks, fuzzy);
2470
+ let nextSearchLine = 0;
2471
+ for (const hunk of orderedHunks) {
2472
+ const stats = v4AHunkLineStats(hunk);
2473
+ if (stats.oldCount === 0 && stats.newCount === 0) continue;
2474
+ const loc = resolveV4AHunkPosition(sourceLines, hunk, nextSearchLine, { fuzzy });
2475
+ if (loc.skip) continue;
2476
+ if (loc.error) {
2477
+ const msg = `${loc.error.replace(/^V4A hunk /, `V4A hunk ${section.path}: `)}`;
2478
+ if (rejectPartial) throw new Error(msg);
2479
+ if (rejectedHunks) rejectedHunks.push({ file: section.path, hunk, reason: msg });
2480
+ continue;
2481
+ }
2482
+ const oldStart = stats.oldCount === 0 ? loc.oldStartIdx : loc.oldStartIdx + 1;
2483
+ const newStart = oldStart;
2484
+ const tail = (hunk.anchors || []).filter(Boolean).join(' ');
2485
+ const oldCount = stats.oldCount === 0 ? 0 : loc.matchLen;
2486
+ const newCount = stats.newCount - (loc.trimmedTrailingNew || 0);
2487
+ sectionHunks.push(`@@ -${oldStart},${oldCount} +${newStart},${newCount} @@${tail ? ` ${tail}` : ''}`);
2488
+ // EOF-trim: resolveV4AHunkPosition dropped the trailing empty line from
2489
+ // oldLinesPattern (and optionally newLinesPattern). Drop the matching
2490
+ // trailing body line(s) so old/new body counts equal the header counts.
2491
+ let dropOldAt = -1;
2492
+ let dropNewAt = -1;
2493
+ if (loc.trimmedTrailing) {
2494
+ for (let i = hunk.lines.length - 1; i >= 0; i--) {
2495
+ const ln = hunk.lines[i];
2496
+ if (isV4AEndOfFileMarker(ln)) continue;
2497
+ const p = ln[0];
2498
+ if (dropOldAt < 0 && (p === ' ' || p === '-')) dropOldAt = i;
2499
+ if (dropNewAt < 0 && loc.trimmedTrailingNew && (p === ' ' || p === '+')) dropNewAt = i;
2500
+ if (dropOldAt >= 0 && (!loc.trimmedTrailingNew || dropNewAt >= 0)) break;
2501
+ }
2502
+ }
2503
+ let srcIdx = loc.oldStartIdx;
2504
+ const srcEnd = loc.oldStartIdx + loc.matchLen;
2505
+ for (let i = 0; i < hunk.lines.length; i++) {
2506
+ const line = hunk.lines[i];
2507
+ if (isV4AEndOfFileMarker(line)) continue;
2508
+ const prefix = line[0];
2509
+ if (prefix === ' ' || prefix === '-') {
2510
+ if (i === dropOldAt || i === dropNewAt) continue;
2511
+ if (srcIdx < srcEnd && srcIdx < sourceLines.length) {
2512
+ sectionHunks.push(prefix + sourceLines[srcIdx]);
2513
+ } else {
2514
+ sectionHunks.push(line);
2515
+ }
2516
+ srcIdx++;
2517
+ } else {
2518
+ if (i === dropNewAt) continue;
2519
+ sectionHunks.push(line);
2520
+ }
2521
+ }
2522
+ nextSearchLine = loc.nextSearchLine;
2523
+ }
2524
+ if (sectionHunks.length > 0) {
2525
+ out.push(`--- a/${displayPath}`);
2526
+ out.push(`+++ b/${displayPath}`);
2527
+ for (const line of sectionHunks) out.push(line);
2528
+ }
2529
+ }
2530
+ return out.join('\n') + '\n';
2531
+ }
2532
+
2533
+ // Native-only apply_patch entry point.
2534
+ // - Pre-validates security (path-escape, symlink-escape, duplicates).
2535
+ // - V4A / unified-bare V4A inputs are converted to standard unified first.
2536
+ // - parsePatch errors / unsupported headers / missing binary throw clean
2537
+ // Error strings — they DO NOT fall back to a JS engine.
2538
+ // - Native engine handles fuzz / reject_partial / hunkless-delete /
2539
+ // zero-length-delete entirely. fuzzy:false → fuzz 0 (strict), else 2.
2540
+ // - On OK the response includes a per-entry success line + native trace.
2541
+ // - On OK_PARTIAL the response prefix is "Error: patch partially applied"
2542
+ // and per-entry SKIP lines surface the Rust failure reasons.
2543
+ // Some providers (notably grok-composer) serialize a multi-line V4A `patch`
2544
+ // argument as a flat key:value object: the `*** Update File: <path>` header's
2545
+ // colon-space plus the newlines make the tool-arg decoder split each patch line
2546
+ // into alternating keys/values, so `patch` arrives as just "*** Begin Patch"
2547
+ // and the real body leaks into stray top-level keys. Rebuild the original by
2548
+ // re-joining the patch value with the stray entries in insertion order; the V4A
2549
+ // parser + native engine then apply it normally. The trigger is tight (an
2550
+ // incomplete `*** Begin Patch` opener with no newline, plus keys outside the
2551
+ // schema) so well-formed calls are untouched. Keys on the shape, not the model.
2552
+ const APPLY_PATCH_SCHEMA_KEYS = new Set(['patch', 'format', 'base_path', 'dry_run', 'reject_partial', 'fuzzy']);
2553
+ function salvageShatteredV4APatchArgs(args) {
2554
+ if (!args || typeof args !== 'object') return args;
2555
+ const rawPatch = typeof args.patch === 'string' ? args.patch : '';
2556
+ if (!rawPatch.startsWith('*** Begin Patch') || rawPatch.includes('\n') || rawPatch.includes('*** End Patch')) return args;
2557
+ const stray = Object.keys(args).filter((k) => !APPLY_PATCH_SCHEMA_KEYS.has(k));
2558
+ if (stray.length === 0) return args;
2559
+ const lines = [rawPatch];
2560
+ for (const key of Object.keys(args)) {
2561
+ if (APPLY_PATCH_SCHEMA_KEYS.has(key)) continue;
2562
+ lines.push(key);
2563
+ lines.push(String(args[key] ?? ''));
2564
+ }
2565
+ while (lines.length && lines[lines.length - 1] === '') lines.pop();
2566
+ const cleaned = {};
2567
+ for (const key of Object.keys(args)) if (APPLY_PATCH_SCHEMA_KEYS.has(key)) cleaned[key] = args[key];
2568
+ cleaned.patch = lines.join('\n');
2569
+ return cleaned;
2570
+ }
2571
+ async function apply_patch(args, cwd, options = {}) {
2572
+ args = salvageShatteredV4APatchArgs(args);
2573
+ // Strip a leading UTF-8 BOM up-front: editors / PowerShell redirections
2574
+ // sometimes prepend `\uFEFF` to text files and the bare BOM trips the
2575
+ // unified envelope check.
2576
+ const patchStr = (typeof args?.patch === 'string' ? args.patch : '').replace(/^\uFEFF/, '');
2577
+ if (!patchStr.trim()) {
2578
+ throw new Error('apply_patch: "patch" is required (unified diff or V4A patch string)');
2579
+ }
2580
+ const requestedFormat = String(args?.format || '').toLowerCase();
2581
+ if (requestedFormat && requestedFormat !== 'unified' && requestedFormat !== 'v4a') {
2582
+ throw new Error('apply_patch: "format" must be "unified" or "v4a"');
2583
+ }
2584
+ let mutationPlan = options?.mutationPlan || planApplyPatchMutationRoute(args, patchStr, requestedFormat);
2585
+ const readStateScope = options?.readStateScope ?? options?.sessionId ?? null;
2586
+ let abortSignal = options?.signal || options?.abortSignal || null;
2587
+ if (!abortSignal && options?.sessionId) {
2588
+ try { abortSignal = await getAbortSignalForSession(options.sessionId); } catch { abortSignal = null; }
2589
+ }
2590
+ if (abortSignal?.aborted) {
2591
+ throw new Error(abortSignal.reason?.message || abortSignal.reason || 'apply_patch aborted');
2592
+ }
2593
+ const basePath = resolveBasePath(cwd, args?.base_path);
2594
+ try {
2595
+ await assertPathReachable(basePath);
2596
+ } catch (err) {
2597
+ return `Error: ${err?.message || String(err)}`;
2598
+ }
2599
+ // Default true — file-batch atomic. reject_partial:false unlocks the
2600
+ // native engine's OK_PARTIAL isolation mode.
2601
+ const rejectPartial = args?.reject_partial !== false;
2602
+ const dryRun = args?.dry_run === true;
2603
+ const fuzzy = args?.fuzzy !== false;
2604
+ // fuzzy:false → strict context match (fuzz 0); else allow 2 lines of
2605
+ // outer-context drift and ignore context trailing spaces/tabs. The same
2606
+ // fuzz value is forwarded to the V4A line-sequence search so both layers agree.
2607
+ const fuzz = fuzzy ? 2 : 0;
2608
+
2609
+ // V4A → unified conversion (in JS). Hunk anchor/context miss in the
2610
+ // conversion stage surfaces a clean Error — no JS apply fallback.
2611
+ let inputPatchStr = patchStr;
2612
+ const rejectedV4AHunks = [];
2613
+ const v4aConvertOpts = { rejectPartial, rejectedHunks: rejectedV4AHunks, fuzzy, dryRun, readStateScope };
2614
+ let v4aRenamePlan = null;
2615
+ if (isV4APatchInput(patchStr, requestedFormat)) {
2616
+ try {
2617
+ const allSections = parseV4APatch(patchStr);
2618
+ v4aRenamePlan = await planV4ARenameSections(allSections, basePath);
2619
+ inputPatchStr = await convertV4ASectionsToUnifiedPatch(v4aRenamePlan.remainingSections, basePath, v4aConvertOpts);
2620
+ if (v4aRenamePlan.renameSections.length > 0) {
2621
+ mutationPlan = v4aRenamePlan.remainingSections.length > 0
2622
+ ? { sourceTool: 'apply_patch', engine: 'v4a-patch', reason: 'v4a-mixed' }
2623
+ : { sourceTool: 'apply_patch', engine: 'v4a-rename', reason: 'v4a-move' };
2624
+ }
2625
+ } catch (err) {
2626
+ throw new Error(`apply_patch: V4A parse failed — ${err?.message || String(err)}`);
2627
+ }
2628
+ } else if (requestedFormat !== 'unified' && hasUnifiedBareV4AHunk(patchStr)) {
2629
+ try {
2630
+ inputPatchStr = await convertUnifiedBareV4AToUnifiedPatch(patchStr, basePath, v4aConvertOpts);
2631
+ } catch (err) {
2632
+ throw new Error(`apply_patch: bare @@ parse failed — ${err?.message || String(err)}`);
2633
+ }
2634
+ }
2635
+ let normalizedPatchStr = prepareInput(inputPatchStr);
2636
+ const v4aRenameOnly = v4aRenamePlan?.renameSections?.length > 0 && v4aRenamePlan.remainingSections.length === 0;
2637
+
2638
+ // parsePatch remains strict. In auto mode only, counted unified diffs
2639
+ // with bad @@ counts can be reinterpreted through the V4A converter so
2640
+ // exact old/context lines are still verified before native apply.
2641
+ let parsed = [];
2642
+ if (!v4aRenameOnly) try {
2643
+ parsed = parsePatch(normalizedPatchStr);
2644
+ } catch (err) {
2645
+ if (!canFallbackCountedUnified(patchStr, requestedFormat, err)) {
2646
+ throw new Error(`apply_patch: parse failed — ${err?.message || String(err)}; prefer Codex/V4A envelope for multi-hunk edits (no @@ line counts)`);
2647
+ }
2648
+ try {
2649
+ inputPatchStr = await convertUnifiedCountedToUnifiedPatchViaV4A(patchStr, basePath, v4aConvertOpts);
2650
+ normalizedPatchStr = prepareInput(inputPatchStr);
2651
+ parsed = parsePatch(normalizedPatchStr);
2652
+ mutationPlan = {
2653
+ sourceTool: 'apply_patch',
2654
+ engine: 'v4a-patch',
2655
+ reason: 'unified-count-fallback',
2656
+ };
2657
+ } catch (fallbackErr) {
2658
+ throw new Error(`apply_patch: parse failed — ${err?.message || String(err)}; V4A fallback failed — ${fallbackErr?.message || String(fallbackErr)}`);
2659
+ }
2660
+ }
2661
+ if (!v4aRenameOnly && (!Array.isArray(parsed) || parsed.length === 0)) {
2662
+ return 'Error: patch contained no file sections';
2663
+ }
2664
+
2665
+ // Pre-validate paths / duplicates / symlink escapes — throws on any
2666
+ // unsupported entry. Throws bubble out to the tool dispatcher as clean
2667
+ // "Error: ..." strings.
2668
+ if (!v4aRenameOnly) {
2669
+ try {
2670
+ await ensureNativePatchBinaryAvailable();
2671
+ } catch (err) {
2672
+ return `Error: ${err?.message || String(err)}`;
2673
+ }
2674
+ }
2675
+ let entries = [];
2676
+ let headerRewrites = [];
2677
+ if (!v4aRenameOnly) {
2678
+ try {
2679
+ ({ entries, headerRewrites } = await preValidateNativeBatch(parsed, basePath));
2680
+ } catch (err) {
2681
+ return `Error: ${err?.message || String(err)}`;
2682
+ }
2683
+ }
2684
+
2685
+ const _lockPaths = [
2686
+ ...entries.map((entry) => entry.fullPath),
2687
+ ...(v4aRenamePlan?.renameSections || []).flatMap((section) => {
2688
+ const src = resolveV4AEntryPath(basePath, section.path);
2689
+ const dest = resolveV4AEntryPath(basePath, section.movePath);
2690
+ return [src, dest];
2691
+ }),
2692
+ ];
2693
+
2694
+ return withBuiltinPathLocks(_lockPaths, () =>
2695
+ withAdvisoryLocks(_lockPaths, async () => {
2696
+ let v4aRenameResults = [];
2697
+ if (v4aRenamePlan?.renameSections?.length) {
2698
+ v4aRenameResults = await applyV4ARenameSections(v4aRenamePlan.renameSections, basePath, v4aConvertOpts);
2699
+ }
2700
+ if (v4aRenameOnly) {
2701
+ const lines = formatV4ARenameSuccessLines(v4aRenameResults);
2702
+ if (lines.length === 0) return 'Error: patch contained no applicable file sections';
2703
+ return wrapPatchMutationOutput(`${lines.join('\n')}\n`, mutationPlan, { backend: 'v4a-rename' });
2704
+ }
2705
+ const nativePatchStr = rewriteHeaderPaths(normalizedPatchStr, headerRewrites);
2706
+ const nativeResult = await dispatchNativePatch({
2707
+ entries,
2708
+ basePath,
2709
+ nativePatchStr,
2710
+ fuzz,
2711
+ rejectPartial,
2712
+ dryRun,
2713
+ readStateScope,
2714
+ signal: abortSignal,
2715
+ parsed,
2716
+ });
2717
+ // V4A conversion may have isolated some hunks (rejectPartial:false).
2718
+ // Surface them as additional REJECT lines so callers see every dropped
2719
+ // change, native or JS-side.
2720
+ let combined = nativeResult;
2721
+ const renameLines = formatV4ARenameSuccessLines(v4aRenameResults);
2722
+ if (renameLines.length > 0 && !isPatchErrorText(nativeResult)) {
2723
+ combined = `${renameLines.join('\n')}\n${nativeResult}`;
2724
+ }
2725
+ if (!isPatchErrorText(combined) && rejectedV4AHunks.length > 0) {
2726
+ const tail = [
2727
+ '',
2728
+ `hunk-level rejected (rejectPartial=false, V4A): ${rejectedV4AHunks.length}`,
2729
+ ...rejectedV4AHunks.map((r) => ` REJECT ${r.file || '(unknown)'} — ${String(r.reason || '').split(';')[0].trim()}`),
2730
+ ];
2731
+ return wrapPatchMutationOutput(`${combined}\n${tail.join('\n')}`, mutationPlan, { backend: 'native-patch' });
2732
+ }
2733
+ return wrapPatchMutationOutput(combined, mutationPlan, { backend: 'native-patch' });
2734
+ }));
2735
+ }
2736
+
2737
+ // Test-only export: lets the regression harness exercise the interior-vs-outer
2738
+ // change-band logic in findFirstFailingUnifiedHunk without spawning the native
2739
+ // binary.
2740
+ export const __patchTestHooks = { findFirstFailingUnifiedHunk, computeUnifiedChangeBand, collectUnifiedOps, unifiedOldLinesMatchAt, splitBufferLinesForPatch };
2741
+
2742
+ export async function executePatchTool(name, args, cwd, options = {}) {
2743
+ const effectiveCwd = cwd || process.cwd();
2744
+ switch (name) {
2745
+ case 'apply_patch': {
2746
+ let result;
2747
+ try {
2748
+ result = await apply_patch(args || {}, effectiveCwd, options);
2749
+ } catch (err) {
2750
+ return `Error: ${err?.message || String(err)}`;
2751
+ }
2752
+ // ② completion progress (claude "Found N" parity). Best-effort, no-op
2753
+ // when onProgress is absent (no progressToken). Never throws — only
2754
+ // emits on success (an "Error:" body is left to the tool result alone).
2755
+ if (typeof options?.onProgress === 'function') {
2756
+ try {
2757
+ const _body = String(result);
2758
+ if (!/^Error[\s:[]/.test(_body)) {
2759
+ if (args?.dry_run === true) options.onProgress('validated');
2760
+ else {
2761
+ const _m = /^(?:applied|checked)\s+(\d+)\b/m.exec(_body);
2762
+ const _n = _m ? Number(_m[1]) : (_body.match(/^\s*OK\s/gm) || []).length;
2763
+ options.onProgress(`applied ${_n} files`);
2764
+ }
2765
+ }
2766
+ } catch { /* best-effort */ }
2767
+ }
2768
+ return result;
2769
+ }
2770
+ default: throw new Error(`Unknown patch tool: ${name}`);
2771
+ }
2772
+ }