talos-code 0.0.1 → 0.2.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 (407) hide show
  1. package/CHANGELOG.md +87 -0
  2. package/LICENSE +661 -0
  3. package/README.md +116 -3
  4. package/THIRD_PARTY_NOTICES.md +40 -0
  5. package/dist/archive/zip.js +73 -0
  6. package/dist/args.js +100 -0
  7. package/dist/automations/history-store.js +34 -0
  8. package/dist/automations/policy-store.js +43 -0
  9. package/dist/automations/runner.js +107 -0
  10. package/dist/automations/schedule.js +85 -0
  11. package/dist/automations/windows-task.js +53 -0
  12. package/dist/commands/advanced-cli.js +233 -0
  13. package/dist/commands/checkpoint-cli.js +45 -0
  14. package/dist/commands/config-cli.js +107 -0
  15. package/dist/commands/context.js +17 -0
  16. package/dist/commands/extensions-cli.js +207 -0
  17. package/dist/commands/project-cli.js +74 -0
  18. package/dist/commands/project-commands.js +143 -0
  19. package/dist/commands/provider-cli.js +244 -0
  20. package/dist/commands/services-cli.js +248 -0
  21. package/dist/commands/session-cli.js +171 -0
  22. package/dist/commands/system-cli.js +405 -0
  23. package/dist/config/commands.js +60 -0
  24. package/dist/config/load.js +382 -0
  25. package/dist/config/migrations.js +48 -0
  26. package/dist/config/types.js +11 -0
  27. package/dist/diagnostics/development-log.js +154 -0
  28. package/dist/diagnostics/doctor.js +104 -0
  29. package/dist/diagnostics/redact.js +80 -0
  30. package/dist/diagnostics/zip.js +52 -0
  31. package/dist/errors.js +156 -0
  32. package/dist/events/bridge.js +130 -0
  33. package/dist/extensions/installer.js +178 -0
  34. package/dist/extensions/package-schema.js +24 -0
  35. package/dist/headless/result.js +84 -0
  36. package/dist/headless/run.js +230 -0
  37. package/dist/i18n/en/approval.js +46 -0
  38. package/dist/i18n/en/common.js +9 -0
  39. package/dist/i18n/en/credentials.js +92 -0
  40. package/dist/i18n/en/errors.js +267 -0
  41. package/dist/i18n/en/firstrun.js +101 -0
  42. package/dist/i18n/en/providers.js +37 -0
  43. package/dist/i18n/en/screen.js +774 -0
  44. package/dist/i18n/en/tools.js +127 -0
  45. package/dist/i18n/en.js +14 -0
  46. package/dist/i18n/error-view.js +307 -0
  47. package/dist/i18n/index.js +19 -0
  48. package/dist/i18n/kernel-map.js +139 -0
  49. package/dist/io.js +45 -0
  50. package/dist/main.js +331 -0
  51. package/dist/paths.js +41 -0
  52. package/dist/protocol/v2/codec.js +9 -0
  53. package/dist/protocol/v2/events.js +555 -0
  54. package/dist/protocol/v2/index.js +4 -0
  55. package/dist/protocol/v2/replay.js +28 -0
  56. package/dist/protocol/v2/types.js +1 -0
  57. package/dist/provider/control-plane.js +135 -0
  58. package/dist/provider/environment-keys.js +275 -0
  59. package/dist/provider/health.js +110 -0
  60. package/dist/provider/missing-key.js +48 -0
  61. package/dist/provider/model-catalog.js +168 -0
  62. package/dist/provider/openrouter-login.js +172 -0
  63. package/dist/provider/provider-text.js +13 -0
  64. package/dist/provider/store.js +95 -0
  65. package/dist/provider/system-keyring.js +214 -0
  66. package/dist/runtime/active-run.js +63 -0
  67. package/dist/runtime/agent-tree.js +74 -0
  68. package/dist/runtime/attachments.js +84 -0
  69. package/dist/runtime/brokered-executor.js +395 -0
  70. package/dist/runtime/context-archive.js +69 -0
  71. package/dist/runtime/context-status.js +76 -0
  72. package/dist/runtime/create-runtime.js +111 -0
  73. package/dist/runtime/model-profile.js +361 -0
  74. package/dist/runtime/output-store.js +137 -0
  75. package/dist/runtime/provider-attempts.js +185 -0
  76. package/dist/runtime/replay-buffer.js +153 -0
  77. package/dist/runtime/repo.js +95 -0
  78. package/dist/runtime/session-facade.js +135 -0
  79. package/dist/runtime/session-summary.js +125 -0
  80. package/dist/runtime/supervisor.js +265 -0
  81. package/dist/runtime/talos-composition.js +1011 -0
  82. package/dist/runtime/types.js +5 -0
  83. package/dist/runtime/usage-snapshot.js +82 -0
  84. package/dist/security/auto-classifier.js +38 -0
  85. package/dist/security/credential-free-environment.js +54 -0
  86. package/dist/security/evaluate.js +243 -0
  87. package/dist/security/execution-backends.js +236 -0
  88. package/dist/security/execution-broker.js +102 -0
  89. package/dist/security/forge-scan.js +74 -0
  90. package/dist/security/from-approval.js +39 -0
  91. package/dist/security/mxc-execution-backend.js +281 -0
  92. package/dist/security/permission-engine.js +138 -0
  93. package/dist/security/permission-explanation.js +54 -0
  94. package/dist/security/persist.js +179 -0
  95. package/dist/security/plugin-guard.js +230 -0
  96. package/dist/security/process-tree-evidence-store.js +74 -0
  97. package/dist/security/process-tree-probe.js +554 -0
  98. package/dist/security/project-resource-inventory.js +186 -0
  99. package/dist/security/project-trust-gate.js +38 -0
  100. package/dist/security/project-trust.js +367 -0
  101. package/dist/security/rule-parser.js +201 -0
  102. package/dist/security/shell-segmentation.js +68 -0
  103. package/dist/security/trust-authority.js +324 -0
  104. package/dist/security/types.js +1 -0
  105. package/dist/security/workspace-identity.js +102 -0
  106. package/dist/services/automation-facade.js +59 -0
  107. package/dist/services/forge-facade.js +77 -0
  108. package/dist/services/hook-facade.js +240 -0
  109. package/dist/services/index.js +16 -0
  110. package/dist/services/library-facade.js +174 -0
  111. package/dist/services/mcp-facade.js +348 -0
  112. package/dist/services/memory-facade.js +126 -0
  113. package/dist/services/notes-facade.js +157 -0
  114. package/dist/services/plugin-facade.js +308 -0
  115. package/dist/services/research-facade.js +110 -0
  116. package/dist/services/task-board-facade.js +236 -0
  117. package/dist/services/workflow-catalog.js +246 -0
  118. package/dist/sessions/export-format.js +98 -0
  119. package/dist/sessions/metadata-store.js +160 -0
  120. package/dist/sessions/share.js +122 -0
  121. package/dist/sessions/transfer.js +16 -0
  122. package/dist/subcommands.js +62 -0
  123. package/dist/tui/agent-roster.js +36 -0
  124. package/dist/tui/agent-view.js +51 -0
  125. package/dist/tui/app.js +4098 -0
  126. package/dist/tui/approval.js +36 -0
  127. package/dist/tui/boot/boot-sequence.js +76 -0
  128. package/dist/tui/boot/cinematic.js +243 -0
  129. package/dist/tui/boot/desktop-logo.js +82 -0
  130. package/dist/tui/boot.js +3 -0
  131. package/dist/tui/busy-input.js +58 -0
  132. package/dist/tui/catalog-service.js +560 -0
  133. package/dist/tui/components/assistant-stream.js +1 -0
  134. package/dist/tui/components/command-menu.js +68 -0
  135. package/dist/tui/components/composer.js +219 -0
  136. package/dist/tui/components/diff.js +35 -0
  137. package/dist/tui/components/footer.js +48 -0
  138. package/dist/tui/components/header.js +6 -0
  139. package/dist/tui/components/markdown.js +305 -0
  140. package/dist/tui/components/status-indicator.js +38 -0
  141. package/dist/tui/components/terminal-shell.js +214 -0
  142. package/dist/tui/components/thinking-row.js +19 -0
  143. package/dist/tui/components/tool-row.js +97 -0
  144. package/dist/tui/components/transcript-virtualizer.js +165 -0
  145. package/dist/tui/components/transcript.js +43 -0
  146. package/dist/tui/descendant-approvals.js +104 -0
  147. package/dist/tui/diff-model.js +77 -0
  148. package/dist/tui/editor-history.js +21 -0
  149. package/dist/tui/editor.js +210 -0
  150. package/dist/tui/event-adapter.js +436 -0
  151. package/dist/tui/exit-output.js +58 -0
  152. package/dist/tui/external-editor.js +53 -0
  153. package/dist/tui/file-completion.js +89 -0
  154. package/dist/tui/focus-manager.js +5 -0
  155. package/dist/tui/highlight.js +30 -0
  156. package/dist/tui/input-router.js +18 -0
  157. package/dist/tui/interrupt.js +99 -0
  158. package/dist/tui/keybindings.js +194 -0
  159. package/dist/tui/keymap-resolver.js +91 -0
  160. package/dist/tui/launch-state.js +148 -0
  161. package/dist/tui/line-diff.js +57 -0
  162. package/dist/tui/live-activity.js +56 -0
  163. package/dist/tui/metrics.js +123 -0
  164. package/dist/tui/onboarding.js +36 -0
  165. package/dist/tui/overlays/agent-tree.js +43 -0
  166. package/dist/tui/overlays/approval-dialog.js +642 -0
  167. package/dist/tui/overlays/automation-center.js +51 -0
  168. package/dist/tui/overlays/checkpoint-picker.js +47 -0
  169. package/dist/tui/overlays/context-inspector.js +36 -0
  170. package/dist/tui/overlays/effort-line.js +110 -0
  171. package/dist/tui/overlays/forge-center.js +46 -0
  172. package/dist/tui/overlays/help-dialog.js +16 -0
  173. package/dist/tui/overlays/history-picker.js +37 -0
  174. package/dist/tui/overlays/hook-center.js +70 -0
  175. package/dist/tui/overlays/library-center.js +55 -0
  176. package/dist/tui/overlays/mcp-center.js +60 -0
  177. package/dist/tui/overlays/memory-center.js +63 -0
  178. package/dist/tui/overlays/model-picker.js +77 -0
  179. package/dist/tui/overlays/notes-center.js +53 -0
  180. package/dist/tui/overlays/overlay-host.js +8 -0
  181. package/dist/tui/overlays/plugin-center.js +76 -0
  182. package/dist/tui/overlays/provider-picker.js +171 -0
  183. package/dist/tui/overlays/queue-editor.js +32 -0
  184. package/dist/tui/overlays/research-center.js +59 -0
  185. package/dist/tui/overlays/scroll-window.js +234 -0
  186. package/dist/tui/overlays/session-picker.js +115 -0
  187. package/dist/tui/overlays/tasks-center.js +83 -0
  188. package/dist/tui/overlays/theme-picker.js +21 -0
  189. package/dist/tui/overlays/transcript-search.js +58 -0
  190. package/dist/tui/overlays/trust-center.js +29 -0
  191. package/dist/tui/overlays/workflow-center.js +46 -0
  192. package/dist/tui/project-file-index.js +214 -0
  193. package/dist/tui/project-references.js +85 -0
  194. package/dist/tui/project-trust-prompt.js +289 -0
  195. package/dist/tui/prompt-history-store.js +116 -0
  196. package/dist/tui/queue-store.js +110 -0
  197. package/dist/tui/regions/budget.js +59 -0
  198. package/dist/tui/regions/views.js +96 -0
  199. package/dist/tui/render-coordinator.js +148 -0
  200. package/dist/tui/render-scheduler.js +4 -0
  201. package/dist/tui/run.js +69 -0
  202. package/dist/tui/selection-list.js +29 -0
  203. package/dist/tui/session-controller.js +791 -0
  204. package/dist/tui/session-export.js +54 -0
  205. package/dist/tui/setup-wizard.js +46 -0
  206. package/dist/tui/shell-input.js +35 -0
  207. package/dist/tui/shell-layout.js +48 -0
  208. package/dist/tui/shell-model.js +98 -0
  209. package/dist/tui/slash-commands.js +82 -0
  210. package/dist/tui/state.js +151 -0
  211. package/dist/tui/status-bar.js +125 -0
  212. package/dist/tui/status-view.js +40 -0
  213. package/dist/tui/terminal-capabilities.js +9 -0
  214. package/dist/tui/terminal-session.js +11 -0
  215. package/dist/tui/text-width.js +66 -0
  216. package/dist/tui/theme-catalog.js +27 -0
  217. package/dist/tui/theme-store.js +23 -0
  218. package/dist/tui/theme.js +23 -0
  219. package/dist/tui/tool-display.js +135 -0
  220. package/dist/tui/tool-facts.js +1 -0
  221. package/dist/tui/tools/bash-renderer.js +32 -0
  222. package/dist/tui/tools/change.js +61 -0
  223. package/dist/tui/tools/edit-renderer.js +17 -0
  224. package/dist/tui/tools/generic-renderer.js +13 -0
  225. package/dist/tui/tools/list-renderer.js +14 -0
  226. package/dist/tui/tools/read-renderer.js +13 -0
  227. package/dist/tui/tools/registry.js +20 -0
  228. package/dist/tui/tools/row-format.js +187 -0
  229. package/dist/tui/tools/search-renderer.js +38 -0
  230. package/dist/tui/tools/shared.js +97 -0
  231. package/dist/tui/tools/write-renderer.js +15 -0
  232. package/dist/tui/transcript-model.js +441 -0
  233. package/dist/tui/ui-preferences.js +79 -0
  234. package/dist/tui/usage-view.js +77 -0
  235. package/dist/tui/vim-mode.js +99 -0
  236. package/dist/update/check.js +15 -0
  237. package/dist/update/npm.js +51 -0
  238. package/dist/update/run.js +129 -0
  239. package/dist/version.js +2 -0
  240. package/dist/workspace/checkpoint-store.js +210 -0
  241. package/dist/workspace/checkpoint.js +413 -0
  242. package/dist/workspace/restore.js +232 -0
  243. package/package.json +64 -5
  244. package/vendor/context-engine/package.json +11 -0
  245. package/vendor/context-engine/src/compaction-planner.mjs +57 -0
  246. package/vendor/context-engine/src/contracts.mjs +48 -0
  247. package/vendor/context-engine/src/engine.mjs +445 -0
  248. package/vendor/context-engine/src/node/context-export.mjs +164 -0
  249. package/vendor/context-engine/src/node/legacy-import.mjs +92 -0
  250. package/vendor/context-engine/src/node/migrations/001-context.sql +126 -0
  251. package/vendor/context-engine/src/node/sqlite-store.mjs +85 -0
  252. package/vendor/context-engine/src/node/sqlite-worker.mjs +559 -0
  253. package/vendor/context-engine/src/profiles.mjs +10 -0
  254. package/vendor/context-engine/src/retrieval.mjs +104 -0
  255. package/vendor/context-engine/src/summary.mjs +97 -0
  256. package/vendor/context-engine/src/usage.mjs +34 -0
  257. package/vendor/harness-ui/package.json +38 -0
  258. package/vendor/harness-ui/src/acp-agent.mjs +350 -0
  259. package/vendor/harness-ui/src/agent-service.mjs +2188 -0
  260. package/vendor/harness-ui/src/agui-events.mjs +452 -0
  261. package/vendor/harness-ui/src/ambiente-solo-server.mjs +121 -0
  262. package/vendor/harness-ui/src/artifact-store.mjs +39 -0
  263. package/vendor/harness-ui/src/assistenza.mjs +123 -0
  264. package/vendor/harness-ui/src/automation-scheduler.mjs +69 -0
  265. package/vendor/harness-ui/src/automation-store.mjs +145 -0
  266. package/vendor/harness-ui/src/browser-annota.mjs +639 -0
  267. package/vendor/harness-ui/src/browser-frame.mjs +211 -0
  268. package/vendor/harness-ui/src/browser-proxy-universale.mjs +519 -0
  269. package/vendor/harness-ui/src/browser-proxy.mjs +87 -0
  270. package/vendor/harness-ui/src/browser-sessione-viva.mjs +329 -0
  271. package/vendor/harness-ui/src/browser-stream.mjs +445 -0
  272. package/vendor/harness-ui/src/browser-vivo.mjs +694 -0
  273. package/vendor/harness-ui/src/chat-image-attachments.mjs +72 -0
  274. package/vendor/harness-ui/src/config.mjs +697 -0
  275. package/vendor/harness-ui/src/contesto-del-progetto.mjs +385 -0
  276. package/vendor/harness-ui/src/context-asset-adapter.mjs +72 -0
  277. package/vendor/harness-ui/src/context-desktop-service.mjs +253 -0
  278. package/vendor/harness-ui/src/context-embedding-runtime.mjs +252 -0
  279. package/vendor/harness-ui/src/context-inference-scheduler.mjs +81 -0
  280. package/vendor/harness-ui/src/context-native-compaction.mjs +75 -0
  281. package/vendor/harness-ui/src/context-provider-adapter.mjs +141 -0
  282. package/vendor/harness-ui/src/context-runtime.mjs +118 -0
  283. package/vendor/harness-ui/src/context-token-counters.mjs +184 -0
  284. package/vendor/harness-ui/src/context-tool-catalog.mjs +86 -0
  285. package/vendor/harness-ui/src/context-tool-output.mjs +72 -0
  286. package/vendor/harness-ui/src/costo-elenco.mjs +252 -0
  287. package/vendor/harness-ui/src/custom-task.mjs +171 -0
  288. package/vendor/harness-ui/src/doctor.mjs +142 -0
  289. package/vendor/harness-ui/src/document-filename.mjs +97 -0
  290. package/vendor/harness-ui/src/document-generator.mjs +493 -0
  291. package/vendor/harness-ui/src/document-report.mjs +331 -0
  292. package/vendor/harness-ui/src/duckduckgo-search.mjs +155 -0
  293. package/vendor/harness-ui/src/elenco-profondo.mjs +337 -0
  294. package/vendor/harness-ui/src/favicon-proxy.mjs +113 -0
  295. package/vendor/harness-ui/src/forge-contract.mjs +221 -0
  296. package/vendor/harness-ui/src/frequent-dirs.mjs +134 -0
  297. package/vendor/harness-ui/src/generated-image-store.mjs +147 -0
  298. package/vendor/harness-ui/src/generation-idle.mjs +332 -0
  299. package/vendor/harness-ui/src/gguf-header.mjs +207 -0
  300. package/vendor/harness-ui/src/git-service.mjs +626 -0
  301. package/vendor/harness-ui/src/gitignore-elenco.mjs +604 -0
  302. package/vendor/harness-ui/src/harness-receipt-keypair.mjs +207 -0
  303. package/vendor/harness-ui/src/hf-direct-transfer.mjs +170 -0
  304. package/vendor/harness-ui/src/hf-hub-client.mjs +106 -0
  305. package/vendor/harness-ui/src/hf-image-proxy.mjs +105 -0
  306. package/vendor/harness-ui/src/hf-model-transfer.mjs +245 -0
  307. package/vendor/harness-ui/src/hook-registry.mjs +186 -0
  308. package/vendor/harness-ui/src/http-app.mjs +6259 -0
  309. package/vendor/harness-ui/src/http-lifecycle.mjs +132 -0
  310. package/vendor/harness-ui/src/id-archivio.mjs +27 -0
  311. package/vendor/harness-ui/src/image-generator.mjs +143 -0
  312. package/vendor/harness-ui/src/istruzioni-di-progetto.mjs +234 -0
  313. package/vendor/harness-ui/src/kernel/dist/kernelPerIlBanco.js +518 -0
  314. package/vendor/harness-ui/src/kernel/talosHarness.mjs +10437 -0
  315. package/vendor/harness-ui/src/library-policy-store.mjs +175 -0
  316. package/vendor/harness-ui/src/library-store.mjs +652 -0
  317. package/vendor/harness-ui/src/llama-server-supervisor.mjs +629 -0
  318. package/vendor/harness-ui/src/local-model-store.mjs +339 -0
  319. package/vendor/harness-ui/src/local-runtime-contract.mjs +66 -0
  320. package/vendor/harness-ui/src/local-runtime-events.mjs +44 -0
  321. package/vendor/harness-ui/src/local-runtime-llama-server.mjs +244 -0
  322. package/vendor/harness-ui/src/local-runtime-probe.mjs +401 -0
  323. package/vendor/harness-ui/src/machine-capacity.mjs +66 -0
  324. package/vendor/harness-ui/src/mappa-cartelle.mjs +491 -0
  325. package/vendor/harness-ui/src/mcp-client.mjs +98 -0
  326. package/vendor/harness-ui/src/mcp-registry.mjs +157 -0
  327. package/vendor/harness-ui/src/mcp-session.mjs +177 -0
  328. package/vendor/harness-ui/src/memory-store.mjs +227 -0
  329. package/vendor/harness-ui/src/model-catalog-models-dev.mjs +276 -0
  330. package/vendor/harness-ui/src/model-catalog.mjs +129 -0
  331. package/vendor/harness-ui/src/model-destination.mjs +189 -0
  332. package/vendor/harness-ui/src/modifica-ancorata.mjs +177 -0
  333. package/vendor/harness-ui/src/native-provider-adapter.mjs +205 -0
  334. package/vendor/harness-ui/src/notes-store.mjs +250 -0
  335. package/vendor/harness-ui/src/openai-compatible-runtime.mjs +428 -0
  336. package/vendor/harness-ui/src/openrouter-oauth.mjs +339 -0
  337. package/vendor/harness-ui/src/path-policy.mjs +442 -0
  338. package/vendor/harness-ui/src/plugin-registry.mjs +780 -0
  339. package/vendor/harness-ui/src/plugin-session.mjs +180 -0
  340. package/vendor/harness-ui/src/process-policy.mjs +345 -0
  341. package/vendor/harness-ui/src/prompt-enhancer-provider.mjs +94 -0
  342. package/vendor/harness-ui/src/provider-auth-cloud.mjs +95 -0
  343. package/vendor/harness-ui/src/provider-credential-store.mjs +541 -0
  344. package/vendor/harness-ui/src/provider-probe.mjs +582 -0
  345. package/vendor/harness-ui/src/provider-registry.mjs +1633 -0
  346. package/vendor/harness-ui/src/pty-terminal.mjs +312 -0
  347. package/vendor/harness-ui/src/public-problem.mjs +109 -0
  348. package/vendor/harness-ui/src/research/card.mjs +235 -0
  349. package/vendor/harness-ui/src/research/citations.mjs +142 -0
  350. package/vendor/harness-ui/src/research/collector.mjs +275 -0
  351. package/vendor/harness-ui/src/research/deposito-a-pezzi.mjs +139 -0
  352. package/vendor/harness-ui/src/research/dossier.mjs +114 -0
  353. package/vendor/harness-ui/src/research/esportazioni.mjs +560 -0
  354. package/vendor/harness-ui/src/research/fetch-cache.mjs +465 -0
  355. package/vendor/harness-ui/src/research/fidelity.mjs +122 -0
  356. package/vendor/harness-ui/src/research/independence.mjs +159 -0
  357. package/vendor/harness-ui/src/research/ledger.mjs +166 -0
  358. package/vendor/harness-ui/src/research/markdown-server.mjs +565 -0
  359. package/vendor/harness-ui/src/research/narration.mjs +181 -0
  360. package/vendor/harness-ui/src/research/open-cards.mjs +131 -0
  361. package/vendor/harness-ui/src/research/opposing.mjs +305 -0
  362. package/vendor/harness-ui/src/research/outline.mjs +111 -0
  363. package/vendor/harness-ui/src/research/page-budget.mjs +209 -0
  364. package/vendor/harness-ui/src/research/pdf.mjs +291 -0
  365. package/vendor/harness-ui/src/research/plan.mjs +301 -0
  366. package/vendor/harness-ui/src/research/raccolta-viva.mjs +452 -0
  367. package/vendor/harness-ui/src/research/recheck-document.mjs +69 -0
  368. package/vendor/harness-ui/src/research/recheck-history.mjs +192 -0
  369. package/vendor/harness-ui/src/research/recheck.mjs +194 -0
  370. package/vendor/harness-ui/src/research/report.mjs +203 -0
  371. package/vendor/harness-ui/src/research/run.mjs +527 -0
  372. package/vendor/harness-ui/src/research/synthesis.mjs +318 -0
  373. package/vendor/harness-ui/src/research/verification.mjs +572 -0
  374. package/vendor/harness-ui/src/research-orchestrator.mjs +2679 -0
  375. package/vendor/harness-ui/src/research-store.mjs +1133 -0
  376. package/vendor/harness-ui/src/runtime-build-manifest.mjs +26 -0
  377. package/vendor/harness-ui/src/runtime-contract.mjs +59 -0
  378. package/vendor/harness-ui/src/runtime-owner-adapter.mjs +1348 -0
  379. package/vendor/harness-ui/src/runtime-owner-contract.mjs +32 -0
  380. package/vendor/harness-ui/src/scheda-di-lavoro.mjs +249 -0
  381. package/vendor/harness-ui/src/search-source-store.mjs +172 -0
  382. package/vendor/harness-ui/src/session-registry.mjs +6095 -0
  383. package/vendor/harness-ui/src/session-store.mjs +220 -0
  384. package/vendor/harness-ui/src/sessione-pronta.mjs +73 -0
  385. package/vendor/harness-ui/src/setup-stato.mjs +31 -0
  386. package/vendor/harness-ui/src/sezioni-istruzioni.mjs +204 -0
  387. package/vendor/harness-ui/src/skill-registry.mjs +120 -0
  388. package/vendor/harness-ui/src/sse-replay-coalescente.mjs +0 -0
  389. package/vendor/harness-ui/src/static-files.mjs +96 -0
  390. package/vendor/harness-ui/src/stream-partition.mjs +123 -0
  391. package/vendor/harness-ui/src/subagent-orchestrator.mjs +453 -0
  392. package/vendor/harness-ui/src/task-catalog.mjs +65 -0
  393. package/vendor/harness-ui/src/tasks-store.mjs +220 -0
  394. package/vendor/harness-ui/src/terminal-registry.mjs +312 -0
  395. package/vendor/harness-ui/src/terminal-ws.mjs +170 -0
  396. package/vendor/harness-ui/src/tool-forge-store.mjs +299 -0
  397. package/vendor/harness-ui/src/tool-schema-normalize.mjs +100 -0
  398. package/vendor/harness-ui/src/usage-cache.mjs +315 -0
  399. package/vendor/harness-ui/src/workspace-browser.mjs +213 -0
  400. package/vendor/harness-ui/src/workspace-context.mjs +124 -0
  401. package/vendor/harness-ui/src/workspace-disk.mjs +62 -0
  402. package/vendor/harness-ui/src/workspace-files.mjs +589 -0
  403. package/vendor/harness-ui/src/workspace-info.mjs +189 -0
  404. package/vendor/harness-ui/src/workspace-launch-store.mjs +150 -0
  405. package/vendor/harness-ui/src/workspace-tree.mjs +67 -0
  406. package/vendor/harness-ui/src/workspace-watcher.mjs +161 -0
  407. package/vendor/manifest.json +170 -0
