agent-nuvira 3.3.3 → 3.3.4

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 (349) hide show
  1. package/README.md +13 -5
  2. package/dist/agent-sdk/src/agent.d.ts +2 -0
  3. package/dist/agent-sdk/src/agent.d.ts.map +1 -1
  4. package/dist/agent-sdk/src/define.d.ts +64 -0
  5. package/dist/agent-sdk/src/define.d.ts.map +1 -0
  6. package/dist/agent-sdk/src/define.js +76 -0
  7. package/dist/agent-sdk/src/define.js.map +1 -0
  8. package/dist/agent-sdk/src/index.d.ts +9 -0
  9. package/dist/agent-sdk/src/index.d.ts.map +1 -1
  10. package/dist/agent-sdk/src/index.js +9 -0
  11. package/dist/agent-sdk/src/index.js.map +1 -1
  12. package/dist/agent-sdk/src/scaffold.d.ts +11 -0
  13. package/dist/agent-sdk/src/scaffold.d.ts.map +1 -1
  14. package/dist/agent-sdk/src/scaffold.js +16 -6
  15. package/dist/agent-sdk/src/scaffold.js.map +1 -1
  16. package/dist/agents/long-form-plan.d.ts.map +1 -1
  17. package/dist/agents/long-form-plan.js +2 -1
  18. package/dist/agents/long-form-plan.js.map +1 -1
  19. package/dist/agents/orchestrator.d.ts.map +1 -1
  20. package/dist/agents/orchestrator.js +5 -4
  21. package/dist/agents/orchestrator.js.map +1 -1
  22. package/dist/cli/agent.d.ts +2 -2
  23. package/dist/cli/agent.js +10 -10
  24. package/dist/cli/chat.d.ts +94 -0
  25. package/dist/cli/chat.d.ts.map +1 -1
  26. package/dist/cli/chat.js +354 -20
  27. package/dist/cli/chat.js.map +1 -1
  28. package/dist/cli/cli-program.d.ts.map +1 -1
  29. package/dist/cli/cli-program.js +5 -0
  30. package/dist/cli/cli-program.js.map +1 -1
  31. package/dist/cli/config.d.ts.map +1 -1
  32. package/dist/cli/config.js +9 -1
  33. package/dist/cli/config.js.map +1 -1
  34. package/dist/cli/doctor.d.ts.map +1 -1
  35. package/dist/cli/doctor.js +3 -2
  36. package/dist/cli/doctor.js.map +1 -1
  37. package/dist/cli/edit.js +2 -2
  38. package/dist/cli/eval.d.ts +17 -0
  39. package/dist/cli/eval.d.ts.map +1 -1
  40. package/dist/cli/eval.js +105 -2
  41. package/dist/cli/eval.js.map +1 -1
  42. package/dist/cli/execute.d.ts +16 -1
  43. package/dist/cli/execute.d.ts.map +1 -1
  44. package/dist/cli/execute.js +192 -22
  45. package/dist/cli/execute.js.map +1 -1
  46. package/dist/cli/loop-executor.d.ts +55 -0
  47. package/dist/cli/loop-executor.d.ts.map +1 -1
  48. package/dist/cli/loop-executor.js +212 -13
  49. package/dist/cli/loop-executor.js.map +1 -1
  50. package/dist/cli/model.d.ts.map +1 -1
  51. package/dist/cli/model.js +9 -8
  52. package/dist/cli/model.js.map +1 -1
  53. package/dist/cli/models.d.ts +1 -1
  54. package/dist/cli/models.js +4 -4
  55. package/dist/cli/parity.d.ts +85 -0
  56. package/dist/cli/parity.d.ts.map +1 -0
  57. package/dist/cli/parity.js +506 -0
  58. package/dist/cli/parity.js.map +1 -0
  59. package/dist/cli/plan.d.ts.map +1 -1
  60. package/dist/cli/plan.js +2 -1
  61. package/dist/cli/plan.js.map +1 -1
  62. package/dist/cli/retrieval.d.ts.map +1 -1
  63. package/dist/cli/retrieval.js +5 -4
  64. package/dist/cli/retrieval.js.map +1 -1
  65. package/dist/cli/sdk.js +4 -4
  66. package/dist/cli/sdk.js.map +1 -1
  67. package/dist/cli/trace.d.ts.map +1 -1
  68. package/dist/cli/trace.js +2 -1
  69. package/dist/cli/trace.js.map +1 -1
  70. package/dist/cli/workflow.js +2 -2
  71. package/dist/cli/workflow.js.map +1 -1
  72. package/dist/config/types.d.ts +44 -0
  73. package/dist/config/types.d.ts.map +1 -1
  74. package/dist/findings/verdicts.d.ts +241 -0
  75. package/dist/findings/verdicts.d.ts.map +1 -0
  76. package/dist/findings/verdicts.js +284 -0
  77. package/dist/findings/verdicts.js.map +1 -0
  78. package/dist/gateway/adapters.d.ts +65 -0
  79. package/dist/gateway/adapters.d.ts.map +1 -1
  80. package/dist/gateway/adapters.js +216 -10
  81. package/dist/gateway/adapters.js.map +1 -1
  82. package/dist/gateway/channel-directory.d.ts +31 -0
  83. package/dist/gateway/channel-directory.d.ts.map +1 -1
  84. package/dist/gateway/channel-directory.js +40 -0
  85. package/dist/gateway/channel-directory.js.map +1 -1
  86. package/dist/gateway/gateway-log.d.ts +1 -1
  87. package/dist/gateway/gateway-log.d.ts.map +1 -1
  88. package/dist/gateway/gateway-log.js.map +1 -1
  89. package/dist/gateway/hooks.d.ts +87 -19
  90. package/dist/gateway/hooks.d.ts.map +1 -1
  91. package/dist/gateway/hooks.js +62 -23
  92. package/dist/gateway/hooks.js.map +1 -1
  93. package/dist/gateway/inbound-media.d.ts +147 -0
  94. package/dist/gateway/inbound-media.d.ts.map +1 -0
  95. package/dist/gateway/inbound-media.js +317 -0
  96. package/dist/gateway/inbound-media.js.map +1 -0
  97. package/dist/gateway/inbox.d.ts +8 -1
  98. package/dist/gateway/inbox.d.ts.map +1 -1
  99. package/dist/gateway/inbox.js.map +1 -1
  100. package/dist/gateway/platform-config.d.ts +14 -0
  101. package/dist/gateway/platform-config.d.ts.map +1 -1
  102. package/dist/gateway/platform-config.js +26 -8
  103. package/dist/gateway/platform-config.js.map +1 -1
  104. package/dist/gateway/realtime.d.ts +114 -0
  105. package/dist/gateway/realtime.d.ts.map +1 -0
  106. package/dist/gateway/realtime.js +402 -0
  107. package/dist/gateway/realtime.js.map +1 -0
  108. package/dist/gateway/registry.d.ts +31 -0
  109. package/dist/gateway/registry.d.ts.map +1 -1
  110. package/dist/gateway/registry.js +224 -4
  111. package/dist/gateway/registry.js.map +1 -1
  112. package/dist/gateway/whatsapp/baileys-bridge.d.ts +11 -1
  113. package/dist/gateway/whatsapp/baileys-bridge.d.ts.map +1 -1
  114. package/dist/gateway/whatsapp/baileys-bridge.js +123 -4
  115. package/dist/gateway/whatsapp/baileys-bridge.js.map +1 -1
  116. package/dist/gateway/whatsapp/bridge.d.ts +6 -2
  117. package/dist/gateway/whatsapp/bridge.d.ts.map +1 -1
  118. package/dist/gateway/whatsapp/bridge.js.map +1 -1
  119. package/dist/index.js +8 -0
  120. package/dist/index.js.map +1 -1
  121. package/dist/inference/factory.d.ts +14 -0
  122. package/dist/inference/factory.d.ts.map +1 -1
  123. package/dist/inference/factory.js +17 -0
  124. package/dist/inference/factory.js.map +1 -1
  125. package/dist/inference/groq-adapter.d.ts +2 -0
  126. package/dist/inference/groq-adapter.d.ts.map +1 -1
  127. package/dist/inference/groq-adapter.js +16 -6
  128. package/dist/inference/groq-adapter.js.map +1 -1
  129. package/dist/inference/tools.d.ts.map +1 -1
  130. package/dist/inference/tools.js +29 -0
  131. package/dist/inference/tools.js.map +1 -1
  132. package/dist/learning/benchmark.d.ts.map +1 -1
  133. package/dist/learning/benchmark.js +3 -2
  134. package/dist/learning/benchmark.js.map +1 -1
  135. package/dist/learning/continuation.d.ts.map +1 -1
  136. package/dist/learning/continuation.js +2 -1
  137. package/dist/learning/continuation.js.map +1 -1
  138. package/dist/learning/cost-tracker.d.ts.map +1 -1
  139. package/dist/learning/cost-tracker.js +2 -1
  140. package/dist/learning/cost-tracker.js.map +1 -1
  141. package/dist/learning/deferred-task.d.ts.map +1 -1
  142. package/dist/learning/deferred-task.js +13 -4
  143. package/dist/learning/deferred-task.js.map +1 -1
  144. package/dist/learning/eval-framework.d.ts.map +1 -1
  145. package/dist/learning/eval-framework.js +2 -1
  146. package/dist/learning/eval-framework.js.map +1 -1
  147. package/dist/learning/long-form.d.ts.map +1 -1
  148. package/dist/learning/long-form.js +2 -1
  149. package/dist/learning/long-form.js.map +1 -1
  150. package/dist/learning/model-registry.d.ts.map +1 -1
  151. package/dist/learning/model-registry.js +2 -1
  152. package/dist/learning/model-registry.js.map +1 -1
  153. package/dist/learning/reasoning-cache.d.ts.map +1 -1
  154. package/dist/learning/reasoning-cache.js +2 -1
  155. package/dist/learning/reasoning-cache.js.map +1 -1
  156. package/dist/learning/reasoning-trace.d.ts +37 -1
  157. package/dist/learning/reasoning-trace.d.ts.map +1 -1
  158. package/dist/learning/reasoning-trace.js +66 -0
  159. package/dist/learning/reasoning-trace.js.map +1 -1
  160. package/dist/learning/resilient-call.d.ts.map +1 -1
  161. package/dist/learning/resilient-call.js +2 -1
  162. package/dist/learning/resilient-call.js.map +1 -1
  163. package/dist/learning/retrieval.d.ts.map +1 -1
  164. package/dist/learning/retrieval.js +2 -1
  165. package/dist/learning/retrieval.js.map +1 -1
  166. package/dist/learning/seeded-benchmark.d.ts +160 -0
  167. package/dist/learning/seeded-benchmark.d.ts.map +1 -0
  168. package/dist/learning/seeded-benchmark.js +321 -0
  169. package/dist/learning/seeded-benchmark.js.map +1 -0
  170. package/dist/learning/seeded-bugs.d.ts +142 -0
  171. package/dist/learning/seeded-bugs.d.ts.map +1 -0
  172. package/dist/learning/seeded-bugs.js +535 -0
  173. package/dist/learning/seeded-bugs.js.map +1 -0
  174. package/dist/learning/step-checkpoint.d.ts +127 -0
  175. package/dist/learning/step-checkpoint.d.ts.map +1 -0
  176. package/dist/learning/step-checkpoint.js +244 -0
  177. package/dist/learning/step-checkpoint.js.map +1 -0
  178. package/dist/nlu/intent-confirm.d.ts +23 -1
  179. package/dist/nlu/intent-confirm.d.ts.map +1 -1
  180. package/dist/nlu/intent-confirm.js +85 -2
  181. package/dist/nlu/intent-confirm.js.map +1 -1
  182. package/dist/nlu/learnings.d.ts +7 -0
  183. package/dist/nlu/learnings.d.ts.map +1 -1
  184. package/dist/nlu/learnings.js +7 -0
  185. package/dist/nlu/learnings.js.map +1 -1
  186. package/dist/observability/debug-log.d.ts +250 -0
  187. package/dist/observability/debug-log.d.ts.map +1 -0
  188. package/dist/observability/debug-log.js +500 -0
  189. package/dist/observability/debug-log.js.map +1 -0
  190. package/dist/observability/event-bus.d.ts.map +1 -1
  191. package/dist/observability/event-bus.js +4 -1
  192. package/dist/observability/event-bus.js.map +1 -1
  193. package/dist/observability/otel.d.ts +278 -0
  194. package/dist/observability/otel.d.ts.map +1 -0
  195. package/dist/observability/otel.js +590 -0
  196. package/dist/observability/otel.js.map +1 -0
  197. package/dist/parity/drivers.d.ts +99 -0
  198. package/dist/parity/drivers.d.ts.map +1 -0
  199. package/dist/parity/drivers.js +1362 -0
  200. package/dist/parity/drivers.js.map +1 -0
  201. package/dist/parity/graph.d.ts +73 -0
  202. package/dist/parity/graph.d.ts.map +1 -0
  203. package/dist/parity/graph.js +162 -0
  204. package/dist/parity/graph.js.map +1 -0
  205. package/dist/parity/matrix.d.ts +105 -0
  206. package/dist/parity/matrix.d.ts.map +1 -0
  207. package/dist/parity/matrix.js +352 -0
  208. package/dist/parity/matrix.js.map +1 -0
  209. package/dist/parity/observation.d.ts +444 -0
  210. package/dist/parity/observation.d.ts.map +1 -0
  211. package/dist/parity/observation.js +333 -0
  212. package/dist/parity/observation.js.map +1 -0
  213. package/dist/parity/scenarios.d.ts +229 -0
  214. package/dist/parity/scenarios.d.ts.map +1 -0
  215. package/dist/parity/scenarios.js +175 -0
  216. package/dist/parity/scenarios.js.map +1 -0
  217. package/dist/parity/surfaces.d.ts +122 -0
  218. package/dist/parity/surfaces.d.ts.map +1 -0
  219. package/dist/parity/surfaces.js +190 -0
  220. package/dist/parity/surfaces.js.map +1 -0
  221. package/dist/runtime/fault-injection.d.ts +173 -0
  222. package/dist/runtime/fault-injection.d.ts.map +1 -0
  223. package/dist/runtime/fault-injection.js +281 -0
  224. package/dist/runtime/fault-injection.js.map +1 -0
  225. package/dist/tools/child-agent-entry.d.ts +23 -0
  226. package/dist/tools/child-agent-entry.d.ts.map +1 -0
  227. package/dist/tools/child-agent-entry.js +129 -0
  228. package/dist/tools/child-agent-entry.js.map +1 -0
  229. package/dist/tools/child-agent-runtime.d.ts +124 -0
  230. package/dist/tools/child-agent-runtime.d.ts.map +1 -0
  231. package/dist/tools/child-agent-runtime.js +704 -0
  232. package/dist/tools/child-agent-runtime.js.map +1 -0
  233. package/dist/tools/coding-tools.d.ts.map +1 -1
  234. package/dist/tools/coding-tools.js +82 -10
  235. package/dist/tools/coding-tools.js.map +1 -1
  236. package/dist/tools/delegation-system.d.ts +31 -0
  237. package/dist/tools/delegation-system.d.ts.map +1 -1
  238. package/dist/tools/delegation-system.js +70 -9
  239. package/dist/tools/delegation-system.js.map +1 -1
  240. package/dist/tools/extract/docx.d.ts +27 -0
  241. package/dist/tools/extract/docx.d.ts.map +1 -0
  242. package/dist/tools/extract/docx.js +48 -0
  243. package/dist/tools/extract/docx.js.map +1 -0
  244. package/dist/tools/extract/html-text.d.ts +27 -0
  245. package/dist/tools/extract/html-text.d.ts.map +1 -0
  246. package/dist/tools/extract/html-text.js +86 -0
  247. package/dist/tools/extract/html-text.js.map +1 -0
  248. package/dist/tools/extract/pdf-ocr.d.ts +58 -0
  249. package/dist/tools/extract/pdf-ocr.d.ts.map +1 -0
  250. package/dist/tools/extract/pdf-ocr.js +116 -0
  251. package/dist/tools/extract/pdf-ocr.js.map +1 -0
  252. package/dist/tools/extract/pdf.d.ts +65 -0
  253. package/dist/tools/extract/pdf.d.ts.map +1 -0
  254. package/dist/tools/extract/pdf.js +197 -0
  255. package/dist/tools/extract/pdf.js.map +1 -0
  256. package/dist/tools/extract/pptx.d.ts +32 -0
  257. package/dist/tools/extract/pptx.d.ts.map +1 -0
  258. package/dist/tools/extract/pptx.js +77 -0
  259. package/dist/tools/extract/pptx.js.map +1 -0
  260. package/dist/tools/extract/xlsx.d.ts +47 -0
  261. package/dist/tools/extract/xlsx.d.ts.map +1 -0
  262. package/dist/tools/extract/xlsx.js +111 -0
  263. package/dist/tools/extract/xlsx.js.map +1 -0
  264. package/dist/tools/finding-tool.d.ts +76 -0
  265. package/dist/tools/finding-tool.d.ts.map +1 -0
  266. package/dist/tools/finding-tool.js +125 -0
  267. package/dist/tools/finding-tool.js.map +1 -0
  268. package/dist/tools/messaging-tools.d.ts +41 -11
  269. package/dist/tools/messaging-tools.d.ts.map +1 -1
  270. package/dist/tools/messaging-tools.js +104 -55
  271. package/dist/tools/messaging-tools.js.map +1 -1
  272. package/dist/tools/neutts-synth.d.ts +27 -3
  273. package/dist/tools/neutts-synth.d.ts.map +1 -1
  274. package/dist/tools/neutts-synth.js +57 -13
  275. package/dist/tools/neutts-synth.js.map +1 -1
  276. package/dist/tools/pipeline-tool.d.ts +43 -1
  277. package/dist/tools/pipeline-tool.d.ts.map +1 -1
  278. package/dist/tools/pipeline-tool.js +13 -2
  279. package/dist/tools/pipeline-tool.js.map +1 -1
  280. package/dist/tools/read-extract.d.ts +116 -45
  281. package/dist/tools/read-extract.d.ts.map +1 -1
  282. package/dist/tools/read-extract.js +494 -158
  283. package/dist/tools/read-extract.js.map +1 -1
  284. package/dist/tools/registry.d.ts +2 -2
  285. package/dist/tools/registry.d.ts.map +1 -1
  286. package/dist/tools/registry.js +152 -23
  287. package/dist/tools/registry.js.map +1 -1
  288. package/dist/tools/subagent-refusal.d.ts +15 -0
  289. package/dist/tools/subagent-refusal.d.ts.map +1 -0
  290. package/dist/tools/subagent-refusal.js +18 -0
  291. package/dist/tools/subagent-refusal.js.map +1 -0
  292. package/dist/tools/subagent-spawner.d.ts +122 -0
  293. package/dist/tools/subagent-spawner.d.ts.map +1 -1
  294. package/dist/tools/subagent-spawner.js +249 -28
  295. package/dist/tools/subagent-spawner.js.map +1 -1
  296. package/dist/tools/tool-hooks.d.ts +177 -0
  297. package/dist/tools/tool-hooks.d.ts.map +1 -0
  298. package/dist/tools/tool-hooks.js +427 -0
  299. package/dist/tools/tool-hooks.js.map +1 -0
  300. package/dist/tools/tool-loop.d.ts +82 -0
  301. package/dist/tools/tool-loop.d.ts.map +1 -1
  302. package/dist/tools/tool-loop.js +167 -9
  303. package/dist/tools/tool-loop.js.map +1 -1
  304. package/dist/tools/tool-refusal.d.ts +68 -0
  305. package/dist/tools/tool-refusal.d.ts.map +1 -0
  306. package/dist/tools/tool-refusal.js +78 -0
  307. package/dist/tools/tool-refusal.js.map +1 -0
  308. package/dist/tools/toolsets.d.ts +8 -0
  309. package/dist/tools/toolsets.d.ts.map +1 -1
  310. package/dist/tools/toolsets.js +12 -2
  311. package/dist/tools/toolsets.js.map +1 -1
  312. package/dist/tools/vision-tools.d.ts +88 -83
  313. package/dist/tools/vision-tools.d.ts.map +1 -1
  314. package/dist/tools/vision-tools.js +134 -103
  315. package/dist/tools/vision-tools.js.map +1 -1
  316. package/dist/tools/worktree.d.ts +210 -0
  317. package/dist/tools/worktree.d.ts.map +1 -0
  318. package/dist/tools/worktree.js +374 -0
  319. package/dist/tools/worktree.js.map +1 -0
  320. package/dist/utils/format.d.ts +3 -0
  321. package/dist/utils/format.d.ts.map +1 -0
  322. package/dist/utils/format.js +32 -0
  323. package/dist/utils/format.js.map +1 -0
  324. package/dist/web-dashboard/attachment-extract.d.ts +64 -0
  325. package/dist/web-dashboard/attachment-extract.d.ts.map +1 -0
  326. package/dist/web-dashboard/attachment-extract.js +154 -0
  327. package/dist/web-dashboard/attachment-extract.js.map +1 -0
  328. package/dist/web-dashboard/chat-console.d.ts +108 -1
  329. package/dist/web-dashboard/chat-console.d.ts.map +1 -1
  330. package/dist/web-dashboard/chat-console.js +36 -0
  331. package/dist/web-dashboard/chat-console.js.map +1 -1
  332. package/dist/web-dashboard/hub-data.d.ts +37 -0
  333. package/dist/web-dashboard/hub-data.d.ts.map +1 -1
  334. package/dist/web-dashboard/hub-data.js +61 -1
  335. package/dist/web-dashboard/hub-data.js.map +1 -1
  336. package/dist/web-dashboard/server.d.ts +14 -0
  337. package/dist/web-dashboard/server.d.ts.map +1 -1
  338. package/dist/web-dashboard/server.js +247 -13
  339. package/dist/web-dashboard/server.js.map +1 -1
  340. package/dist/web-dashboard/src/types.d.ts +196 -48
  341. package/dist/web-dashboard/src/types.d.ts.map +1 -1
  342. package/package.json +18 -3
  343. package/src/web-dashboard/public/assets/{index-Cyd6tIew.css → index-XJjj2cBX.css} +1 -1
  344. package/src/web-dashboard/public/assets/index-YWF9FpwQ.js +207 -0
  345. package/src/web-dashboard/public/assets/index-YWF9FpwQ.js.map +1 -0
  346. package/src/web-dashboard/public/index.html +2 -2
  347. package/dist/tools/child-agent-worker.js +0 -212
  348. package/src/web-dashboard/public/assets/index-CxDj7p6i.js +0 -207
  349. package/src/web-dashboard/public/assets/index-CxDj7p6i.js.map +0 -1
@@ -0,0 +1,374 @@
1
+ /**
2
+ * WS5 (#27) — a turn's own git worktree, and the diff it leaves behind.
3
+ *
4
+ * WHY A WORKTREE AT ALL. The ask is "try this and show me what changed". Running
5
+ * that in the operator's own tree makes the answer inseparable from the damage: a
6
+ * turn that edits twelve files to test one hypothesis has changed the tree the
7
+ * operator is working in, and the only record of it is whatever they notice. A
8
+ * second checkout of the SAME repository (a `git worktree`) gives the turn a real
9
+ * tree — same code, same test runner, same `node_modules` — that can be thrown
10
+ * away, and turns "what changed" into a diff against the commit it started from.
11
+ *
12
+ * WHY THE BASE IS `HEAD`, AND WHY THE NOTICE SAYS SO. `git worktree add … HEAD`
13
+ * checks out a commit, so UNCOMMITTED work in the operator's tree is not visible
14
+ * inside the copy. That is a real trap — a run that "did not see my edit" — so the
15
+ * number of dirty files in the source tree is MEASURED at creation and reported,
16
+ * rather than left to be discovered. A clean tree reports nothing.
17
+ *
18
+ * WHY IT REFUSES INSTEAD OF DEGRADING. Not every directory can be isolated: not a
19
+ * repository, no commit yet, no `git` at all. The tempting behaviour — run in the
20
+ * real tree and report the turn normally — is the one outcome this feature exists
21
+ * to prevent, because the caller acts on a diff that describes a tree nobody ran
22
+ * in. So isolation either happens or the turn STOPS, and the refusal says why.
23
+ *
24
+ * WHY THE DIFF IS MEASURED BEFORE TEARDOWN. Removing the worktree is what
25
+ * destroys the evidence, so the order is not a detail: measure, then remove. A
26
+ * teardown that fails (a git lock, a file handle) is reported as "not removed"
27
+ * rather than assumed, since the directory is left in the operator's profile with
28
+ * a branch pointing at it.
29
+ *
30
+ * GIT'S OWN STDOUT IS KEPT SEPARATE FROM THE MESSAGE. One helper here returns a
31
+ * never-empty `out` (git printing nothing is a fact worth a sentence), and a
32
+ * second field `raw` holding exactly what git wrote. Counting or parsing `out` is
33
+ * how a PRISTINE repository came to be reported as having one uncommitted change:
34
+ * `git status --porcelain` prints nothing, the fallback sentence has one line, and
35
+ * the count was taken from the sentence (see TOOL_TRUTHFULNESS_TRACKER).
36
+ */
37
+ import { spawnSync } from 'node:child_process';
38
+ import { randomBytes } from 'node:crypto';
39
+ import { existsSync, mkdirSync, rmSync, symlinkSync } from 'node:fs';
40
+ import { join } from 'node:path';
41
+ import { envBuff, resolveNuviraHome } from '../config/paths.js';
42
+ /** The environment key a deployment asks for isolation through. */
43
+ export const WORKTREE_ENABLE_ENV = 'NUVIRA_ISOLATE';
44
+ /** The environment keys a forked child reads to learn which worktree it is in. */
45
+ export const WORKTREE_DIR_ENV = 'NUVIRA_WORKTREE_DIR';
46
+ export const WORKTREE_BASE_ENV = 'NUVIRA_WORKTREE_BASE';
47
+ /** How long `git worktree add` may take — a big repository is still a checkout. */
48
+ export const WORKTREE_CREATE_TIMEOUT_MS = 20_000;
49
+ /** Bound on one file's diff body, so a huge change cannot be handed over whole. */
50
+ export const WORKTREE_PATCH_CHARS = 120_000;
51
+ /**
52
+ * Resolve an isolation request from a caller's options, with the environment as
53
+ * the fallback — not the other way round.
54
+ *
55
+ * `undefined` means "nobody said anything", and only then does `NUVIRA_ISOLATE`
56
+ * decide. An explicit `false` is a DECISION (a surface that offers an off switch
57
+ * must be able to turn a deployment's `NUVIRA_ISOLATE=1` off for one turn), and
58
+ * it outranks the environment. This is the distinction a commander-style `false`
59
+ * default destroys: with one, every CLI turn carries an explicit decline and the
60
+ * documented `NUVIRA_ISOLATE=1` is unreachable from the command line.
61
+ */
62
+ export function resolveIsolationRequest(input) {
63
+ const asked = input.worktree === undefined ? envAsks(WORKTREE_ENABLE_ENV) : input.worktree === true;
64
+ // `keep` is only meaningful when isolation was asked for; a lever with nothing
65
+ // to keep would otherwise read as a request in its own right.
66
+ const keep = asked && input.keepWorktree === true;
67
+ return { asked, keep };
68
+ }
69
+ /** True when an environment key asks for something (`1`/`true`/`yes`). */
70
+ function envAsks(name) {
71
+ const raw = envBuff(name.replace(/^NUVIRA_/, ''));
72
+ if (raw === undefined)
73
+ return false;
74
+ const value = raw.trim().toLowerCase();
75
+ return value === '1' || value === 'true' || value === 'yes';
76
+ }
77
+ function gitRun(dir, args, timeoutMs = WORKTREE_CREATE_TIMEOUT_MS) {
78
+ try {
79
+ const r = spawnSync('git', args, {
80
+ cwd: dir,
81
+ encoding: 'utf-8',
82
+ timeout: timeoutMs,
83
+ maxBuffer: 64 * 1024 * 1024,
84
+ });
85
+ const raw = (r.stdout ?? '').trim();
86
+ return {
87
+ ok: r.status === 0,
88
+ raw,
89
+ out: raw || 'git reported no output',
90
+ err: (r.stderr ?? '').trim(),
91
+ };
92
+ }
93
+ catch (err) {
94
+ const message = err instanceof Error ? err.message : String(err);
95
+ return { ok: false, raw: '', out: 'git reported no output', err: message };
96
+ }
97
+ }
98
+ /**
99
+ * Files with uncommitted work in `dir`, counted from git's OWN output.
100
+ *
101
+ * Counts LINES of `raw`: `git status --porcelain` prints one line per changed or
102
+ * untracked path, and prints NOTHING for a clean tree — which must count as zero,
103
+ * not as the one line of a fallback sentence.
104
+ */
105
+ function dirtyCount(dir) {
106
+ const status = gitRun(dir, ['status', '--porcelain'], 5_000);
107
+ if (!status.ok)
108
+ return 0;
109
+ return status.raw.split('\n').filter((line) => line.trim() !== '').length;
110
+ }
111
+ /** The commit a turn would start from, or null when there is none. */
112
+ function headOf(dir) {
113
+ const head = gitRun(dir, ['rev-parse', 'HEAD'], 5_000);
114
+ const sha = head.raw.trim();
115
+ return head.ok && /^[0-9a-f]{40}$/i.test(sha) ? sha : null;
116
+ }
117
+ /**
118
+ * Why `repoCwd` cannot be isolated, or null when it can.
119
+ *
120
+ * Shared by the refusal path and by the parity harness, which uses it to assert
121
+ * that the checkout it is about to drive CAN be isolated — a harness that
122
+ * silently proved nothing because it ran somewhere unisolatable would be worse
123
+ * than a failure.
124
+ */
125
+ export function worktreeRefusal(repoCwd) {
126
+ const inside = gitRun(repoCwd, ['rev-parse', '--is-inside-work-tree'], 5_000);
127
+ if (!inside.ok || inside.raw.trim() !== 'true') {
128
+ return `'${repoCwd}' is not inside a git work tree (git is unavailable, or this is not a repository)`;
129
+ }
130
+ if (!headOf(repoCwd)) {
131
+ return `'${repoCwd}' has no commit to start from — an isolated copy is a checkout of HEAD`;
132
+ }
133
+ return null;
134
+ }
135
+ /** The refusal, as the operator reads it. A failure, not a degradation. */
136
+ export function isolationRefusalText(why) {
137
+ return (`Isolation was requested, but this turn cannot be isolated: ${why}.\n` +
138
+ 'Nothing ran — no model was called and no file was touched.');
139
+ }
140
+ /** Where worktrees live (`~/.nuvira/worktrees`, overridable for tests/CI). */
141
+ function worktreesRoot() {
142
+ return envBuff('WORKTREES_DIR') || join(resolveNuviraHome(), 'worktrees');
143
+ }
144
+ /** A filesystem- and branch-safe name for this turn, from its label. */
145
+ function worktreeName(label) {
146
+ const slug = label
147
+ .toLowerCase()
148
+ .replace(/[^a-z0-9]+/g, '-')
149
+ .replace(/^-+|-+$/g, '')
150
+ .slice(0, 32)
151
+ .replace(/-+$/g, '');
152
+ const stamp = randomBytes(4).toString('hex').slice(0, 6);
153
+ const salt = randomBytes(2).toString('hex').slice(0, 4);
154
+ return `${slug || 'turn'}-${stamp}-${salt}`;
155
+ }
156
+ /**
157
+ * Make a worktree of `repoCwd`, or return null when that is impossible.
158
+ *
159
+ * `node_modules` is LINKED rather than installed: a worktree has no dependencies
160
+ * of its own, and an isolated turn that could not run a test would measure
161
+ * nothing. The link is best-effort — a filesystem that cannot symlink still gets
162
+ * an isolated tree, just one without dependencies.
163
+ */
164
+ export function createIsolatedWorktree(input) {
165
+ if (worktreeRefusal(input.repoCwd) !== null)
166
+ return null;
167
+ const base = headOf(input.repoCwd);
168
+ if (!base)
169
+ return null;
170
+ const name = worktreeName(input.label);
171
+ const dir = join(worktreesRoot(), name);
172
+ const branch = `nuvira/${name}`;
173
+ try {
174
+ mkdirSync(worktreesRoot(), { recursive: true });
175
+ }
176
+ catch {
177
+ return null;
178
+ }
179
+ const added = gitRun(input.repoCwd, ['worktree', 'add', '-b', branch, dir, 'HEAD'], input.timeoutMs ?? WORKTREE_CREATE_TIMEOUT_MS);
180
+ if (!added.ok) {
181
+ // Leave nothing behind: a half-made worktree registers with the parent repo
182
+ // even when the checkout failed.
183
+ try {
184
+ rmSync(dir, { recursive: true, force: true });
185
+ gitRun(input.repoCwd, ['worktree', 'prune'], 5_000);
186
+ }
187
+ catch {
188
+ /* best-effort */
189
+ }
190
+ return null;
191
+ }
192
+ const sourceModules = join(input.repoCwd, 'node_modules');
193
+ const linkedModules = join(dir, 'node_modules');
194
+ if (existsSync(sourceModules) && !existsSync(linkedModules)) {
195
+ try {
196
+ symlinkSync(sourceModules, linkedModules, 'dir');
197
+ }
198
+ catch {
199
+ /* a filesystem that cannot link still gets an isolated tree */
200
+ }
201
+ }
202
+ return { dir, base, branch, repoCwd: input.repoCwd, sourceDirty: dirtyCount(input.repoCwd) };
203
+ }
204
+ /** Split git's own unified diff into one body per file, keyed by path. */
205
+ function payloadOf(patch, files) {
206
+ const sections = patch.split(/^diff --git /m).filter((section) => section.trim() !== '');
207
+ const byPath = new Map();
208
+ for (const section of sections) {
209
+ // `a/<path> b/<path>` — the path with a space in it is why this reads the
210
+ // `b/` side rather than splitting the line on whitespace.
211
+ const match = section.match(/^a\/(.+?) b\//);
212
+ if (!match)
213
+ continue;
214
+ byPath.set(match[1], `diff --git ${section}`.slice(0, WORKTREE_PATCH_CHARS));
215
+ }
216
+ return files.map((path) => ({ path, body: byPath.get(path) ?? '' }));
217
+ }
218
+ /**
219
+ * The pathspec that keeps the linked `node_modules` out of a measurement.
220
+ *
221
+ * The repository's own ignore rule is `node_modules/`, which matches a DIRECTORY —
222
+ * and what a worktree gets is a SYMLINK, which that rule does not match at all. So
223
+ * a run that changed one file reported two, the second being the dependency link
224
+ * this module created itself. Excluded by pathspec rather than by writing an
225
+ * ignore file into the worktree, because the ignore file is the repository's.
226
+ */
227
+ const MEASURE_SCOPE = ['.', ':(exclude)node_modules'];
228
+ /**
229
+ * Measure the diff against the worktree's base commit.
230
+ *
231
+ * UNTRACKED FILES COUNT, and that is why the index is staged first: a run that
232
+ * CREATES a file has changed the tree, and `git diff` alone would report nothing
233
+ * for it. Staging inside a worktree that is about to be thrown away is free, and
234
+ * a worktree that is KEPT has its new files staged — which is what `git diff
235
+ * --cached` reads on the next measurement.
236
+ */
237
+ export function worktreeDiff(handle, opts = {}) {
238
+ const timeout = opts.timeoutMs ?? WORKTREE_CREATE_TIMEOUT_MS;
239
+ gitRun(handle.dir, ['add', '-A', '--', ...MEASURE_SCOPE], timeout);
240
+ const names = gitRun(handle.dir, ['diff', '--cached', '--name-only', handle.base, '--', ...MEASURE_SCOPE], timeout);
241
+ const files = names.ok
242
+ ? names.raw.split('\n').map((line) => line.trim()).filter((line) => line !== '')
243
+ : [];
244
+ const short = handle.base.slice(0, 7);
245
+ const summary = files.length === 0
246
+ ? `nothing changed against ${short}`
247
+ : `${files.length} file${files.length === 1 ? '' : 's'} changed against ${short}`;
248
+ const patch = gitRun(handle.dir, ['diff', '--cached', handle.base, '--', ...MEASURE_SCOPE], timeout);
249
+ return {
250
+ files,
251
+ summary,
252
+ unchanged: files.length === 0,
253
+ payload: { files: payloadOf(patch.ok ? patch.raw : '', files), summary },
254
+ };
255
+ }
256
+ /** The honest "nothing is known yet" diff, for a measurement that failed. */
257
+ function unmeasuredDiff(base) {
258
+ const short = base.slice(0, 7);
259
+ const summary = `the diff against ${short} could not be measured`;
260
+ return { files: [], summary, unchanged: false, payload: { files: [], summary } };
261
+ }
262
+ /**
263
+ * Remove a worktree and the branch that was made with it.
264
+ *
265
+ * Returns whether the directory is GONE, not whether git was happy: a removal
266
+ * that succeeded but left the directory (a file handle, a lock) has still left a
267
+ * directory in the operator's profile, and "removed: true" is the claim the
268
+ * console prints.
269
+ */
270
+ export function discardWorktree(handle, opts = {}) {
271
+ const timeout = opts.timeoutMs ?? WORKTREE_CREATE_TIMEOUT_MS;
272
+ let gitSaid = false;
273
+ try {
274
+ gitSaid = gitRun(handle.repoCwd, ['worktree', 'remove', '--force', handle.dir], timeout).ok;
275
+ }
276
+ catch {
277
+ gitSaid = false;
278
+ }
279
+ if (existsSync(handle.dir)) {
280
+ try {
281
+ rmSync(handle.dir, { recursive: true, force: true });
282
+ }
283
+ catch {
284
+ /* fall through — the directory decides */
285
+ }
286
+ }
287
+ try {
288
+ if (!existsSync(handle.dir))
289
+ gitRun(handle.repoCwd, ['branch', '-D', handle.branch], 5_000);
290
+ gitRun(handle.repoCwd, ['worktree', 'prune'], 5_000);
291
+ }
292
+ catch {
293
+ /* best-effort */
294
+ }
295
+ return gitSaid || !existsSync(handle.dir);
296
+ }
297
+ /**
298
+ * Start isolation for a turn, or return null when nobody asked for one.
299
+ *
300
+ * Null and a refusal are different answers and both are load-bearing: null means
301
+ * the turn runs where it always did, and a refusal means the turn DOES NOT RUN.
302
+ */
303
+ export function beginIsolation(input) {
304
+ if (!input.request.asked)
305
+ return null;
306
+ const why = worktreeRefusal(input.repoCwd);
307
+ if (why)
308
+ return { ok: false, refusal: isolationRefusalText(why) };
309
+ const worktree = createIsolatedWorktree({ repoCwd: input.repoCwd, label: input.label });
310
+ if (!worktree) {
311
+ const reason = worktreeRefusal(input.repoCwd) ?? 'the worktree could not be created';
312
+ return { ok: false, refusal: isolationRefusalText(reason) };
313
+ }
314
+ return { ok: true, worktree };
315
+ }
316
+ /** The environment a forked child reads, so a tool can say where it is running. */
317
+ export function worktreeEnv(handle) {
318
+ return {
319
+ [WORKTREE_DIR_ENV]: handle.dir,
320
+ [WORKTREE_BASE_ENV]: handle.base,
321
+ };
322
+ }
323
+ /**
324
+ * Where the turn is running, said when it starts.
325
+ *
326
+ * The base commit is named as a commit AND as a branch, and the dirty count is
327
+ * reported when it is non-zero — the two facts that decide whether the diff at
328
+ * the end describes what the operator expected.
329
+ */
330
+ export function worktreeNotice(handle) {
331
+ const lines = [
332
+ `🌿 isolated in a git worktree: ${handle.dir}`,
333
+ ` base: ${handle.base.slice(0, 7)} (branch ${handle.branch})`,
334
+ ];
335
+ if (handle.sourceDirty > 0) {
336
+ lines.push(` note: ${handle.sourceDirty} uncommitted change(s) in the source tree are NOT in this worktree`);
337
+ }
338
+ return lines.join('\n');
339
+ }
340
+ /**
341
+ * Finish isolation: measure the diff, then tear the worktree down (unless kept).
342
+ *
343
+ * Never throws — the caller attaches this to the turn's own result, and a git
344
+ * error must not replace an answer with a message about a directory.
345
+ */
346
+ export function endIsolation(handle, opts) {
347
+ let diff;
348
+ try {
349
+ diff = worktreeDiff(handle);
350
+ }
351
+ catch {
352
+ diff = unmeasuredDiff(handle.base);
353
+ }
354
+ let removed = false;
355
+ if (!opts.keep) {
356
+ try {
357
+ removed = discardWorktree(handle);
358
+ }
359
+ catch {
360
+ removed = false;
361
+ }
362
+ }
363
+ const lines = [` ${diff.summary}`];
364
+ for (const file of diff.files)
365
+ lines.push(` · ${file}`);
366
+ if (opts.keep)
367
+ lines.push(` (kept: ${handle.dir} — nothing was removed)`);
368
+ else if (removed)
369
+ lines.push(' (the worktree was removed — the diff above is what is left of it)');
370
+ else
371
+ lines.push(` (the worktree could NOT be removed: ${handle.dir})`);
372
+ return { dir: handle.dir, base: handle.base, diff, removed, notice: lines.join('\n') };
373
+ }
374
+ //# sourceMappingURL=worktree.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"worktree.js","sourceRoot":"","sources":["../../src/tools/worktree.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ /** Format a count for display: `1048576` → `1,048,576`, on every machine. */
2
+ export declare function formatCount(n: number): string;
3
+ //# sourceMappingURL=format.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"format.d.ts","sourceRoot":"","sources":["../../src/utils/format.ts"],"names":[],"mappings":"AA4BA,6EAA6E;AAC7E,wBAAgB,WAAW,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAE7C"}
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Number formatting with a PINNED locale.
3
+ *
4
+ * WHY THIS EXISTS. `n.toLocaleString()` follows the machine's default locale, so
5
+ * one build printed `1,048,576` on an en-US box, `10,48,576` on en-IN and
6
+ * `1.048.576` on de-DE. That is wrong twice over:
7
+ *
8
+ * 1. A count in a report, a log line, or a bug report is a FACT, and it must
9
+ * read the same on every machine.
10
+ * 2. A locale-dependent value cannot be asserted deterministically. The
11
+ * dashboard chip suite and the long-form progress suite passed in CI and
12
+ * failed on a developer machine for exactly this reason, and de-DE renders a
13
+ * word count as `35.000`, which reads as a decimal rather than as 35,000.
14
+ *
15
+ * THE CONVENTION. Every user-visible number goes through {@link formatCount}.
16
+ * This is the same choice the codebase already made in the places it pinned a
17
+ * locale by hand (`toLocaleString('en-US')`, `toLocaleDateString('en-US', …)`) —
18
+ * it is now made once, here, instead of at each call site.
19
+ *
20
+ * SCOPE. Numbers only. Dates still vary by machine in the sites that do not pass
21
+ * a locale; that is a separate decision and is deliberately not covered here.
22
+ *
23
+ * The locale is `en-US` because the grouping is unambiguous: it separates every
24
+ * three digits and never uses a character (a dot, a space, a narrow no-break
25
+ * space) that another locale would read as a decimal point.
26
+ */
27
+ const EN_US = new Intl.NumberFormat('en-US');
28
+ /** Format a count for display: `1048576` → `1,048,576`, on every machine. */
29
+ export function formatCount(n) {
30
+ return EN_US.format(n);
31
+ }
32
+ //# sourceMappingURL=format.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"format.js","sourceRoot":"","sources":["../../src/utils/format.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,MAAM,KAAK,GAAG,IAAI,IAAI,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC;AAE7C,6EAA6E;AAC7E,MAAM,UAAU,WAAW,CAAC,CAAS;IACnC,OAAO,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;AACzB,CAAC"}
@@ -0,0 +1,64 @@
1
+ /**
2
+ * P2 — attachment hydration: turn a composer attachment into text the model can use.
3
+ *
4
+ * The composer used to read EVERY attached file with `File.text()` and ship the result
5
+ * inline. For a PDF that is the file's bytes decoded as UTF-8, which is how the
6
+ * 2026-09-27 incident happened: `[Attachment: report.pdf]` arrived carrying
7
+ * `%PDF-1.4 … /FlateDecode …`, the model could not assess anything, and nothing told
8
+ * anyone extraction had failed. `chat-console.ts` even documented the intended
9
+ * behaviour ("binary files are rejected client-side") while no code implemented it.
10
+ *
11
+ * Now the client encodes what it cannot read as base64 and this module does the
12
+ * extraction server-side, through the SAME `read_extract` path the agent uses for a
13
+ * file in the project folder. So an attachment and a workspace file can never disagree
14
+ * about whether a document is readable — and a refusal is carried into the turn as a
15
+ * refusal, never as mojibake.
16
+ *
17
+ * Security posture: bytes are written only into a fresh throwaway directory, with a
18
+ * sanitised basename, under a hard size cap, and the directory is removed afterwards.
19
+ * Nothing is executed, and nothing outside that directory is read or written.
20
+ */
21
+ /** Cap on attachments per turn (the composer allows 10). */
22
+ export declare const MAX_ATTACHMENTS = 10;
23
+ /** Cap on inline text content, in characters. */
24
+ export declare const MAX_TEXT_CHARS = 300000;
25
+ /** Cap on decoded binary content, in bytes. */
26
+ export declare const MAX_BINARY_BYTES = 300000;
27
+ export interface RawAttachment {
28
+ name?: unknown;
29
+ content?: unknown;
30
+ /** 'base64' for bytes the client could not decode as text; absent means inline text. */
31
+ encoding?: unknown;
32
+ kind?: unknown;
33
+ }
34
+ /** The composer's chip kinds — mirrors `ChatAttachment.kind` in chat-console.ts. */
35
+ export type AttachmentKind = 'file' | 'paste' | 'drop';
36
+ export interface HydratedAttachment {
37
+ name: string;
38
+ /** What the model sees: the extracted text, or a plain statement that it failed. */
39
+ content: string;
40
+ /** ChatAttachment's union — so a hydrated list passes straight to the engine. */
41
+ kind: AttachmentKind;
42
+ /** True when the content came from parsing binary bytes. */
43
+ extracted?: boolean;
44
+ /** The extractor's typed refusal code, when extraction failed. */
45
+ refusalCode?: string;
46
+ }
47
+ /**
48
+ * Sanitise an attachment name into a safe basename that keeps its extension —
49
+ * `read_extract` dispatches on the extension, so dropping it would make a PDF
50
+ * unreadable. Path separators and anything exotic are removed.
51
+ */
52
+ export declare function safeAttachmentName(name: string): string;
53
+ /**
54
+ * Hydrate raw composer attachments into model-ready text.
55
+ *
56
+ * Text attachments pass through (capped). Base64 attachments are written to a
57
+ * throwaway dir, run through `read_extract`, and replaced with either the extracted
58
+ * text or an explicit refusal.
59
+ */
60
+ export declare function hydrateAttachments(raw: unknown, opts?: {
61
+ max?: number;
62
+ tmpRoot?: string;
63
+ }): Promise<HydratedAttachment[]>;
64
+ //# sourceMappingURL=attachment-extract.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"attachment-extract.d.ts","sourceRoot":"","sources":["../../src/web-dashboard/attachment-extract.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAQH,4DAA4D;AAC5D,eAAO,MAAM,eAAe,KAAK,CAAC;AAClC,iDAAiD;AACjD,eAAO,MAAM,cAAc,SAAU,CAAC;AACtC,+CAA+C;AAC/C,eAAO,MAAM,gBAAgB,SAAU,CAAC;AAExC,MAAM,WAAW,aAAa;IAC5B,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,wFAAwF;IACxF,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAED,oFAAoF;AACpF,MAAM,MAAM,cAAc,GAAG,MAAM,GAAG,OAAO,GAAG,MAAM,CAAC;AAEvD,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,oFAAoF;IACpF,OAAO,EAAE,MAAM,CAAC;IAChB,iFAAiF;IACjF,IAAI,EAAE,cAAc,CAAC;IACrB,4DAA4D;IAC5D,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,kEAAkE;IAClE,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAOD;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAIvD;AAED;;;;;;GAMG;AACH,wBAAsB,kBAAkB,CACtC,GAAG,EAAE,OAAO,EACZ,IAAI,GAAE;IAAE,GAAG,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAO,GAC5C,OAAO,CAAC,kBAAkB,EAAE,CAAC,CAqC/B"}
@@ -0,0 +1,154 @@
1
+ /**
2
+ * P2 — attachment hydration: turn a composer attachment into text the model can use.
3
+ *
4
+ * The composer used to read EVERY attached file with `File.text()` and ship the result
5
+ * inline. For a PDF that is the file's bytes decoded as UTF-8, which is how the
6
+ * 2026-09-27 incident happened: `[Attachment: report.pdf]` arrived carrying
7
+ * `%PDF-1.4 … /FlateDecode …`, the model could not assess anything, and nothing told
8
+ * anyone extraction had failed. `chat-console.ts` even documented the intended
9
+ * behaviour ("binary files are rejected client-side") while no code implemented it.
10
+ *
11
+ * Now the client encodes what it cannot read as base64 and this module does the
12
+ * extraction server-side, through the SAME `read_extract` path the agent uses for a
13
+ * file in the project folder. So an attachment and a workspace file can never disagree
14
+ * about whether a document is readable — and a refusal is carried into the turn as a
15
+ * refusal, never as mojibake.
16
+ *
17
+ * Security posture: bytes are written only into a fresh throwaway directory, with a
18
+ * sanitised basename, under a hard size cap, and the directory is removed afterwards.
19
+ * Nothing is executed, and nothing outside that directory is read or written.
20
+ */
21
+ import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
22
+ import { tmpdir } from 'node:os';
23
+ import { basename, join } from 'node:path';
24
+ import { getReadExtractManager } from '../tools/read-extract.js';
25
+ /** Cap on attachments per turn (the composer allows 10). */
26
+ export const MAX_ATTACHMENTS = 10;
27
+ /** Cap on inline text content, in characters. */
28
+ export const MAX_TEXT_CHARS = 300_000;
29
+ /** Cap on decoded binary content, in bytes. */
30
+ export const MAX_BINARY_BYTES = 300_000;
31
+ /** Coerce a wire value to a known chip kind; anything else is an uploaded file. */
32
+ function normalizeKind(kind) {
33
+ return kind === 'paste' || kind === 'drop' ? kind : 'file';
34
+ }
35
+ /**
36
+ * Sanitise an attachment name into a safe basename that keeps its extension —
37
+ * `read_extract` dispatches on the extension, so dropping it would make a PDF
38
+ * unreadable. Path separators and anything exotic are removed.
39
+ */
40
+ export function safeAttachmentName(name) {
41
+ const base = basename(name.trim().replace(/\\/g, '/'));
42
+ const cleaned = base.replace(/[^A-Za-z0-9._-]+/g, '_').replace(/^[._]+/, '');
43
+ return cleaned.slice(0, 100) || 'attachment';
44
+ }
45
+ /**
46
+ * Hydrate raw composer attachments into model-ready text.
47
+ *
48
+ * Text attachments pass through (capped). Base64 attachments are written to a
49
+ * throwaway dir, run through `read_extract`, and replaced with either the extracted
50
+ * text or an explicit refusal.
51
+ */
52
+ export async function hydrateAttachments(raw, opts = {}) {
53
+ if (!Array.isArray(raw))
54
+ return [];
55
+ const max = opts.max ?? MAX_ATTACHMENTS;
56
+ const candidates = raw
57
+ .filter((a) => !!a && typeof a === 'object')
58
+ .filter((a) => typeof a.name === 'string' && a.name.trim().length > 0)
59
+ .filter((a) => typeof a.content === 'string')
60
+ .slice(0, max);
61
+ if (candidates.length === 0)
62
+ return [];
63
+ const binary = candidates.filter((a) => a.encoding === 'base64');
64
+ const text = candidates.filter((a) => a.encoding !== 'base64');
65
+ const out = [];
66
+ for (const a of text) {
67
+ out.push({
68
+ name: sanitize(a.name),
69
+ content: a.content.slice(0, MAX_TEXT_CHARS),
70
+ kind: normalizeKind(a.kind),
71
+ });
72
+ }
73
+ if (binary.length === 0)
74
+ return out;
75
+ const dir = mkdtempSync(join(opts.tmpRoot ?? tmpdir(), 'nuvira-attach-'));
76
+ try {
77
+ for (const a of binary) {
78
+ out.push(await hydrateOneBinary(a, dir));
79
+ }
80
+ }
81
+ finally {
82
+ rmSync(dir, { recursive: true, force: true });
83
+ }
84
+ return out;
85
+ }
86
+ // ─── Internals ──────────────────────────────────────────────────────────────
87
+ async function hydrateOneBinary(a, dir) {
88
+ const name = sanitize(a.name);
89
+ const kind = normalizeKind(a.kind);
90
+ const b64 = a.content.trim();
91
+ let bytes;
92
+ try {
93
+ bytes = Buffer.from(b64, 'base64');
94
+ }
95
+ catch {
96
+ return refusal(name, kind, 'The attachment was not valid base64, so its bytes could not be read.');
97
+ }
98
+ if (bytes.length === 0) {
99
+ return refusal(name, kind, 'The attachment decoded to zero bytes, so there was nothing to read.');
100
+ }
101
+ if (bytes.length > MAX_BINARY_BYTES) {
102
+ return refusal(name, kind, `The attachment is larger than the ${Math.round(MAX_BINARY_BYTES / 1024)} KB limit (${Math.round(bytes.length / 1024)} KB), so it was not read.`);
103
+ }
104
+ const target = join(dir, name);
105
+ writeFileSync(target, bytes);
106
+ const result = await getReadExtractManager().extract(target);
107
+ if (!result.success) {
108
+ const alternatives = (result.alternatives ?? []).map((x) => `- ${x}`).join('\n');
109
+ return {
110
+ name,
111
+ kind,
112
+ extracted: false,
113
+ refusalCode: result.code ?? 'unavailable',
114
+ content: `[The attached file "${name}" could NOT be read — do not describe or summarise its contents.]\n`
115
+ + `Reason: ${result.error ?? 'extraction failed'}\n`
116
+ + (alternatives ? `What can be done instead:\n${alternatives}\n` : ''),
117
+ };
118
+ }
119
+ // Extraction succeeded. Carry the provenance the model needs: how it was read, and
120
+ // whether the text is a transcription (OCR) rather than a parsed layer.
121
+ const notes = [];
122
+ if (result.metadata?.ocr) {
123
+ notes.push('recovered by OCR from page images — verify digits against the original');
124
+ }
125
+ if (result.metadata?.truncated) {
126
+ notes.push('truncated to the extraction character budget');
127
+ }
128
+ for (const w of result.metadata?.warnings ?? [])
129
+ notes.push(w);
130
+ const header = `[Extracted from the attached file "${name}" (${result.format}`
131
+ + (result.metadata?.pages ? `, ${result.metadata.pages} page(s)` : '')
132
+ + (result.metadata?.slides ? `, ${result.metadata.slides} slide(s)` : '')
133
+ + (result.metadata?.sheets ? `, sheet(s): ${result.metadata.sheets.join(', ')}` : '')
134
+ + `)${notes.length > 0 ? ` — note: ${notes.join('; ')}` : ''}]\n`;
135
+ return {
136
+ name,
137
+ kind,
138
+ extracted: true,
139
+ content: header + result.text,
140
+ };
141
+ }
142
+ function sanitize(name) {
143
+ return safeAttachmentName(name);
144
+ }
145
+ function refusal(name, kind, reason) {
146
+ return {
147
+ name,
148
+ kind,
149
+ extracted: false,
150
+ refusalCode: 'unsupported_format',
151
+ content: `[The attached file "${name}" could NOT be read — do not describe or summarise its contents.]\nReason: ${reason}\n`,
152
+ };
153
+ }
154
+ //# sourceMappingURL=attachment-extract.js.map