@@ -0,0 +1,72 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { dirname, isAbsolute } from 'node:path';
3
+
4
+ // Qualified explicit filters from RTK 0.44.2 (700bdde); never use autodetection.
5
+ const FILTERS = new Set(['cargo-test', 'pytest', 'go-test', 'go-build', 'tsc', 'vitest', 'grep', 'rg', 'find', 'fd', 'git-log', 'git-diff', 'git-status', 'log', 'mypy', 'ruff-check', 'ruff-format', 'prettier', 'phpunit', 'pest', 'paratest', 'php-test', 'ecs', 'phpstan', 'pint']);
6
+ const MAX_BYTES = 10 * 1024 * 1024;
7
+ const fail = (message, code) => { throw Object.assign(new Error(message), { code }); };
8
+ const checkSignal = signal => { if (signal?.aborted) fail('Tool output compression cancelled', 'CTX_CANCELLED'); };
9
+ const validId = value => typeof value === 'string' && value.trim() !== '' && value.length <= 256;
10
+
11
+ export function createToolOutputCompressor({ binaryPath, processPolicy, store, timeoutMs = 10000 } = {}) {
12
+ if (typeof binaryPath !== 'string' || !isAbsolute(binaryPath) || !processPolicy || typeof processPolicy.spawn !== 'function'
13
+ || !store || typeof store.readOriginals !== 'function' || !Number.isInteger(timeoutMs) || timeoutMs <= 0 || timeoutMs > 60000) fail('Compressor dependencies are invalid', 'CTX_OUTPUT_CONFIG');
14
+
15
+ async function filterStdin(text, filter, signal) {
16
+ return new Promise((resolve, reject) => {
17
+ let child; let timer; let done = false; let length = 0; let errorLength = 0; const output = [];
18
+ const stop = () => { try { child?.kill('SIGTERM'); } catch { /* process may already be gone */ } };
19
+ const finish = (value, error) => {
20
+ if (done) return;
21
+ done = true;
22
+ clearTimeout(timer);
23
+ signal?.removeEventListener('abort', onAbort);
24
+ if (error) reject(error); else resolve(value);
25
+ };
26
+ const fallback = () => { finish(null); stop(); };
27
+ const onAbort = () => { finish(null, Object.assign(new Error('Tool output compression cancelled'), { code: 'CTX_CANCELLED' })); stop(); };
28
+ try {
29
+ // Authority remains in the caller-owned policy. `command` is never an argument.
30
+ child = processPolicy.spawn(binaryPath, ['pipe', '--filter', filter], { cwd: dirname(binaryPath), shell: false, windowsHide: true, stdio: ['pipe', 'pipe', 'pipe'] });
31
+ if (!child?.stdin || typeof child.once !== 'function') return fallback();
32
+ child.on('error', fallback);
33
+ child.stdin.on('error', fallback);
34
+ child.stdout?.on('data', chunk => {
35
+ if (done) return;
36
+ const bytes = Buffer.from(chunk); length += bytes.length;
37
+ if (length > MAX_BYTES) return fallback();
38
+ output.push(bytes);
39
+ });
40
+ child.stderr?.on('data', chunk => { errorLength += Buffer.byteLength(chunk); if (errorLength > 65536) fallback(); });
41
+ child.once('close', code => {
42
+ if (code !== 0) return finish(null);
43
+ try { finish(new TextDecoder('utf-8', { fatal: true }).decode(Buffer.concat(output))); } catch { finish(null); }
44
+ });
45
+ timer = setTimeout(fallback, timeoutMs);
46
+ signal?.addEventListener('abort', onAbort, { once: true });
47
+ if (signal?.aborted) return onAbort();
48
+ child.stdin.end(Buffer.from(text, 'utf8'));
49
+ } catch { fallback(); }
50
+ });
51
+ }
52
+
53
+ async function compressCapturedToolOutput({ sessionId, recordId, text, filter, command, exitCode, signal } = {}) {
54
+ checkSignal(signal);
55
+ if (!validId(sessionId) || !validId(recordId) || typeof text !== 'string' || !text.isWellFormed()
56
+ || (command !== undefined && typeof command !== 'string') || (exitCode !== undefined && exitCode !== null && !Number.isInteger(exitCode))) fail('Captured output is invalid', 'CTX_OUTPUT_INVALID');
57
+ const rows = await store.readOriginals({ sessionId, ids: [recordId], limit: 1 });
58
+ const record = Array.isArray(rows) && rows.length === 1 ? rows[0] : null;
59
+ if (!record || record.id !== recordId || record.sessionId !== sessionId || record.message?.role !== 'tool'
60
+ || record.message.content !== text || typeof record.sha256 !== 'string'
61
+ || createHash('sha256').update(JSON.stringify(record.message)).digest('hex') !== record.sha256) fail('Exact tool output must be archived before compression', 'CTX_ORIGINAL_MISMATCH');
62
+ checkSignal(signal);
63
+ const result = { text, originalRef: { sessionId, recordId, sha256: record.sha256 }, compressed: false, originalChars: text.length, outputChars: text.length, command, exitCode };
64
+ if (!FILTERS.has(filter) || Buffer.byteLength(text, 'utf8') > MAX_BYTES || text.length === 0) return result;
65
+ const output = await filterStdin(text, filter, signal);
66
+ checkSignal(signal);
67
+ if (typeof output !== 'string' || output.trim() === '' || output.length >= text.length) return result;
68
+ return { ...result, text: output, compressed: true, filter, outputChars: output.length };
69
+ }
70
+
71
+ return Object.freeze({ compressCapturedToolOutput });
72
+ }
@@ -0,0 +1,252 @@
1
+ /*
2
+ * Quanto costa mettere un elenco di file in testa al prompt — DETTO CON IL METODO IN CHIARO.
3
+ *
4
+ * ⛔ Perché questo modulo esiste (09/09/2026, Context Manager): in `context-token-counters.mjs`
5
+ * i BYTE erano contati come token e la misura sbagliava di 3,9 volte — un corpo di 39.513 byte,
6
+ * che il fornitore ha misurato in 10.073 token, valeva 70.903. Effetto: overflow apparente a
7
+ * ogni giro. La cura non è una costante migliore: è che il numero dica SEMPRE se è stato
8
+ * contato o stimato, e come. Qui una stima non può travestirsi da misura: `metodo` è un campo
9
+ * del risultato, non un commento.
10
+ *
11
+ * ⛔ Un solo contatore in tutto il repo. Questo modulo NON tokenizza: riceve `contatore` come
12
+ * porta. L'adattatore `contatoreDaContextEngine()` in fondo prende il contatore che c'è già
13
+ * (`createContextTokenCounter` in `src/context-token-counters.mjs`) e ne fa una porta sincrona.
14
+ * Due contatori che danno due numeri diversi per la stessa cosa sono peggio di nessun contatore.
15
+ *
16
+ * RICERCA WEB (letta il 10/09/2026, prima di scrivere una riga):
17
+ * 1. «1 token ≈ 4 caratteri» è la REGOLA D'ORO di OpenAI per l'INGLESE, e sta entro ~10% solo lì;
18
+ * il rapporto cambia molto con lingua, dominio e tokenizer.
19
+ * — OpenAI Developer Community, "Rules of Thumb for number of source code characters to tokens"
20
+ * <https://community.openai.com/t/rules-of-thumb-for-number-of-source-code-characters-to-tokens/622947>
21
+ * 2. Il BPE a livello di byte parte da un alfabeto Σ={0..255} e fonde BYTE UTF-8, non caratteri
22
+ * Unicode: «penalizza le lingue con grafemi multibyte (tamil, hindi, cinese), fino a 3 volte
23
+ * più token dell'inglese» (parità tamil 4,54 con GPT-2). ⇒ per il testo non latino la stima si
24
+ * fa sui BYTE, mai sui caratteri.
25
+ * — EmergentMind, "Byte-level BPE Tokenizers" <https://www.emergentmind.com/topics/byte-level-bpe-tokenizers>
26
+ * 3. Misura citabile: 8 caratteri cinesi costano 11 token in cl100k_base, contro 5 token per 16
27
+ * caratteri inglesi. La regola dei 4 caratteri predirebbe 2 token per quegli 8 ideogrammi:
28
+ * sbaglia di 5,5 volte. (Vedi la prova COSTO-ELENCO-08.)
29
+ * — Token Optimization Guide, "Language Comparison"
30
+ * <https://olivomarco.github.io/github-copilot-token-optimization/03-language-comparison/>
31
+ * 4. Anche un conteggio del fornitore è dichiarato dal fornitore stesso come STIMA a meno di uno
32
+ * scarto piccolo, e un tokenizer nuovo cambia i numeri (Claude 4.7+: ~30% token in più sullo
33
+ * stesso testo). ⇒ `'contato'` qui vuol dire «un tokenizer ha guardato QUESTO testo», non
34
+ * «verità eterna»; la confidenza lo dice.
35
+ * — Claude Platform Docs, "Token counting" <https://platform.claude.com/docs/en/build-with-claude/token-counting>
36
+ *
37
+ * ⛔ Nessuna dipendenza nuova: in `node_modules` non c'è nessun tokenizer BPE (verificato il
38
+ * 10/09/2026, cercando tiktoken/cl100k/o200k/vocab). Un conteggio VERO offline oggi non è
39
+ * possibile in questo repo — e questo modulo lo dice invece di fingerlo.
40
+ */
41
+
42
+ const fallisci = (codice, messaggio) => { throw Object.assign(new Error(messaggio), { code: codice }); };
43
+
44
+ /**
45
+ * Taratura della stima. ⛔ È la STESSA di `src/context-token-counters.mjs:70` — di proposito: due
46
+ * stime diverse per lo stesso testo rifarebbero il difetto del 09/09 in forma nuova.
47
+ * Misurata quel giorno su z-ai/glm-5.3-flash (italiano + JSON di strumenti): 3,92 byte/token e
48
+ * 3,64 caratteri/token reali; dividere per 3,5 tiene la stima ~12% SOPRA il vero, cioè prudente
49
+ * nel verso giusto, con il margine dichiarato a parte invece che nascosto nel numero.
50
+ */
51
+ export const TARATURA_STIMA = Object.freeze({
52
+ bytePerToken: 3.5,
53
+ margine: 0.15,
54
+ margineMinimoToken: 8,
55
+ misurata: '09/09/2026 su z-ai/glm-5.3-flash (3,92 byte/token, 3,64 caratteri/token)',
56
+ });
57
+
58
+ /**
59
+ * Listino e comportamento della cache — MISURATI il 22/08/2026, non stimati qui.
60
+ * Tre chiamate ravvicinate sullo stesso prefisso da 16.811 token: la TERZA ne legge 16.768 dalla
61
+ * cache e costa $0,000172 contro $0,001011, cioè 5,9 volte meno. Il fattore di listino è 6,0
62
+ * esatto ($0,06/M contro $0,01/M); il 5,9 misurato è più basso perché 43 token dei 16.811 non
63
+ * erano in cache — i conti tornano: 16.768×$0,01/M + 43×$0,06/M = $0,00017026.
64
+ * ⛔ La cache PRENDE DALLA TERZA CHIAMATA: le prime due si pagano piene. Chi prova due volte sole
65
+ * conclude «non funziona», ed è l'unico esito sbagliato possibile.
66
+ * Z.AI cacheggia da sé, senza configurazione.
67
+ */
68
+ export const LISTINO_22_08 = Object.freeze({
69
+ promptDollariPerMilione: 0.06,
70
+ cacheReadDollariPerMilione: 0.01,
71
+ giriPagatiPieni: 2,
72
+ misurato: '22/08/2026 — 16.811 token, terza chiamata $0,000172 contro $0,001011',
73
+ });
74
+
75
+ const METODI = ['auto', 'contato', 'stimato'];
76
+ const arrotonda = (valore, cifre) => Number(valore.toFixed(cifre));
77
+ const dollari = (token, perMilione) => arrotonda(token * perMilione / 1_000_000, 10);
78
+ // ⛔ Non ASCII = il BPE a livello di byte spezza il carattere in 2-4 byte: la confidenza deve dirlo.
79
+ const haNonLatino = testo => [...testo].some(carattere => carattere.codePointAt(0) > 127);
80
+
81
+ /* ⛔ Un elenco di percorsi NON è prosa: `src/context-token-counters.mjs` è pieno di `/ - .`, e il BPE
82
+ spezza a ogni separatore («nomi, codice o caratteri non inglesi vengono tagliati in più pezzi» —
83
+ OpenAI Developer Community, letto 10/09/2026). Qui NON correggiamo il numero con un coefficiente
84
+ inventato: alziamo una BANDIERA, perché su testo così la stima è un pavimento e non un tetto.
85
+ La soglia 0,10 è una soglia di SEGNALAZIONE, non una taratura: la prosa italiana sta molto sotto,
86
+ un elenco di percorsi molto sopra. */
87
+ const SOGLIA_PUNTEGGIATURA_DENSA = 0.10;
88
+ const quotaPunteggiatura = testo => {
89
+ const caratteri = [...testo];
90
+ if (!caratteri.length) return 0;
91
+ return caratteri.filter(carattere => !/[\p{L}\p{N}\s]/u.test(carattere)).length / caratteri.length;
92
+ };
93
+
94
+ /**
95
+ * La regola d'oro «1 token ogni 4 caratteri». ⛔ NON è una misura, e non è usata da nessun
96
+ * risultato di questo modulo: esiste solo perché le prove possano mostrare DI QUANTO sbaglia.
97
+ * Vedi ricerca (1) e (3) in testa al file.
98
+ */
99
+ export function stimaRegolaQuattroCaratteri(testo) {
100
+ if (typeof testo !== 'string') fallisci('COSTO_TESTO_INVALIDO', 'Il testo da stimare deve essere una stringa.');
101
+ return Math.ceil([...testo].length / 4);
102
+ }
103
+
104
+ /** Quanto costa questo testo, con il metodo dichiarato. */
105
+ export function costoDelTesto(testo, { metodo = 'auto', contatore } = {}) {
106
+ if (typeof testo !== 'string') fallisci('COSTO_TESTO_INVALIDO', 'Il testo da misurare deve essere una stringa.');
107
+ if (!METODI.includes(metodo)) fallisci('COSTO_METODO_IGNOTO', `Metodo sconosciuto: ${metodo}. Ammessi: ${METODI.join(', ')}.`);
108
+ if (contatore !== undefined && typeof contatore !== 'function') fallisci('COSTO_CONTATORE_INVALIDO', 'Il contatore deve essere una funzione (testo) => numero di token.');
109
+ // ⛔ Chiedere `'contato'` senza avere un contatore non produce una stima battezzata misura: si ferma.
110
+ if (metodo === 'contato' && !contatore) fallisci('COSTO_CONTATORE_ASSENTE', 'Il metodo "contato" richiede un contatore vero: senza, il numero sarebbe una stima travestita da misura.');
111
+
112
+ const byte = Buffer.byteLength(testo, 'utf8');
113
+ const caratteri = [...testo].length;
114
+ const nonLatino = haNonLatino(testo);
115
+ const quotaSeparatori = quotaPunteggiatura(testo);
116
+ const punteggiaturaDensa = quotaSeparatori > SOGLIA_PUNTEGGIATURA_DENSA;
117
+ const base = { byte, caratteri, unitaDiCodice: testo.length, nonLatino, punteggiaturaDensa, quotaSeparatori: Number(quotaSeparatori.toFixed(4)) };
118
+
119
+ if (contatore && metodo !== 'stimato') {
120
+ const conteggio = contatore(testo);
121
+ if (typeof conteggio?.then === 'function') fallisci('COSTO_CONTATORE_ASINCRONO', 'Il contatore deve essere sincrono: usa contatoreDaContextEngine() per pre-misurare i testi e ottenere una porta sincrona.');
122
+ if (!Number.isSafeInteger(conteggio) || conteggio < 0) fallisci('COSTO_CONTEGGIO_INVALIDO', 'Il contatore non ha restituito un numero di token valido; nessun ripiego silenzioso su una stima.');
123
+ // ⛔ Un contatore che dichiara di aver stimato NON diventa «contato» passando di qui.
124
+ const suoMetodo = contatore.metodo === 'stimato' ? 'stimato' : 'contato';
125
+ const fonte = contatore.fonte ?? 'contatore iniettato (fonte non dichiarata)';
126
+ return {
127
+ ...base,
128
+ token: conteggio,
129
+ metodo: suoMetodo,
130
+ fonte,
131
+ margineToken: suoMetodo === 'contato' ? 0 : Math.max(TARATURA_STIMA.margineMinimoToken, Math.ceil(conteggio * TARATURA_STIMA.margine)),
132
+ confidenza: suoMetodo === 'contato'
133
+ ? `contato: ${fonte} ha tokenizzato questo testo. ⛔ Il fornitore dichiara la propria misura come stima a meno di uno scarto piccolo, e un tokenizer nuovo cambia i numeri (Claude 4.7+: ~30% token in più sullo stesso testo — Claude Platform Docs, token-counting, letto 10/09/2026).`
134
+ : `stimato dal contatore iniettato (${fonte}): ha dichiarato metodo "stimato", e passare di qui non lo trasforma in una misura.`,
135
+ };
136
+ }
137
+
138
+ /* Stima sui BYTE UTF-8, non sui caratteri: il BPE a livello di byte fonde byte, quindi un
139
+ carattere accentato (2 byte), un ideogramma (3) o un'emoji (4) costano di più di una lettera
140
+ ASCII, e la regola dei 4 caratteri li conta uguali. Vedi ricerca (2) e (3). */
141
+ const token = Math.ceil(byte / TARATURA_STIMA.bytePerToken);
142
+ const margineToken = token === 0 ? 0 : Math.max(TARATURA_STIMA.margineMinimoToken, Math.ceil(token * TARATURA_STIMA.margine));
143
+ const avvisoNonLatino = nonLatino
144
+ ? ' ⛔ Il testo contiene caratteri non ASCII: per gli script non latini il BPE a livello di byte arriva fino a 3 volte l\'inglese e questa stima è un PAVIMENTO, non un tetto (8 ideogrammi = 11 token misurati in cl100k_base, dove byte/3,5 ne predice 7 e caratteri/4 solo 2).'
145
+ : '';
146
+ const avvisoDenso = punteggiaturaDensa
147
+ ? ` ⛔ Testo fitto di separatori (${Math.round(quotaSeparatori * 100)}% di caratteri non alfanumerici: è la forma di un elenco di percorsi, non di prosa): il BPE spezza a ogni "/", "-" e ".", quindi anche qui la stima è un PAVIMENTO.`
148
+ : '';
149
+ return {
150
+ ...base,
151
+ token,
152
+ metodo: 'stimato',
153
+ fonte: 'nessun contatore iniettato',
154
+ margineToken,
155
+ confidenza: `stimato: byte UTF-8 (${byte}) ÷ ${TARATURA_STIMA.bytePerToken}, taratura ${TARATURA_STIMA.misurata}; margine dichiarato ±${Math.round(TARATURA_STIMA.margine * 100)}% (${margineToken} token). ⛔ Non è un conteggio: nessun tokenizer ha guardato questo testo.${avvisoNonLatino}${avvisoDenso}`,
156
+ };
157
+ }
158
+
159
+ /** Il costo di un elenco, e quanto ne resta nella finestra. */
160
+ export function costoElenco(testoElenco, { finestra, giri = 1, contatore, metodo = 'auto', listino = LISTINO_22_08 } = {}) {
161
+ if (!Number.isSafeInteger(giri) || giri < 1) fallisci('COSTO_GIRI_INVALIDI', 'I giri devono essere un intero maggiore o uguale a 1.');
162
+ const misura = costoDelTesto(testoElenco, { metodo, contatore });
163
+ const token = misura.token;
164
+
165
+ /* ⛔ Finestra assente, zero o non intera: nessuna percentuale inventata. Non «100%», non «0%»:
166
+ ASSENTE. Un 0% su finestra ignota è la bugia più comoda che ci sia. */
167
+ const finestraNota = Number.isSafeInteger(finestra) && finestra > 0;
168
+ const percentualeFinestra = finestraNota ? arrotonda(token / finestra * 100, 2) : null;
169
+
170
+ const costoSuNGiri = token * giri;
171
+ const giriPieni = Math.min(giri, listino.giriPagatiPieni);
172
+ const giriScontati = Math.max(0, giri - listino.giriPagatiPieni);
173
+ const tokenPieni = token * giriPieni;
174
+ const tokenInCache = token * giriScontati;
175
+ const dollariSenzaCache = dollari(costoSuNGiri, listino.promptDollariPerMilione);
176
+ const dollariConCache = arrotonda(dollari(tokenPieni, listino.promptDollariPerMilione) + dollari(tokenInCache, listino.cacheReadDollariPerMilione), 10);
177
+ const cacheAttiva = giriScontati > 0 && token > 0;
178
+
179
+ return {
180
+ token,
181
+ metodo: misura.metodo,
182
+ confidenza: misura.confidenza,
183
+ fonte: misura.fonte,
184
+ margineToken: misura.margineToken,
185
+ byte: misura.byte,
186
+ caratteri: misura.caratteri,
187
+ nonLatino: misura.nonLatino,
188
+ finestraNota,
189
+ percentualeFinestra,
190
+ tokenLiberiNellaFinestra: finestraNota ? finestra - token : null,
191
+ oltreLaFinestra: finestraNota ? token > finestra : null,
192
+ costoSuNGiri,
193
+ conCache: {
194
+ attiva: cacheAttiva,
195
+ giri,
196
+ giriPieni,
197
+ giriScontati,
198
+ tokenPieni,
199
+ tokenInCache,
200
+ dollari: dollariConCache,
201
+ dollariSenzaCache,
202
+ risparmioDollari: arrotonda(dollariSenzaCache - dollariConCache, 10),
203
+ fattoreRisparmio: dollariConCache > 0 ? arrotonda(dollariSenzaCache / dollariConCache, 3) : null,
204
+ listino: `prompt $${listino.promptDollariPerMilione}/M, input_cache_read $${listino.cacheReadDollariPerMilione}/M (${listino.misurato})`,
205
+ avvertenza: cacheAttiva
206
+ ? `⛔ I primi ${listino.giriPagatiPieni} giri si pagano PIENI (${tokenPieni} token a $${listino.promptDollariPerMilione}/M): la cache prende dalla TERZA chiamata. Il risparmio qui vale solo perché i giri sono ${giri}.`
207
+ : `⛔ Con ${giri} gir${giri === 1 ? 'o' : 'i'} la cache non ha ancora preso: si paga tutto pieno, esattamente come senza cache. Chi prova due volte sole conclude «non funziona» — è l'unico esito sbagliato possibile, non un difetto della cache.`,
208
+ },
209
+ };
210
+ }
211
+
212
+ /**
213
+ * Adattatore: prende il contatore che c'è GIÀ (`createContextTokenCounter`, `src/context-token-counters.mjs:40`)
214
+ * e ne ricava una porta sincrona per `costoDelTesto`/`costoElenco`.
215
+ *
216
+ * ⛔ Pre-misura i testi che gli passi e basta: sui testi che non ha visto la porta SI FERMA invece
217
+ * di ripiegare su una stima — un ripiego silenzioso è esattamente il modo in cui una stima
218
+ * diventa una misura per sbaglio.
219
+ * ⛔ `sottraiInvolucro`: `countPreparedContext` conta l'INTERA richiesta (nome del modello, ruoli,
220
+ * scaffolding JSON), non il testo nudo. Misurando anche la richiesta vuota e sottraendo si
221
+ * ottiene il costo MARGINALE dell'elenco — che è la domanda vera: «quanto costa AGGIUNGERLO».
222
+ * ⛔ Se il contatore risponde con il suo ripiego euristico (`method === 'heuristic'`), la porta si
223
+ * dichiara `metodo: 'stimato'` e il risultato lo eredita: nessun travestimento.
224
+ */
225
+ export async function contatoreDaContextEngine({ counter, model, testi, signal, sottraiInvolucro = true } = {}) {
226
+ if (typeof counter?.countPreparedContext !== 'function') fallisci('COSTO_CONTATORE_ASSENTE', 'Serve un contatore con countPreparedContext (createContextTokenCounter).');
227
+ if (!Array.isArray(testi) || testi.some(testo => typeof testo !== 'string')) fallisci('COSTO_TESTI_INVALIDI', 'I testi da pre-misurare devono essere un elenco di stringhe.');
228
+ const conta = async testo => {
229
+ const esito = await counter.countPreparedContext({ messages: [{ role: 'user', content: testo }], tools: [], model, signal });
230
+ if (!Number.isSafeInteger(esito?.inputTokens) || esito.inputTokens < 0) fallisci('COSTO_CONTEGGIO_INVALIDO', 'Il contatore del Context Engine non ha restituito un conteggio valido.');
231
+ return esito;
232
+ };
233
+ const involucro = sottraiInvolucro ? (await conta('')).inputTokens : 0;
234
+ const misurati = new Map();
235
+ let stimato = false;
236
+ const metodiVisti = new Set();
237
+ for (const testo of testi) {
238
+ const esito = await conta(testo);
239
+ if (esito.method === 'heuristic') stimato = true;
240
+ metodiVisti.add(esito.method);
241
+ misurati.set(testo, Math.max(0, esito.inputTokens - involucro));
242
+ }
243
+ const fonte = `context-token-counters (${model?.provider ?? 'provider ignoto'}/${model?.model ?? 'modello ignoto'}, method=${[...metodiVisti].join('+') || 'nessuno'}${sottraiInvolucro ? `, involucro ${involucro} token sottratto` : ''})`;
244
+ const porta = testo => {
245
+ if (!misurati.has(testo)) fallisci('COSTO_TESTO_NON_MISURATO', 'Questo testo non è stato pre-misurato: la porta non stima di nascosto. Passalo a contatoreDaContextEngine().');
246
+ return misurati.get(testo);
247
+ };
248
+ porta.fonte = fonte;
249
+ porta.metodo = stimato ? 'stimato' : 'contato';
250
+ porta.involucroToken = involucro;
251
+ return porta;
252
+ }
@@ -0,0 +1,171 @@
1
+ /**
2
+ * custom-task.mjs — un compito LIBERO su una cartella dell'allowlist
3
+ * (`TALOS_HARNESS_UI_PROJECT_DIRS`, vedi `config.mjs`), non uno dei task
4
+ * fissi del corpus benchmark. Owner, 27/8: "per adesso un allowlist per
5
+ * testare... come se fosse un vero coding agent di produzione".
6
+ *
7
+ * ⛔⛔ Diverso da `task-catalog.mjs` apposta: quello fa SEMPRE una copia
8
+ * usa-e-getta (`preparaCopia`), perché il suo scopo è misurare un harness
9
+ * senza mai toccare l'originale del corpus. Qui l'obiettivo dichiarato è
10
+ * l'opposto — vedere l'effetto REALE su un progetto vero, esattamente
11
+ * come un coding agent di produzione — quindi si lavora DIRETTAMENTE
12
+ * sulla cartella scelta. Il confine di sicurezza non è "una copia": è
13
+ * l'allowlist stessa, verificata all'AVVIO del server (vedi config.mjs),
14
+ * mai un percorso a piacere accettato a runtime.
15
+ *
16
+ * ⭐⭐⭐ 28/8 — ECCEZIONE dichiarata al paragrafo sopra: col permesso "Full
17
+ * access" (piano elegant-spinning-dongarra.md, owner) un percorso A
18
+ * PIACERE È ammesso — `cartellaLibera` sotto. Questo file NON scrive una
19
+ * denylist di percorsi sensibili come CONFINE di sicurezza, e il motivo è
20
+ * questo, non quello scritto qui fino al 16/09:
21
+ * ⛔ 16/09 — CITAZIONE CORRETTA. La versione precedente attribuiva a un
22
+ * inesistente «Docker/Developers Digest, agosto 2026» la tesi che le
23
+ * denylist fossero «una strategia fallita». Verificato aprendo le fonti
24
+ * (segnalazione della lane CLI, controllata qui): NON esiste una
25
+ * pubblicazione congiunta; Developers Digest, «AI Coding Agent Security
26
+ * Models Compared 2026» (28 LUGLIO 2026,
27
+ * developersdigest.tech/blog/ai-coding-agent-security-models-compared-2026)
28
+ * RACCOMANDA «credential masking or deny rules for SSH keys and .env
29
+ * files»; Docker, «AI Coding Agent Horror Stories» (18 MAGGIO 2026,
30
+ * docker.com/blog/ai-coding-agent-horror-stories-security-risks) SPEDISCE
31
+ * una blocklist («~/.aws, ~/.ssh, ~/.docker, ~/.gnupg, ~/.netrc, ~/.npm and
32
+ * ~/.cargo are all on the blocklist»); la frase sugli escape «nella
33
+ * categoria denylist» è di un TERZO editore, Pillar Security, «The Week of
34
+ * Sandbox Escapes» (20 LUGLIO 2026, pillar.security/blog/the-week-of-sandbox-escapes),
35
+ * e parla di profili di CONFINAMENTO del sistema operativo (Seatbelt su
36
+ * macOS): «a list of things somebody remembered to block that's always
37
+ * one entry short». ⇒ La distinzione che conta, e che nessuna fonte fa al
38
+ * posto nostro: una lista usata come CONFINE («questa lista ferma un
39
+ * attaccante») è debole perché un percorso si calcola a runtime — su
40
+ * questo Pillar ha ragione; una lista usata come INNESCO («questa lista
41
+ * decide quando CHIEDERE») non promette contenimento: se manca un
42
+ * percorso, il caso peggiore è lo stato di oggi. Il commento vecchio
43
+ * applicava una critica della prima classe a un meccanismo della
44
+ * seconda. NVIDIA («Practical Security Guidance for Sandboxing Agentic
45
+ * Workflows», developer.nvidia.com) raccomanda un piccolo elenco di
46
+ * percorsi protetti anche a Full access — e copre l'ACCESSO, lettura
47
+ * compresa, non le sole scritture come riassumeva il dossier del 03/09.
48
+ * ⇒ Qui resta «nessuna denylist come confine»; l'innesco «la shell chiede
49
+ * davanti a un percorso segreto» è la fase P0-bis (owner 16/09, «Sì»).
50
+ * La sicurezza vera sta altrove: (1) il permesso "Full access" è una SCELTA ESPLICITA
51
+ * dell'owner per QUELLA sessione, mai un default; (2) il percorso deve
52
+ * ESISTERE ed essere leggibile/scrivibile DAVVERO (stessa validazione
53
+ * già in uso per l'allowlist fissa, solo a runtime invece che all'avvio
54
+ * — l'owner sceglie un percorso che gestisce già col suo stesso OS); (3)
55
+ * da quel momento la cartella scelta diventa la RADICE della sessione
56
+ * esattamente come una voce dell'allowlist — lo stesso `isPathInside`
57
+ * la contiene, solo più in alto nell'albero. Non è "nessun confine": è
58
+ * "il confine è dove l'owner ha detto esplicitamente", il pattern che
59
+ * VS Code chiama Workspace Trust.
60
+ */
61
+ import { accessSync, constants, realpathSync, statSync } from 'node:fs';
62
+ import { isAbsolute } from 'node:path';
63
+
64
+ export class CustomTaskError extends Error {
65
+ constructor(message, code = 'PROJECT_NOT_ALLOWED') {
66
+ super(message);
67
+ this.name = 'CustomTaskError';
68
+ this.code = code;
69
+ }
70
+ }
71
+
72
+ const COMANDO_PROVA_DEFAULT = 'npm test';
73
+ const CONSEGNA_MASSIMA_BYTE = 8192;
74
+
75
+ /** Proiezione leggera per il menu — mai il percorso assoluto verso il browser: solo id e nome, stesso principio di `listaTaskDisponibili`. */
76
+ export function elencaCartelleProgetto(cartelleProgetto) {
77
+ return cartelleProgetto.map(({ id, nome }) => ({ id, nome }));
78
+ }
79
+
80
+ /**
81
+ * ⭐⭐⭐ 28/8 — la validazione di un percorso "Full access", a RUNTIME (non
82
+ * all'avvio: qui il percorso non è noto in anticipo). Stessa forma
83
+ * dell'errore già dichiarata sopra, stessi controlli di
84
+ * `config.mjs#parseCartelleProgetto` — deliberatamente duplicati invece
85
+ * di importati: quella funzione valida un ARRAY all'avvio e fallisce
86
+ * l'intero processo (`fail()`, mai recuperabile), questa valida UN
87
+ * percorso per richiesta e deve tornare un errore HTTP onesto, non far
88
+ * cadere il server.
89
+ */
90
+ function validaCartellaLibera(percorsoInput, { realpathSyncFn = realpathSync, statSyncFn = statSync, accessSyncFn = accessSync } = {}) {
91
+ if (typeof percorsoInput !== 'string' || percorsoInput.trim().length === 0) {
92
+ throw new CustomTaskError('cartellaLibera mancante', 'QUERY_INVALID');
93
+ }
94
+ if (!isAbsolute(percorsoInput)) {
95
+ throw new CustomTaskError('cartellaLibera deve essere un percorso assoluto', 'QUERY_INVALID');
96
+ }
97
+ let reale;
98
+ try {
99
+ reale = realpathSyncFn(percorsoInput);
100
+ } catch {
101
+ throw new CustomTaskError(`Il percorso non esiste o non è raggiungibile: ${percorsoInput}`, 'PROJECT_NOT_ALLOWED');
102
+ }
103
+ let stat;
104
+ try {
105
+ stat = statSyncFn(reale);
106
+ } catch {
107
+ throw new CustomTaskError(`Il percorso non è leggibile: ${percorsoInput}`, 'PROJECT_NOT_ALLOWED');
108
+ }
109
+ if (!stat.isDirectory()) {
110
+ throw new CustomTaskError(`Il percorso non è una cartella: ${percorsoInput}`, 'PROJECT_NOT_ALLOWED');
111
+ }
112
+ try {
113
+ accessSyncFn(reale, constants.R_OK | constants.W_OK);
114
+ } catch {
115
+ throw new CustomTaskError(`La cartella non è leggibile/scrivibile: ${percorsoInput}`, 'PROJECT_NOT_ALLOWED');
116
+ }
117
+ return reale;
118
+ }
119
+
120
+ /**
121
+ * @returns {{cartella:string, comandoProva:string, task:object}} — mai un
122
+ * `pulisci()`: non c'è una copia usa-e-getta da buttare, la cartella È
123
+ * il progetto vero.
124
+ *
125
+ * ⭐ `cartellaLibera` (assoluta, validata da `validaCartellaLibera`) e
126
+ * `cartellaId` (dall'allowlist) sono MUTUAMENTE ESCLUSIVI — chi chiama
127
+ * decide quale dei due passare in base al permesso della sessione (vedi
128
+ * session-registry.mjs), questa funzione si limita a rifiutare se
129
+ * arrivano ENTRAMBI o NESSUNO dei due, mai a scegliere lei stessa.
130
+ */
131
+ export function preparaEsecuzioneLibera(cartelleProgetto, { cartellaId, cartellaLibera, consegna, comandoProva }, deps = {}) {
132
+ if ((cartellaId && cartellaLibera) || (!cartellaId && !cartellaLibera)) {
133
+ throw new CustomTaskError('serve ESATTAMENTE uno fra cartellaId e cartellaLibera', 'QUERY_INVALID');
134
+ }
135
+
136
+ let cartella;
137
+ let nomeProgetto;
138
+ if (cartellaLibera) {
139
+ cartella = validaCartellaLibera(cartellaLibera, deps);
140
+ nomeProgetto = cartella.split(/[\\/]/).pop() || cartella;
141
+ } else {
142
+ if (typeof cartellaId !== 'string' || cartellaId.length === 0) {
143
+ throw new CustomTaskError('cartellaId mancante', 'QUERY_INVALID');
144
+ }
145
+ const voce = cartelleProgetto.find((candidata) => candidata.id === cartellaId);
146
+ if (!voce) throw new CustomTaskError(`Cartella non ammessa: ${cartellaId}`);
147
+ cartella = voce.percorso;
148
+ nomeProgetto = voce.nome;
149
+ }
150
+
151
+ if (typeof consegna !== 'string' || consegna.trim().length === 0) {
152
+ throw new CustomTaskError('consegna mancante', 'QUERY_INVALID');
153
+ }
154
+ if (Buffer.byteLength(consegna, 'utf8') > CONSEGNA_MASSIMA_BYTE) {
155
+ throw new CustomTaskError(`consegna oltre ${CONSEGNA_MASSIMA_BYTE} byte`, 'QUERY_INVALID');
156
+ }
157
+
158
+ let comando = COMANDO_PROVA_DEFAULT;
159
+ if (comandoProva !== undefined && comandoProva !== null) {
160
+ if (typeof comandoProva !== 'string' || comandoProva.trim().length === 0) {
161
+ throw new CustomTaskError('comandoProva non valido', 'QUERY_INVALID');
162
+ }
163
+ comando = comandoProva;
164
+ }
165
+
166
+ return {
167
+ cartella,
168
+ comandoProva: comando,
169
+ task: { consegna, consegnaCorta: consegna.length > 80 ? `${consegna.slice(0, 77)}...` : consegna, progetto: nomeProgetto },
170
+ };
171
+ }
@@ -0,0 +1,142 @@
1
+ /**
2
+ * doctor.mjs — 4 controlli VERI, non un "Doctor: Healthy" scritto a mano.
3
+ * Blocco Settings, sotto-carta "Control plane". Owner, 27/8: "analizza
4
+ * bene... eliminare tutti i mockup" — trovato che il pulsante Doctor
5
+ * mostrava sempre lo stesso testo hardcoded, indipendentemente da
6
+ * qualunque stato reale del sistema.
7
+ *
8
+ * ⛔ Stesso principio già in uso in tutto il progetto (`enforcement`
9
+ * dichiarato, mai un bluff): ogni riga dice cosa ha VERIFICATO, non cosa
10
+ * ci si aspetta. `shell` riusa `eseguiComandoSandboxato` — la stessa
11
+ * funzione che l'attrezzo `shell` chiama davvero, non una seconda
12
+ * verifica scritta a mano che potrebbe disallinearsi.
13
+ */
14
+ import { mkdtempSync, rmSync } from 'node:fs';
15
+ import { tmpdir } from 'node:os';
16
+ import { join } from 'node:path';
17
+
18
+ import { createProcessPolicy } from './process-policy.mjs';
19
+
20
+ const DOCTOR_PROCESS_POLICY = createProcessPolicy({ allowedExecutables: ['git', 'echo'], capabilities: { doctor: tmpdir() } });
21
+
22
+ async function eseguiComandoSandboxatoLocale(comando, cartella) {
23
+ // Doctor esegue soltanto il controllo fisso `echo ok`; non accetta testo
24
+ // composto dal chiamante. Il resto del sistema usa il runtime owner.
25
+ if (comando !== 'echo ok') return { codice: 1, testo: 'Controllo non previsto', enforcement: 'none' };
26
+ const risultato = await DOCTOR_PROCESS_POLICY.runApprovedProcess({
27
+ executable: 'echo',
28
+ args: ['ok'],
29
+ cwd: cartella,
30
+ capability: 'doctor',
31
+ envKeys: ['PATH', 'SystemRoot', 'WINDIR'],
32
+ });
33
+ return { codice: risultato.code ?? 1, testo: risultato.stdout.trim(), enforcement: 'desktop' };
34
+ }
35
+
36
+ function controllaGit(spawnSyncFn) {
37
+ try {
38
+ if (spawnSyncFn) return spawnSyncFn('git', ['--version']).status === 0;
39
+ DOCTOR_PROCESS_POLICY.execFileSync('git', ['--version'], { cwd: process.cwd() });
40
+ return true;
41
+ } catch {
42
+ return false;
43
+ }
44
+ }
45
+
46
+ /**
47
+ * @param {boolean} chiaveConfigurata — config.chiaveApi presente sul server.
48
+ * @param {Array<{id:string,nome:string,percorso?:string}>|undefined} cartelleProgetto —
49
+ * elenco già validato da config.mjs. Quando fornito, il Doctor aggiunge un
50
+ * controllo reale per la scelta della cartella nella modale Nuova sessione.
51
+ * @param {Array<object>|undefined} providerRows — stato pubblico dei provider, mai segreti
52
+ * @param {boolean|undefined} providerStoreAvailable — portachiavi di sistema disponibile
53
+ * @param {{configurato:boolean,pronto:boolean,dettaglio:string}|undefined} ownerRuntime — stato reale del runtime agente
54
+ * @param {{disponibile:boolean,dettaglio:string}|undefined} catalogoTask — disponibilità del catalogo task preset
55
+ * @param {{corrotte:string[],ultimaLettura:{ripristinate:number,totali:number}}|undefined} sessioniPersistenza — stato di lettura dei registri persistiti
56
+ * @returns {{chiaveApi:boolean, shell:'wsl2'|'none'|'desktop', git:boolean, naviga:boolean, cartelleProgetto?:{disponibili:boolean,conteggio:number,dettaglio:string}, providers?:{storeAvailable:boolean,items:Array<object>}, ownerRuntime?:{configurato:boolean,pronto:boolean,dettaglio:string}, catalogoTask?:{disponibile:boolean,dettaglio:string}, sessioniPersistenza?:{corrotte:string[],ultimaLettura:{ripristinate:number,totali:number}}}}
57
+ */
58
+ export async function diagnosi({
59
+ chiaveConfigurata, cartelleProgetto, providerRows, providerStoreAvailable, ownerRuntime, catalogoTask, sessioniPersistenza,
60
+ // ⭐ 04/9, R-03 — stato pubblico della fonte di ricerca web (search-source-store.listPublic()): fonte, prontezza; mai una chiave.
61
+ ricercaWeb,
62
+ // ⭐ 04/9, W0-04 — i lab accesi (config.labs): informativo, mai un problema.
63
+ labsAccesi,
64
+ eseguiComandoSandboxatoFn = eseguiComandoSandboxatoLocale, spawnSyncFn,
65
+ } = {}) {
66
+ // ⛔ Una cartella usa-e-getta SOLO per il comando diagnostico, mai una
67
+ // cartella del progetto vero — il Doctor non deve toccare niente.
68
+ const cartellaProva = mkdtempSync(join(tmpdir(), 'talos-doctor-'));
69
+ let shell;
70
+ try {
71
+ const esito = await eseguiComandoSandboxatoFn('echo ok', cartellaProva);
72
+ shell = esito.enforcement;
73
+ } finally {
74
+ rmSync(cartellaProva, { recursive: true, force: true });
75
+ }
76
+ const risultato = {
77
+ chiaveApi: Boolean(chiaveConfigurata),
78
+ shell,
79
+ git: controllaGit(spawnSyncFn),
80
+ naviga: true, // built-in, nessuna dipendenza esterna da verificare
81
+ };
82
+ if (Array.isArray(labsAccesi)) {
83
+ risultato.labs = { accesi: [...labsAccesi], dettaglio: labsAccesi.length ? `Labs accesi: ${labsAccesi.join(', ')} (NOT_LIVE_VALIDATED finché non provati).` : 'Labs accesi: nessuno.' };
84
+ }
85
+ if (ricercaWeb && typeof ricercaWeb === 'object') {
86
+ const fonte = Array.isArray(ricercaWeb.fonti) ? ricercaWeb.fonti.find((f) => f.id === ricercaWeb.source) : null;
87
+ risultato.ricercaWeb = {
88
+ fonte: ricercaWeb.source,
89
+ etichetta: fonte?.label ?? (ricercaWeb.source === 'off' ? 'Spenta' : ricercaWeb.source),
90
+ pronta: ricercaWeb.readiness === 'pronta',
91
+ dettaglio: ricercaWeb.readiness === 'pronta' ? (fonte?.keyless ? 'Pronta, senza chiave: DuckDuckGo (pagina pubblica, può bloccare sotto uso intenso).' : `Pronta: ${fonte?.label ?? ricercaWeb.source}.`)
92
+ : ricercaWeb.readiness === 'spenta' ? 'Spenta da te: il modello non cercherà sul web.'
93
+ : ricercaWeb.readiness === 'chiave-mancante' ? `Serve ancora la chiave di ${fonte?.label ?? ricercaWeb.source}.`
94
+ : `Serve ancora l'indirizzo dell'istanza ${fonte?.label ?? ricercaWeb.source}.`,
95
+ };
96
+ }
97
+ if (Array.isArray(cartelleProgetto)) {
98
+ risultato.cartelleProgetto = {
99
+ disponibili: cartelleProgetto.length > 0,
100
+ conteggio: cartelleProgetto.length,
101
+ dettaglio: cartelleProgetto.length > 0
102
+ ? `${cartelleProgetto.length} cartell${cartelleProgetto.length === 1 ? 'a' : 'e'} di progetto disponibili.`
103
+ : 'Nessuna cartella di progetto è stata configurata nell’elenco consentito.',
104
+ };
105
+ }
106
+ if (Array.isArray(providerRows)) {
107
+ risultato.providers = { storeAvailable: providerStoreAvailable === true, items: providerRows };
108
+ }
109
+ if (ownerRuntime && typeof ownerRuntime === 'object') {
110
+ risultato.ownerRuntime = {
111
+ configurato: ownerRuntime.configurato === true,
112
+ pronto: ownerRuntime.pronto === true,
113
+ dettaglio: typeof ownerRuntime.dettaglio === 'string' ? ownerRuntime.dettaglio : 'Stato runtime non osservato.',
114
+ };
115
+ }
116
+ if (catalogoTask && typeof catalogoTask === 'object') {
117
+ risultato.catalogoTask = {
118
+ disponibile: catalogoTask.disponibile === true,
119
+ dettaglio: typeof catalogoTask.dettaglio === 'string' ? catalogoTask.dettaglio : 'Elenco attività predefinite non osservato.',
120
+ };
121
+ }
122
+ if (sessioniPersistenza && typeof sessioniPersistenza === 'object') {
123
+ // ⭐ 04/9, W0-01 — ogni file scartato porta il suo motivo: il Doctor dice DOVE è finita la differenza fra totali e ripristinate.
124
+ const scartate = Array.isArray(sessioniPersistenza.scartate)
125
+ ? sessioniPersistenza.scartate.filter((s) => s && typeof s.sessionId === 'string').map((s) => ({ sessionId: s.sessionId, motivo: String(s.motivo || 'ignoto'), ...(s.dettaglio ? { dettaglio: String(s.dettaglio).slice(0, 200) } : {}) }))
126
+ : [];
127
+ const ripristinate = Number(sessioniPersistenza.ultimaLettura?.ripristinate) || 0;
128
+ const totali = Number(sessioniPersistenza.ultimaLettura?.totali) || 0;
129
+ const perMotivo = {};
130
+ for (const s of scartate) perMotivo[s.motivo] = (perMotivo[s.motivo] || 0) + 1;
131
+ risultato.sessioniPersistenza = {
132
+ corrotte: Array.isArray(sessioniPersistenza.corrotte) ? [...sessioniPersistenza.corrotte] : [],
133
+ scartate,
134
+ perMotivo,
135
+ ultimaLettura: { ripristinate, totali },
136
+ dettaglio: scartate.length === 0
137
+ ? `${ripristinate} sessioni ripristinate su ${totali}: nessuna scartata.`
138
+ : `${ripristinate} ripristinate, ${scartate.length} scartate su ${totali}: ${Object.entries(perMotivo).map(([m, n]) => `${n} ${m}`).join(', ')}.`,
139
+ };
140
+ }
141
+ return risultato;
142
+ }