@vascend/talos 0.1.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 (620) hide show
  1. package/README.md +202 -0
  2. package/dist/backend/asset-events.d.ts +31 -0
  3. package/dist/backend/asset-events.js +149 -0
  4. package/dist/backend/assets.d.ts +31 -0
  5. package/dist/backend/assets.js +210 -0
  6. package/dist/backend/auth.d.ts +18 -0
  7. package/dist/backend/auth.js +59 -0
  8. package/dist/backend/config.d.ts +14 -0
  9. package/dist/backend/config.js +41 -0
  10. package/dist/backend/conversations.d.ts +21 -0
  11. package/dist/backend/conversations.js +141 -0
  12. package/dist/backend/crypto.d.ts +9 -0
  13. package/dist/backend/crypto.js +68 -0
  14. package/dist/backend/engine.d.ts +13 -0
  15. package/dist/backend/engine.js +78 -0
  16. package/dist/backend/http.d.ts +25 -0
  17. package/dist/backend/http.js +112 -0
  18. package/dist/backend/index.d.ts +1 -0
  19. package/dist/backend/index.js +24 -0
  20. package/dist/backend/routes.d.ts +14 -0
  21. package/dist/backend/routes.js +169 -0
  22. package/dist/backend/sdk.d.ts +12 -0
  23. package/dist/backend/sdk.js +11 -0
  24. package/dist/backend/server.d.ts +12 -0
  25. package/dist/backend/server.js +91 -0
  26. package/dist/backend/temp-image-store.d.ts +46 -0
  27. package/dist/backend/temp-image-store.js +197 -0
  28. package/dist/backend/types.d.ts +103 -0
  29. package/dist/backend/types.js +29 -0
  30. package/dist/backend/users.d.ts +13 -0
  31. package/dist/backend/users.js +76 -0
  32. package/dist/backend/ws-handler.d.ts +14 -0
  33. package/dist/backend/ws-handler.js +72 -0
  34. package/dist/backend/ws.d.ts +19 -0
  35. package/dist/backend/ws.js +146 -0
  36. package/dist/core/agent.d.ts +56 -0
  37. package/dist/core/agent.js +1783 -0
  38. package/dist/core/background.d.ts +45 -0
  39. package/dist/core/background.js +140 -0
  40. package/dist/core/bootstrap.d.ts +45 -0
  41. package/dist/core/bootstrap.js +294 -0
  42. package/dist/core/cli-args.d.ts +41 -0
  43. package/dist/core/cli-args.js +282 -0
  44. package/dist/core/collapsible-output.d.ts +38 -0
  45. package/dist/core/collapsible-output.js +143 -0
  46. package/dist/core/compaction-checkpoint.d.ts +9 -0
  47. package/dist/core/compaction-checkpoint.js +58 -0
  48. package/dist/core/compaction.d.ts +42 -0
  49. package/dist/core/compaction.js +270 -0
  50. package/dist/core/config.d.ts +49 -0
  51. package/dist/core/config.js +417 -0
  52. package/dist/core/context-ledger.d.ts +99 -0
  53. package/dist/core/context-ledger.js +222 -0
  54. package/dist/core/context-policy.d.ts +17 -0
  55. package/dist/core/context-policy.js +81 -0
  56. package/dist/core/continuation-review.d.ts +23 -0
  57. package/dist/core/continuation-review.js +64 -0
  58. package/dist/core/crash-report.d.ts +3 -0
  59. package/dist/core/crash-report.js +27 -0
  60. package/dist/core/credentials.d.ts +11 -0
  61. package/dist/core/credentials.js +183 -0
  62. package/dist/core/doctor.d.ts +24 -0
  63. package/dist/core/doctor.js +125 -0
  64. package/dist/core/effort.d.ts +5 -0
  65. package/dist/core/effort.js +42 -0
  66. package/dist/core/env-paths.d.ts +3 -0
  67. package/dist/core/env-paths.js +38 -0
  68. package/dist/core/execution-guidance.d.ts +9 -0
  69. package/dist/core/execution-guidance.js +73 -0
  70. package/dist/core/execution-target.d.ts +41 -0
  71. package/dist/core/execution-target.js +69 -0
  72. package/dist/core/file-coordination.d.ts +66 -0
  73. package/dist/core/file-coordination.js +176 -0
  74. package/dist/core/file-lease-store.d.ts +14 -0
  75. package/dist/core/file-lease-store.js +89 -0
  76. package/dist/core/image-limits.d.ts +26 -0
  77. package/dist/core/image-limits.js +205 -0
  78. package/dist/core/interruptible-completion.d.ts +3 -0
  79. package/dist/core/interruptible-completion.js +42 -0
  80. package/dist/core/kingdoms-mode-state.d.ts +24 -0
  81. package/dist/core/kingdoms-mode-state.js +139 -0
  82. package/dist/core/licenza.d.ts +38 -0
  83. package/dist/core/licenza.js +125 -0
  84. package/dist/core/model-limits.d.ts +2 -0
  85. package/dist/core/model-limits.js +36 -0
  86. package/dist/core/output-lifecycle.d.ts +44 -0
  87. package/dist/core/output-lifecycle.js +190 -0
  88. package/dist/core/package-update.d.ts +15 -0
  89. package/dist/core/package-update.js +121 -0
  90. package/dist/core/permission-queue.d.ts +2 -0
  91. package/dist/core/permission-queue.js +20 -0
  92. package/dist/core/prompt-attachment.d.ts +21 -0
  93. package/dist/core/prompt-attachment.js +123 -0
  94. package/dist/core/request-plan.d.ts +16 -0
  95. package/dist/core/request-plan.js +36 -0
  96. package/dist/core/run-timing.d.ts +17 -0
  97. package/dist/core/run-timing.js +123 -0
  98. package/dist/core/self-update.d.ts +17 -0
  99. package/dist/core/self-update.js +54 -0
  100. package/dist/core/session-refs.d.ts +27 -0
  101. package/dist/core/session-refs.js +126 -0
  102. package/dist/core/session-storage.d.ts +4 -0
  103. package/dist/core/session-storage.js +48 -0
  104. package/dist/core/session-title.d.ts +1 -0
  105. package/dist/core/session-title.js +21 -0
  106. package/dist/core/session-usage.d.ts +17 -0
  107. package/dist/core/session-usage.js +74 -0
  108. package/dist/core/session.d.ts +58 -0
  109. package/dist/core/session.js +296 -0
  110. package/dist/core/settings.d.ts +17 -0
  111. package/dist/core/settings.js +107 -0
  112. package/dist/core/storage-retention.d.ts +21 -0
  113. package/dist/core/storage-retention.js +227 -0
  114. package/dist/core/text-sanitize.d.ts +1 -0
  115. package/dist/core/text-sanitize.js +35 -0
  116. package/dist/core/tool-artifacts.d.ts +6 -0
  117. package/dist/core/tool-artifacts.js +51 -0
  118. package/dist/core/tool-compress.d.ts +8 -0
  119. package/dist/core/tool-compress.js +111 -0
  120. package/dist/core/tool-executor.d.ts +16 -0
  121. package/dist/core/tool-executor.js +97 -0
  122. package/dist/core/tool-result-index.d.ts +52 -0
  123. package/dist/core/tool-result-index.js +327 -0
  124. package/dist/core/tool-result-store.d.ts +38 -0
  125. package/dist/core/tool-result-store.js +190 -0
  126. package/dist/core/tool-scheduler.d.ts +16 -0
  127. package/dist/core/tool-scheduler.js +35 -0
  128. package/dist/core/trust.d.ts +21 -0
  129. package/dist/core/trust.js +95 -0
  130. package/dist/core/types.d.ts +402 -0
  131. package/dist/core/types.js +1 -0
  132. package/dist/core/update-check.d.ts +26 -0
  133. package/dist/core/update-check.js +89 -0
  134. package/dist/core/update-process.d.ts +10 -0
  135. package/dist/core/update-process.js +31 -0
  136. package/dist/core/user-inbox.d.ts +23 -0
  137. package/dist/core/user-inbox.js +56 -0
  138. package/dist/core/user-input.d.ts +10 -0
  139. package/dist/core/user-input.js +35 -0
  140. package/dist/core/version.d.ts +1 -0
  141. package/dist/core/version.js +10 -0
  142. package/dist/core/vibe-modes.d.ts +38 -0
  143. package/dist/core/vibe-modes.js +383 -0
  144. package/dist/core/workspace-roots.d.ts +13 -0
  145. package/dist/core/workspace-roots.js +91 -0
  146. package/dist/core/workspace-sessions.d.ts +51 -0
  147. package/dist/core/workspace-sessions.js +243 -0
  148. package/dist/cowork/client.d.ts +29 -0
  149. package/dist/cowork/client.js +49 -0
  150. package/dist/cowork/engine-bridge.d.ts +22 -0
  151. package/dist/cowork/engine-bridge.js +98 -0
  152. package/dist/cowork/index.d.ts +11 -0
  153. package/dist/cowork/index.js +16 -0
  154. package/dist/cowork/ipc.d.ts +97 -0
  155. package/dist/cowork/ipc.js +96 -0
  156. package/dist/cowork/manager.d.ts +66 -0
  157. package/dist/cowork/manager.js +325 -0
  158. package/dist/cowork/store.d.ts +23 -0
  159. package/dist/cowork/store.js +182 -0
  160. package/dist/cowork/sync.d.ts +20 -0
  161. package/dist/cowork/sync.js +164 -0
  162. package/dist/cowork/types.d.ts +78 -0
  163. package/dist/cowork/types.js +21 -0
  164. package/dist/extensions/ext-tools.d.ts +3 -0
  165. package/dist/extensions/ext-tools.js +73 -0
  166. package/dist/extensions/loader.d.ts +91 -0
  167. package/dist/extensions/loader.js +474 -0
  168. package/dist/extensions/settings.d.ts +20 -0
  169. package/dist/extensions/settings.js +86 -0
  170. package/dist/extensions/types.d.ts +219 -0
  171. package/dist/extensions/types.js +1 -0
  172. package/dist/horizon/async-queue.d.ts +9 -0
  173. package/dist/horizon/async-queue.js +46 -0
  174. package/dist/horizon/engine.d.ts +85 -0
  175. package/dist/horizon/engine.js +603 -0
  176. package/dist/horizon/index.d.ts +4 -0
  177. package/dist/horizon/index.js +2 -0
  178. package/dist/horizon/main-team.d.ts +163 -0
  179. package/dist/horizon/main-team.js +774 -0
  180. package/dist/horizon/types.d.ts +233 -0
  181. package/dist/horizon/types.js +1 -0
  182. package/dist/index.d.ts +2 -0
  183. package/dist/index.js +233 -0
  184. package/dist/mcp/protocol.d.ts +81 -0
  185. package/dist/mcp/protocol.js +122 -0
  186. package/dist/package.json +1 -0
  187. package/dist/packages/packages.d.ts +12 -0
  188. package/dist/packages/packages.js +323 -0
  189. package/dist/prompts/templates.d.ts +13 -0
  190. package/dist/prompts/templates.js +139 -0
  191. package/dist/providers/anthropic-oauth.d.ts +13 -0
  192. package/dist/providers/anthropic-oauth.js +124 -0
  193. package/dist/providers/anthropic.d.ts +27 -0
  194. package/dist/providers/anthropic.js +526 -0
  195. package/dist/providers/catalog.d.ts +15 -0
  196. package/dist/providers/catalog.js +166 -0
  197. package/dist/providers/effort.d.ts +12 -0
  198. package/dist/providers/effort.js +102 -0
  199. package/dist/providers/model-catalog.d.ts +43 -0
  200. package/dist/providers/model-catalog.js +70 -0
  201. package/dist/providers/model-effort.d.ts +5 -0
  202. package/dist/providers/model-effort.js +43 -0
  203. package/dist/providers/model-registry.d.ts +39 -0
  204. package/dist/providers/model-registry.js +142 -0
  205. package/dist/providers/models-dev.d.ts +9 -0
  206. package/dist/providers/models-dev.js +97 -0
  207. package/dist/providers/oauth-callback-server.d.ts +28 -0
  208. package/dist/providers/oauth-callback-server.js +184 -0
  209. package/dist/providers/oauth-orchestrator.d.ts +58 -0
  210. package/dist/providers/oauth-orchestrator.js +159 -0
  211. package/dist/providers/oauth-store.d.ts +12 -0
  212. package/dist/providers/oauth-store.js +139 -0
  213. package/dist/providers/open-browser.d.ts +20 -0
  214. package/dist/providers/open-browser.js +92 -0
  215. package/dist/providers/openai-compat.d.ts +14 -0
  216. package/dist/providers/openai-compat.js +523 -0
  217. package/dist/providers/openai-oauth.d.ts +15 -0
  218. package/dist/providers/openai-oauth.js +382 -0
  219. package/dist/providers/openrouter-auth.d.ts +73 -0
  220. package/dist/providers/openrouter-auth.js +321 -0
  221. package/dist/providers/openrouter-manual-fallback.d.ts +25 -0
  222. package/dist/providers/openrouter-manual-fallback.js +161 -0
  223. package/dist/providers/openrouter.d.ts +15 -0
  224. package/dist/providers/openrouter.js +107 -0
  225. package/dist/providers/plan-limits.d.ts +6 -0
  226. package/dist/providers/plan-limits.js +175 -0
  227. package/dist/providers/provider-reset.d.ts +4 -0
  228. package/dist/providers/provider-reset.js +83 -0
  229. package/dist/providers/quota-limits.d.ts +4 -0
  230. package/dist/providers/quota-limits.js +33 -0
  231. package/dist/providers/registry.d.ts +33 -0
  232. package/dist/providers/registry.js +213 -0
  233. package/dist/providers/retry.d.ts +37 -0
  234. package/dist/providers/retry.js +347 -0
  235. package/dist/rpc/server.d.ts +23 -0
  236. package/dist/rpc/server.js +222 -0
  237. package/dist/sdk.d.ts +67 -0
  238. package/dist/sdk.js +64 -0
  239. package/dist/skills/skills.d.ts +17 -0
  240. package/dist/skills/skills.js +155 -0
  241. package/dist/text-sanitize.js +28 -0
  242. package/dist/tools/ask-user.d.ts +2 -0
  243. package/dist/tools/ask-user.js +60 -0
  244. package/dist/tools/builtin.d.ts +48 -0
  245. package/dist/tools/builtin.js +2494 -0
  246. package/dist/tools/capabilities.d.ts +5 -0
  247. package/dist/tools/capabilities.js +55 -0
  248. package/dist/tools/computer-capture.d.ts +2 -0
  249. package/dist/tools/computer-capture.js +91 -0
  250. package/dist/tools/computer-posix.d.ts +5 -0
  251. package/dist/tools/computer-posix.js +136 -0
  252. package/dist/tools/computer-use.d.ts +14 -0
  253. package/dist/tools/computer-use.js +108 -0
  254. package/dist/tools/discovery.d.ts +12 -0
  255. package/dist/tools/discovery.js +205 -0
  256. package/dist/tools/execute.d.ts +2 -0
  257. package/dist/tools/execute.js +73 -0
  258. package/dist/tools/normalize-input.d.ts +17 -0
  259. package/dist/tools/normalize-input.js +174 -0
  260. package/dist/tools/output-artifact.d.ts +3 -0
  261. package/dist/tools/output-artifact.js +80 -0
  262. package/dist/tools/pagination.d.ts +15 -0
  263. package/dist/tools/pagination.js +93 -0
  264. package/dist/tools/plan-status.d.ts +2 -0
  265. package/dist/tools/plan-status.js +109 -0
  266. package/dist/tools/program-input.d.ts +3 -0
  267. package/dist/tools/program-input.js +19 -0
  268. package/dist/tools/program.d.ts +3 -0
  269. package/dist/tools/program.js +567 -0
  270. package/dist/tools/protocol-catalog.d.ts +12 -0
  271. package/dist/tools/protocol-catalog.js +46 -0
  272. package/dist/tools/ramification-tools.d.ts +12 -0
  273. package/dist/tools/ramification-tools.js +528 -0
  274. package/dist/tools/regex-compat.d.ts +9 -0
  275. package/dist/tools/regex-compat.js +96 -0
  276. package/dist/tools/registry.d.ts +51 -0
  277. package/dist/tools/registry.js +185 -0
  278. package/dist/tools/resolve.d.ts +2 -0
  279. package/dist/tools/resolve.js +107 -0
  280. package/dist/tools/shell-env.d.ts +2 -0
  281. package/dist/tools/shell-env.js +7 -0
  282. package/dist/tools/team-input.d.ts +3 -0
  283. package/dist/tools/team-input.js +129 -0
  284. package/dist/tools/team-tools.d.ts +7 -0
  285. package/dist/tools/team-tools.js +348 -0
  286. package/dist/tools/workspace-sessions.d.ts +2 -0
  287. package/dist/tools/workspace-sessions.js +35 -0
  288. package/dist/tui/account-entry.d.ts +1 -0
  289. package/dist/tui/account-entry.js +93 -0
  290. package/dist/tui/attachment-highlight.d.ts +1 -0
  291. package/dist/tui/attachment-highlight.js +24 -0
  292. package/dist/tui/attachment-refs.d.ts +2 -0
  293. package/dist/tui/attachment-refs.js +3 -0
  294. package/dist/tui/bottom-input.d.ts +219 -0
  295. package/dist/tui/bottom-input.js +1958 -0
  296. package/dist/tui/branding.d.ts +107 -0
  297. package/dist/tui/branding.js +750 -0
  298. package/dist/tui/clipboard-image.d.ts +10 -0
  299. package/dist/tui/clipboard-image.js +163 -0
  300. package/dist/tui/clipboard-text.d.ts +9 -0
  301. package/dist/tui/clipboard-text.js +51 -0
  302. package/dist/tui/clipboard-windows.d.ts +8 -0
  303. package/dist/tui/clipboard-windows.js +84 -0
  304. package/dist/tui/collapsible-command.d.ts +15 -0
  305. package/dist/tui/collapsible-command.js +48 -0
  306. package/dist/tui/command-feedback.d.ts +10 -0
  307. package/dist/tui/command-feedback.js +46 -0
  308. package/dist/tui/command-output-registry.d.ts +28 -0
  309. package/dist/tui/command-output-registry.js +103 -0
  310. package/dist/tui/command-transcript.d.ts +12 -0
  311. package/dist/tui/command-transcript.js +90 -0
  312. package/dist/tui/commands.d.ts +20 -0
  313. package/dist/tui/commands.js +131 -0
  314. package/dist/tui/context-bar.d.ts +4 -0
  315. package/dist/tui/context-bar.js +74 -0
  316. package/dist/tui/context-meter.d.ts +25 -0
  317. package/dist/tui/context-meter.js +81 -0
  318. package/dist/tui/cost-binding.d.ts +8 -0
  319. package/dist/tui/cost-binding.js +43 -0
  320. package/dist/tui/cost-tracker.d.ts +49 -0
  321. package/dist/tui/cost-tracker.js +122 -0
  322. package/dist/tui/diff.d.ts +9 -0
  323. package/dist/tui/diff.js +119 -0
  324. package/dist/tui/i18n-catalog.d.ts +5 -0
  325. package/dist/tui/i18n-catalog.js +184 -0
  326. package/dist/tui/i18n.d.ts +6 -0
  327. package/dist/tui/i18n.js +10 -0
  328. package/dist/tui/image-attachments.d.ts +46 -0
  329. package/dist/tui/image-attachments.js +167 -0
  330. package/dist/tui/images.d.ts +12 -0
  331. package/dist/tui/images.js +87 -0
  332. package/dist/tui/intro.d.ts +13 -0
  333. package/dist/tui/intro.js +109 -0
  334. package/dist/tui/language-preference.d.ts +6 -0
  335. package/dist/tui/language-preference.js +24 -0
  336. package/dist/tui/live-output.d.ts +2 -0
  337. package/dist/tui/live-output.js +18 -0
  338. package/dist/tui/locales.d.ts +19 -0
  339. package/dist/tui/locales.js +22 -0
  340. package/dist/tui/login-menu.d.ts +17 -0
  341. package/dist/tui/login-menu.js +58 -0
  342. package/dist/tui/logo.d.ts +8 -0
  343. package/dist/tui/logo.js +305 -0
  344. package/dist/tui/markdown-inline.d.ts +3 -0
  345. package/dist/tui/markdown-inline.js +99 -0
  346. package/dist/tui/markdown-table.d.ts +4 -0
  347. package/dist/tui/markdown-table.js +63 -0
  348. package/dist/tui/markdown.d.ts +11 -0
  349. package/dist/tui/markdown.js +257 -0
  350. package/dist/tui/mcp-transcript.d.ts +7 -0
  351. package/dist/tui/mcp-transcript.js +40 -0
  352. package/dist/tui/model-details.d.ts +2 -0
  353. package/dist/tui/model-details.js +21 -0
  354. package/dist/tui/model-selection.d.ts +7 -0
  355. package/dist/tui/model-selection.js +41 -0
  356. package/dist/tui/native-scrollback.d.ts +27 -0
  357. package/dist/tui/native-scrollback.js +134 -0
  358. package/dist/tui/onboarding.d.ts +72 -0
  359. package/dist/tui/onboarding.js +207 -0
  360. package/dist/tui/paste-collapse.d.ts +39 -0
  361. package/dist/tui/paste-collapse.js +117 -0
  362. package/dist/tui/perm-mode.d.ts +8 -0
  363. package/dist/tui/perm-mode.js +41 -0
  364. package/dist/tui/prompt-queue-store.d.ts +26 -0
  365. package/dist/tui/prompt-queue-store.js +87 -0
  366. package/dist/tui/prompt-queue.d.ts +47 -0
  367. package/dist/tui/prompt-queue.js +187 -0
  368. package/dist/tui/provider-menu.d.ts +33 -0
  369. package/dist/tui/provider-menu.js +175 -0
  370. package/dist/tui/question.d.ts +24 -0
  371. package/dist/tui/question.js +32 -0
  372. package/dist/tui/queue-screen.d.ts +25 -0
  373. package/dist/tui/queue-screen.js +152 -0
  374. package/dist/tui/queue-team.d.ts +3 -0
  375. package/dist/tui/queue-team.js +28 -0
  376. package/dist/tui/queued-questions.d.ts +20 -0
  377. package/dist/tui/queued-questions.js +163 -0
  378. package/dist/tui/quick-help.d.ts +1 -0
  379. package/dist/tui/quick-help.js +53 -0
  380. package/dist/tui/repl.d.ts +78 -0
  381. package/dist/tui/repl.js +4130 -0
  382. package/dist/tui/restore-queued-input.d.ts +3 -0
  383. package/dist/tui/restore-queued-input.js +20 -0
  384. package/dist/tui/review-detail.d.ts +6 -0
  385. package/dist/tui/review-detail.js +56 -0
  386. package/dist/tui/review-files.d.ts +3 -0
  387. package/dist/tui/review-files.js +22 -0
  388. package/dist/tui/review-render.d.ts +22 -0
  389. package/dist/tui/review-render.js +98 -0
  390. package/dist/tui/review-screen.d.ts +38 -0
  391. package/dist/tui/review-screen.js +358 -0
  392. package/dist/tui/review-store.d.ts +60 -0
  393. package/dist/tui/review-store.js +270 -0
  394. package/dist/tui/review-team.d.ts +2 -0
  395. package/dist/tui/review-team.js +27 -0
  396. package/dist/tui/selector-mouse.d.ts +26 -0
  397. package/dist/tui/selector-mouse.js +65 -0
  398. package/dist/tui/selector.d.ts +51 -0
  399. package/dist/tui/selector.js +299 -0
  400. package/dist/tui/session-replay.d.ts +10 -0
  401. package/dist/tui/session-replay.js +78 -0
  402. package/dist/tui/session-usage.d.ts +22 -0
  403. package/dist/tui/session-usage.js +55 -0
  404. package/dist/tui/skill-transcript.d.ts +5 -0
  405. package/dist/tui/skill-transcript.js +31 -0
  406. package/dist/tui/startup.d.ts +5 -0
  407. package/dist/tui/startup.js +61 -0
  408. package/dist/tui/status-line-loader.d.ts +8 -0
  409. package/dist/tui/status-line-loader.js +33 -0
  410. package/dist/tui/status-line.d.ts +38 -0
  411. package/dist/tui/status-line.js +213 -0
  412. package/dist/tui/system-language.d.ts +8 -0
  413. package/dist/tui/system-language.js +41 -0
  414. package/dist/tui/task-labels.d.ts +2 -0
  415. package/dist/tui/task-labels.js +7 -0
  416. package/dist/tui/term-bg.d.ts +4 -0
  417. package/dist/tui/term-bg.js +67 -0
  418. package/dist/tui/terminal-screen.d.ts +12 -0
  419. package/dist/tui/terminal-screen.js +1 -0
  420. package/dist/tui/terminal-text.d.ts +7 -0
  421. package/dist/tui/terminal-text.js +123 -0
  422. package/dist/tui/theme.d.ts +25 -0
  423. package/dist/tui/theme.js +383 -0
  424. package/dist/tui/tool-style.d.ts +7 -0
  425. package/dist/tui/tool-style.js +119 -0
  426. package/dist/tui/update-prompt.d.ts +14 -0
  427. package/dist/tui/update-prompt.js +104 -0
  428. package/dist/tui/usage-footer.d.ts +9 -0
  429. package/dist/tui/usage-footer.js +123 -0
  430. package/dist/tui/vascend-login.d.ts +68 -0
  431. package/dist/tui/vascend-login.js +355 -0
  432. package/dist/tui/web-login.d.ts +28 -0
  433. package/dist/tui/web-login.js +115 -0
  434. package/dist/vascend/activity-status.d.ts +12 -0
  435. package/dist/vascend/activity-status.js +68 -0
  436. package/dist/vascend/architecture-map.d.ts +73 -0
  437. package/dist/vascend/architecture-map.js +424 -0
  438. package/dist/vascend/attention-layout.d.ts +19 -0
  439. package/dist/vascend/attention-layout.js +141 -0
  440. package/dist/vascend/budget-allocator.d.ts +31 -0
  441. package/dist/vascend/budget-allocator.js +79 -0
  442. package/dist/vascend/candidate-collector.d.ts +20 -0
  443. package/dist/vascend/candidate-collector.js +220 -0
  444. package/dist/vascend/code-events.d.ts +24 -0
  445. package/dist/vascend/code-events.js +304 -0
  446. package/dist/vascend/code-graph-memory.d.ts +38 -0
  447. package/dist/vascend/code-graph-memory.js +334 -0
  448. package/dist/vascend/code-graph-runtime.d.ts +19 -0
  449. package/dist/vascend/code-graph-runtime.js +190 -0
  450. package/dist/vascend/code-graph-types.d.ts +58 -0
  451. package/dist/vascend/code-graph-types.js +50 -0
  452. package/dist/vascend/code-parser.d.ts +4 -0
  453. package/dist/vascend/code-parser.js +257 -0
  454. package/dist/vascend/competitor-knowledge.d.ts +53 -0
  455. package/dist/vascend/competitor-knowledge.js +262 -0
  456. package/dist/vascend/completeness.d.ts +16 -0
  457. package/dist/vascend/completeness.js +90 -0
  458. package/dist/vascend/context-autotuner.d.ts +45 -0
  459. package/dist/vascend/context-autotuner.js +231 -0
  460. package/dist/vascend/context-candidate.d.ts +45 -0
  461. package/dist/vascend/context-candidate.js +150 -0
  462. package/dist/vascend/context-capsule.d.ts +51 -0
  463. package/dist/vascend/context-capsule.js +133 -0
  464. package/dist/vascend/context-fabric.d.ts +108 -0
  465. package/dist/vascend/context-fabric.js +480 -0
  466. package/dist/vascend/context-feedback-loop.d.ts +56 -0
  467. package/dist/vascend/context-feedback-loop.js +204 -0
  468. package/dist/vascend/context-learning-identity.d.ts +4 -0
  469. package/dist/vascend/context-learning-identity.js +8 -0
  470. package/dist/vascend/context-links.d.ts +14 -0
  471. package/dist/vascend/context-links.js +83 -0
  472. package/dist/vascend/context-lint.d.ts +22 -0
  473. package/dist/vascend/context-lint.js +91 -0
  474. package/dist/vascend/context-market-selection.d.ts +17 -0
  475. package/dist/vascend/context-market-selection.js +50 -0
  476. package/dist/vascend/context-metrics.d.ts +25 -0
  477. package/dist/vascend/context-metrics.js +57 -0
  478. package/dist/vascend/context-pack-render.d.ts +16 -0
  479. package/dist/vascend/context-pack-render.js +52 -0
  480. package/dist/vascend/context-pareto.d.ts +33 -0
  481. package/dist/vascend/context-pareto.js +165 -0
  482. package/dist/vascend/context-pattern-explorer.d.ts +41 -0
  483. package/dist/vascend/context-pattern-explorer.js +276 -0
  484. package/dist/vascend/context-regret.d.ts +32 -0
  485. package/dist/vascend/context-regret.js +93 -0
  486. package/dist/vascend/context-retriever.d.ts +22 -0
  487. package/dist/vascend/context-retriever.js +243 -0
  488. package/dist/vascend/context-scope-index.d.ts +5 -0
  489. package/dist/vascend/context-scope-index.js +96 -0
  490. package/dist/vascend/context-scorer.d.ts +35 -0
  491. package/dist/vascend/context-scorer.js +162 -0
  492. package/dist/vascend/context-search-index.d.ts +12 -0
  493. package/dist/vascend/context-search-index.js +25 -0
  494. package/dist/vascend/context-snapshot.d.ts +22 -0
  495. package/dist/vascend/context-snapshot.js +135 -0
  496. package/dist/vascend/context-window-compiler.d.ts +42 -0
  497. package/dist/vascend/context-window-compiler.js +237 -0
  498. package/dist/vascend/contract-memory.d.ts +61 -0
  499. package/dist/vascend/contract-memory.js +435 -0
  500. package/dist/vascend/council.d.ts +58 -0
  501. package/dist/vascend/council.js +337 -0
  502. package/dist/vascend/decision-promotion.d.ts +48 -0
  503. package/dist/vascend/decision-promotion.js +410 -0
  504. package/dist/vascend/delta-context.d.ts +12 -0
  505. package/dist/vascend/delta-context.js +36 -0
  506. package/dist/vascend/dev-context-compiler.d.ts +27 -0
  507. package/dist/vascend/dev-context-compiler.js +133 -0
  508. package/dist/vascend/dev-context-layout.d.ts +30 -0
  509. package/dist/vascend/dev-context-layout.js +262 -0
  510. package/dist/vascend/evidence-slots.d.ts +14 -0
  511. package/dist/vascend/evidence-slots.js +55 -0
  512. package/dist/vascend/expansion-levels.d.ts +15 -0
  513. package/dist/vascend/expansion-levels.js +178 -0
  514. package/dist/vascend/file-lock.d.ts +7 -0
  515. package/dist/vascend/file-lock.js +95 -0
  516. package/dist/vascend/granular-kingdom.d.ts +25 -0
  517. package/dist/vascend/granular-kingdom.js +259 -0
  518. package/dist/vascend/granular-launcher.d.ts +48 -0
  519. package/dist/vascend/granular-launcher.js +202 -0
  520. package/dist/vascend/kingdom-bus.d.ts +25 -0
  521. package/dist/vascend/kingdom-bus.js +54 -0
  522. package/dist/vascend/kingdom-channel.d.ts +22 -0
  523. package/dist/vascend/kingdom-channel.js +180 -0
  524. package/dist/vascend/kingdom-kpi.d.ts +48 -0
  525. package/dist/vascend/kingdom-kpi.js +172 -0
  526. package/dist/vascend/kingdom-live.d.ts +28 -0
  527. package/dist/vascend/kingdom-live.js +61 -0
  528. package/dist/vascend/kingdom-run.d.ts +18 -0
  529. package/dist/vascend/kingdom-run.js +506 -0
  530. package/dist/vascend/kingdom-scribe.d.ts +20 -0
  531. package/dist/vascend/kingdom-scribe.js +52 -0
  532. package/dist/vascend/kingdom.d.ts +79 -0
  533. package/dist/vascend/kingdom.js +870 -0
  534. package/dist/vascend/lexical-index.d.ts +10 -0
  535. package/dist/vascend/lexical-index.js +99 -0
  536. package/dist/vascend/lexical-search.d.ts +29 -0
  537. package/dist/vascend/lexical-search.js +262 -0
  538. package/dist/vascend/long-term-context.d.ts +51 -0
  539. package/dist/vascend/long-term-context.js +194 -0
  540. package/dist/vascend/mandate.d.ts +33 -0
  541. package/dist/vascend/mandate.js +107 -0
  542. package/dist/vascend/memory-packet.d.ts +13 -0
  543. package/dist/vascend/memory-packet.js +155 -0
  544. package/dist/vascend/memory-status.d.ts +22 -0
  545. package/dist/vascend/memory-status.js +73 -0
  546. package/dist/vascend/memory-store.d.ts +62 -0
  547. package/dist/vascend/memory-store.js +273 -0
  548. package/dist/vascend/objective-revision.d.ts +69 -0
  549. package/dist/vascend/objective-revision.js +319 -0
  550. package/dist/vascend/patch-plan-store.d.ts +33 -0
  551. package/dist/vascend/patch-plan-store.js +240 -0
  552. package/dist/vascend/patch-plan.d.ts +97 -0
  553. package/dist/vascend/patch-plan.js +345 -0
  554. package/dist/vascend/plan-progress.d.ts +11 -0
  555. package/dist/vascend/plan-progress.js +68 -0
  556. package/dist/vascend/plan-store.d.ts +64 -0
  557. package/dist/vascend/plan-store.js +460 -0
  558. package/dist/vascend/proof-chains.d.ts +19 -0
  559. package/dist/vascend/proof-chains.js +120 -0
  560. package/dist/vascend/ramification-store.d.ts +25 -0
  561. package/dist/vascend/ramification-store.js +94 -0
  562. package/dist/vascend/ramification.d.ts +193 -0
  563. package/dist/vascend/ramification.js +583 -0
  564. package/dist/vascend/recall-planner.d.ts +17 -0
  565. package/dist/vascend/recall-planner.js +109 -0
  566. package/dist/vascend/requirements.d.ts +31 -0
  567. package/dist/vascend/requirements.js +151 -0
  568. package/dist/vascend/reviewer.d.ts +9 -0
  569. package/dist/vascend/reviewer.js +93 -0
  570. package/dist/vascend/room-graph.d.ts +29 -0
  571. package/dist/vascend/room-graph.js +242 -0
  572. package/dist/vascend/room-memory.d.ts +57 -0
  573. package/dist/vascend/room-memory.js +631 -0
  574. package/dist/vascend/runtime.d.ts +84 -0
  575. package/dist/vascend/runtime.js +286 -0
  576. package/dist/vascend/self-improve.d.ts +104 -0
  577. package/dist/vascend/self-improve.js +396 -0
  578. package/dist/vascend/session-search.d.ts +52 -0
  579. package/dist/vascend/session-search.js +349 -0
  580. package/dist/vascend/supersession-graph.d.ts +14 -0
  581. package/dist/vascend/supersession-graph.js +70 -0
  582. package/dist/vascend/team-areas.d.ts +9 -0
  583. package/dist/vascend/team-areas.js +28 -0
  584. package/dist/vascend/team-handoff.d.ts +24 -0
  585. package/dist/vascend/team-handoff.js +37 -0
  586. package/dist/vascend/team-scheduler.d.ts +13 -0
  587. package/dist/vascend/team-scheduler.js +74 -0
  588. package/dist/vascend/team-session.d.ts +42 -0
  589. package/dist/vascend/team-session.js +142 -0
  590. package/dist/vascend/test-failure-memory.d.ts +76 -0
  591. package/dist/vascend/test-failure-memory.js +354 -0
  592. package/dist/vascend/token-market.d.ts +42 -0
  593. package/dist/vascend/token-market.js +144 -0
  594. package/dist/vascend/tuner-baseline-store.d.ts +52 -0
  595. package/dist/vascend/tuner-baseline-store.js +446 -0
  596. package/dist/vascend/user-profile.d.ts +44 -0
  597. package/dist/vascend/user-profile.js +131 -0
  598. package/dist/vascend/vision.d.ts +94 -0
  599. package/dist/vascend/vision.js +375 -0
  600. package/dist/vascend/work-agent.d.ts +12 -0
  601. package/dist/vascend/work-agent.js +83 -0
  602. package/dist/vascend/work-queue.d.ts +62 -0
  603. package/dist/vascend/work-queue.js +255 -0
  604. package/dist/vascend/work-runner.d.ts +64 -0
  605. package/dist/vascend/work-runner.js +137 -0
  606. package/dist/vascend/worktree.d.ts +38 -0
  607. package/dist/vascend/worktree.js +206 -0
  608. package/dist/vendor/computer-use/README.md +7 -0
  609. package/dist/vendor/computer-use/computer-desktop.js +211 -0
  610. package/dist/vendor/computer-use/computer-observe.js +315 -0
  611. package/dist/vendor/computer-use/computer-scripts.js +1230 -0
  612. package/dist/vendor/computer-use/computer-tools.js +2136 -0
  613. package/dist/vendor/computer-use/package.json +1 -0
  614. package/dist/vendor/computer-use/rac/atomic-input.js +91 -0
  615. package/dist/vendor/computer-use/rac/grammatica.js +101 -0
  616. package/dist/vendor/computer-use/rac/parser.js +185 -0
  617. package/dist/vendor/computer-use/rac/protocol.js +136 -0
  618. package/install.ps1 +30 -0
  619. package/install.sh +50 -0
  620. package/package.json +56 -0
@@ -0,0 +1,2494 @@
1
+ import { outputArtifact } from "./output-artifact.js";
2
+ import { spawn } from "node:child_process";
3
+ import { open, readFile, writeFile, readdir, stat, mkdir, realpath, lstat, readlink } from "node:fs/promises";
4
+ import { existsSync } from "node:fs";
5
+ import { homedir } from "node:os";
6
+ import { basename, join, dirname, resolve, relative, isAbsolute, sep } from "node:path";
7
+ import { invokeToolCall } from "../core/tool-executor.js";
8
+ import { formatSessionHits, searchSessions } from "../vascend/session-search.js";
9
+ import { formatUserProfile, forgetUserFact, readUserProfile, rememberUserFact } from "../vascend/user-profile.js";
10
+ import { formatQueue, WorkQueue } from "../vascend/work-queue.js";
11
+ import { listMemoryScopes, MemoryStore } from "../vascend/memory-store.js";
12
+ import { RoomMemory } from "../vascend/room-memory.js";
13
+ import { DecisionStore, decisionToRoomRecord } from "../vascend/decision-promotion.js";
14
+ import { ContractStore, contractToRoomRecord } from "../vascend/contract-memory.js";
15
+ import { VascendPlanStore } from "../vascend/plan-store.js";
16
+ import { discoverSkills, findSkill } from "../skills/skills.js";
17
+ import { backgroundManager, terminateChildProcess } from "../core/background.js";
18
+ import { pageOf, paginate, pageStore } from "./pagination.js";
19
+ import { missingParamError, normalizeToolInput } from "./normalize-input.js";
20
+ import { teamTools } from "./team-tools.js";
21
+ import { ramificationTools } from "./ramification-tools.js";
22
+ import { kingdomBus } from "../vascend/kingdom-bus.js";
23
+ import { resolveKingdomBus } from "../vascend/kingdom-channel.js";
24
+ import { knowledgeBase } from "../vascend/competitor-knowledge.js";
25
+ import { renderArchitectureMap, scanArchitectureMap } from "../vascend/architecture-map.js";
26
+ import { compileSearchPattern } from "./regex-compat.js";
27
+ import { toolInputSchema } from "../mcp/protocol.js";
28
+ import { LEGACY_DISCOVERY_TOOL_NAMES, toolInventory, toolListTool } from "./discovery.js";
29
+ import { BUILTIN_CAPABILITIES, BUILTIN_CONTINUATIONS } from "./capabilities.js";
30
+ import { runProgramTool } from "./program.js";
31
+ import { shellEnvironment } from "./shell-env.js";
32
+ import { workspaceSessionsTool } from "./workspace-sessions.js";
33
+ import { vascendStatusTool } from "./plan-status.js";
34
+ import { askUserTool } from "./ask-user.js";
35
+ export { askUserTool } from "./ask-user.js";
36
+ import { hasTaskEvidence, isConcreteTaskEvidence, planBlockers, sameCastlePlan, taskTitles } from "../vascend/plan-progress.js";
37
+ import { fileActor, formatFileConflicts } from "../core/file-coordination.js";
38
+ // Shell per il tool bash. Su Windows `exec` userebbe cmd.exe, dove la sintassi
39
+ // POSIX (find, sed, for f in, pipe) fallisce: se c'e' Git Bash, lo preferiamo.
40
+ // Override esplicito con TALOS_SHELL.
41
+ function detectBashShell() {
42
+ if (process.platform !== "win32")
43
+ return undefined; // /bin/sh va bene
44
+ const override = (process.env["TALOS_SHELL"] ?? "").trim();
45
+ if (override)
46
+ return override;
47
+ const candidates = [
48
+ "C:/Program Files/Git/bin/bash.exe",
49
+ "C:/Program Files/Git/usr/bin/bash.exe",
50
+ "C:/Program Files (x86)/Git/bin/bash.exe",
51
+ `${process.env["LOCALAPPDATA"] ?? ""}/Programs/Git/bin/bash.exe`,
52
+ ];
53
+ for (const c of candidates) {
54
+ if (c && existsSync(c))
55
+ return c;
56
+ }
57
+ return undefined; // fallback: cmd.exe (POSIX non garantito)
58
+ }
59
+ const BASH_SHELL = detectBashShell();
60
+ // Timeout dei comandi bash (ms): evita che un comando lento (es. find su
61
+ // node_modules) blocchi la sessione per minuti. Override con TALOS_BASH_TIMEOUT.
62
+ function bashTimeout() {
63
+ const n = Number(process.env["TALOS_BASH_TIMEOUT"]);
64
+ return Number.isFinite(n) && n > 0 ? n : 120_000;
65
+ }
66
+ // Compatibilita' CLI: radici globali usate soltanto quando il chiamante non
67
+ // fornisce la policy per-turn `ToolContext.fsRoots`.
68
+ function extraRoots() {
69
+ return (process.env["TALOS_EXTRA_ROOTS"] ?? "")
70
+ .split(/[,;]/)
71
+ .map((s) => s.trim())
72
+ .filter(Boolean)
73
+ .map((r) => resolve(r));
74
+ }
75
+ // Normalizza i path in stile Git-Bash/MSYS (/c/Users/...) verso Windows
76
+ // (C:/Users/...). Node fs interpreterebbe /c/... come C:\c\... -> ENOENT.
77
+ function normalizeFsPath(p) {
78
+ if (process.platform !== "win32")
79
+ return p;
80
+ const m = /^\/([a-zA-Z])\/(.*)$/.exec(p);
81
+ return m ? `${m[1].toUpperCase()}:/${m[2]}` : p;
82
+ }
83
+ function expandHomePath(raw) {
84
+ if (raw === "~")
85
+ return homedir();
86
+ if (raw.startsWith("~/") || raw.startsWith("~\\"))
87
+ return join(homedir(), raw.slice(2));
88
+ return raw;
89
+ }
90
+ /**
91
+ * Canonicalizza anche un path ancora da creare. Risolve col filesystem il suo
92
+ * antenato esistente piu' vicino e riappende la coda mancante: in questo modo
93
+ * un symlink intermedio non puo' trasformare una scrittura apparentemente
94
+ * interna in una scrittura fuori root.
95
+ */
96
+ async function canonicalPath(abs, seenLinks = new Set()) {
97
+ let cursor = resolve(abs);
98
+ const missing = [];
99
+ for (;;) {
100
+ let info;
101
+ try {
102
+ info = await lstat(cursor);
103
+ }
104
+ catch (cause) {
105
+ const code = cause.code;
106
+ if (code !== "ENOENT" && code !== "ENOTDIR")
107
+ throw cause;
108
+ const parent = dirname(cursor);
109
+ if (parent === cursor)
110
+ throw cause;
111
+ missing.push(basename(cursor));
112
+ cursor = parent;
113
+ continue;
114
+ }
115
+ if (info.isSymbolicLink()) {
116
+ if (seenLinks.has(cursor))
117
+ throw new Error(`ciclo di symlink nel percorso: ${cursor}`);
118
+ seenLinks.add(cursor);
119
+ const target = await readlink(cursor);
120
+ return canonicalPath(resolve(dirname(cursor), target, ...missing.reverse()), seenLinks);
121
+ }
122
+ const existing = await realpath(cursor);
123
+ return resolve(existing, ...missing.reverse());
124
+ }
125
+ }
126
+ function isWithin(root, target) {
127
+ const rel = relative(root, target);
128
+ return rel === "" || (!isAbsolute(rel) && rel !== ".." && !rel.startsWith(`..${sep}`));
129
+ }
130
+ // Se fsRoots e' definito, la policy e' interamente per-turn e non dipende da
131
+ // process.env. L'assenza mantiene il comportamento storico della CLI.
132
+ async function safePath(cwd, raw, fsRoots) {
133
+ const p = normalizeFsPath(expandHomePath(raw));
134
+ const abs = resolve(isAbsolute(p) ? p : resolve(cwd, p));
135
+ const perTurn = fsRoots !== undefined;
136
+ if (!perTurn && process.env["TALOS_CONFINE_FS"] !== "1")
137
+ return abs;
138
+ const rawRoots = perTurn ? [cwd, ...fsRoots] : [cwd, ...extraRoots()];
139
+ const [target, ...roots] = await Promise.all([
140
+ canonicalPath(abs),
141
+ ...rawRoots.map((root) => canonicalPath(resolve(cwd, normalizeFsPath(expandHomePath(root))))),
142
+ ]);
143
+ if (roots.some((root) => isWithin(root, target)))
144
+ return abs;
145
+ const hint = perTurn
146
+ ? "La cartella deve essere autorizzata dall'host per questo turno."
147
+ : "Confinamento CLI attivo: configura TALOS_EXTRA_ROOTS o disattiva TALOS_CONFINE_FS.";
148
+ throw new Error(`percorso fuori dalle radici consentite: ${p}. ${hint}`);
149
+ }
150
+ function toolError(output) {
151
+ return { output, isError: true };
152
+ }
153
+ const MAX_OUTPUT = 30_000;
154
+ function clamp(s) {
155
+ return s.length > MAX_OUTPUT ? s.slice(0, MAX_OUTPUT) + "\n...[troncato]" : s;
156
+ }
157
+ // Risolve shell + argomenti per spawn (Git Bash su Windows se presente, per la
158
+ // sintassi POSIX; altrimenti cmd.exe; /bin/sh altrove).
159
+ function resolveShell(command, requested) {
160
+ const isWin = process.platform === "win32";
161
+ if (requested === "powershell")
162
+ return { shell: isWin ? "powershell.exe" : "pwsh", args: ["-NoProfile", "-NonInteractive", "-Command", command] };
163
+ if (requested === "cmd" && isWin)
164
+ return { shell: process.env["ComSpec"] || "cmd.exe", args: ["/d", "/s", "/c", command] };
165
+ if (requested === "sh")
166
+ return { shell: "sh", args: ["-c", command] };
167
+ if (requested === "bash")
168
+ return { shell: BASH_SHELL || "bash", args: ["-c", command] };
169
+ if (BASH_SHELL)
170
+ return { shell: BASH_SHELL, args: ["-c", command] };
171
+ if (isWin)
172
+ return { shell: process.env["ComSpec"] || "cmd.exe", args: ["/d", "/s", "/c", command] };
173
+ return { shell: "/bin/sh", args: ["-c", command] };
174
+ }
175
+ export const bashTool = {
176
+ name: "bash",
177
+ description: "Esegue comandi nel workspace, inclusi rg, rg --files, git diff e letture mirate. Richiede conferma dell'utente. " +
178
+ `Shell predefinita: ${BASH_SHELL ? "bash" : process.platform === "win32" ? "cmd" : "sh"}; usa shell per sceglierne un'altra. ` +
179
+ "Per controlli brevi preferisci il primo piano. Con background=true attende fino a 1 secondo: " +
180
+ "se il comando finisce restituisce l'esito, altrimenti ricevi un ID e puoi " +
181
+ "leggere stato/output o interromperli con bash_output. Per un comando in primo piano " +
182
+ "puoi impostare timeout_seconds. L'utente puo' anche premere Ctrl+B per spostarlo in background.",
183
+ schema: {
184
+ type: "object",
185
+ properties: {
186
+ command: { type: "string", description: "Comando da eseguire" },
187
+ shell: { type: "string", enum: ["auto", "bash", "powershell", "cmd", "sh"], description: "Shell da usare; default auto. Usa sintassi e quoting della shell scelta." },
188
+ background: {
189
+ type: "boolean",
190
+ description: "Restituisce l'esito se pronto entro yield_time_ms, altrimenti un ID da seguire con bash_output",
191
+ },
192
+ yield_time_ms: {
193
+ type: "integer", minimum: 0, maximum: 10000,
194
+ description: "Attesa iniziale con background=true: default 1000 ms; 0 restituisce subito, utile per server persistenti",
195
+ },
196
+ timeout_seconds: {
197
+ type: "number",
198
+ minimum: 1,
199
+ maximum: 3600,
200
+ description: "Timeout del comando in primo piano; default configurato dall'ambiente",
201
+ },
202
+ },
203
+ required: ["command"],
204
+ },
205
+ handler: async (input, ctx) => {
206
+ ctx.signal.throwIfAborted();
207
+ const command = String(input["command"] ?? "").trim();
208
+ if (!command)
209
+ return toolError(missingParamError("command", input, '{"command":"npm test"}'));
210
+ const shell = String(input["shell"] ?? "auto");
211
+ if (!["auto", "bash", "powershell", "cmd", "sh"].includes(shell) || shell === "cmd" && process.platform !== "win32") {
212
+ return toolError("shell non disponibile o non valida: scegli auto, bash, powershell, sh (cmd solo su Windows)");
213
+ }
214
+ // Lavorando in squadra i collaboratori condividono la STESSA working dir/repo: se
215
+ // ognuno fa git che muta (add/commit/push/reset/checkout/stash...) in
216
+ // parallelo si sovrascrivono a vicenda (index.lock, "changes would be
217
+ // overwritten", un git add -A stagia il lavoro a meta' di un altro) e alla
218
+ // fine non resta nulla. I collaboratori MODIFICANO solo i file; il commit/push
219
+ // avviene una sola volta alla fine, fuori dallo swarm.
220
+ // Solo in modalita' CONDIVISA (collaboratore senza worktree proprio). In modalita'
221
+ // ISOLATA (ctx.kingdom.isolated) il collaboratore ha un worktree dedicato e usa
222
+ // git liberamente: i commit stanno sul suo ramo, niente race.
223
+ if (ctx.kingdom && !ctx.kingdom.isolated && mutatesGit(command)) {
224
+ return toolError("BLOCCATO mentre lavori in squadra (working dir condivisa): niente git che modifica (add/commit/push/" +
225
+ "pull/merge/rebase/reset/checkout/switch/restore/stash/clean/rm). I collaboratori condividono la " +
226
+ "stessa working dir e si sovrascriverebbero a vicenda. Modifica SOLO i file del tuo incarico: le " +
227
+ "modifiche restano nel working tree e verranno committate/pushate UNA volta alla fine. " +
228
+ "(git status/diff/log/show sono ok.)");
229
+ }
230
+ const ok = await ctx.confirm("bash", command);
231
+ ctx.signal.throwIfAborted();
232
+ if (!ok)
233
+ return toolError("comando rifiutato dall'utente");
234
+ const requestedTimeout = Number(input["timeout_seconds"]);
235
+ const timeoutMs = Number.isFinite(requestedTimeout) && requestedTimeout > 0
236
+ ? Math.min(3_600_000, Math.max(1_000, Math.round(requestedTimeout * 1000)))
237
+ : bashTimeout();
238
+ return runBashSpawn(command, ctx, {
239
+ background: input["background"] === true,
240
+ shell,
241
+ yieldMs: typeof input["yield_time_ms"] === "number" && Number.isFinite(input["yield_time_ms"])
242
+ ? Math.max(0, Math.min(10_000, Math.floor(input["yield_time_ms"]))) : 1000,
243
+ timeoutMs,
244
+ });
245
+ },
246
+ };
247
+ // True se il comando esegue un'operazione git che MUTA l'indice/working tree/
248
+ // remoto (da vietare ai collaboratori). Le operazioni di sola lettura
249
+ // (status/diff/log/show/blame/ls-files/rev-parse/fetch) restano permesse.
250
+ function mutatesGit(command) {
251
+ return /\bgit\s+(?:-[^\s]+\s+|--[^\s]+\s+)*(add|commit|push|pull|merge|rebase|reset|checkout|switch|restore|stash|clean|cherry-pick|revert|rm|mv|am|apply|tag|gc|prune)\b/i.test(command);
252
+ }
253
+ function runBashSpawn(command, ctx, options) {
254
+ ctx.signal.throwIfAborted();
255
+ const mgr = backgroundManager;
256
+ const task = mgr.createTask(command);
257
+ const { shell, args } = resolveShell(command, options.shell);
258
+ const timeoutMs = options.timeoutMs;
259
+ return new Promise((resolvePromise) => {
260
+ let settled = false;
261
+ let yieldTimer;
262
+ const child = spawn(shell, args, { cwd: ctx.cwd, windowsHide: true, env: shellEnvironment() });
263
+ mgr.setChild(task.id, child);
264
+ const onAbort = () => {
265
+ terminateChildProcess(child);
266
+ mgr.finishTask(task.id, "killed");
267
+ finish("comando interrotto", true);
268
+ };
269
+ const timer = setTimeout(() => {
270
+ terminateChildProcess(child);
271
+ mgr.finishTask(task.id, "killed");
272
+ finish(`errore: comando interrotto dopo ${Math.round(timeoutMs / 1000)}s (timeout). ` +
273
+ `Per un comando lungo, lascialo girare e premi Ctrl+B per metterlo in background.`, true);
274
+ }, timeoutMs);
275
+ const cleanup = () => {
276
+ clearTimeout(timer);
277
+ if (yieldTimer)
278
+ clearTimeout(yieldTimer);
279
+ ctx.signal.removeEventListener("abort", onAbort);
280
+ };
281
+ const finish = (msg, isError = false) => {
282
+ if (settled)
283
+ return;
284
+ settled = true;
285
+ cleanup();
286
+ mgr.clearForeground(task.id);
287
+ resolvePromise(isError ? toolError(msg) : msg);
288
+ };
289
+ child.stdout?.on("data", (d) => mgr.appendOutput(task.id, d.toString("utf8")));
290
+ child.stderr?.on("data", (d) => mgr.appendOutput(task.id, d.toString("utf8")));
291
+ child.on("error", (err) => {
292
+ mgr.finishTask(task.id, "failed");
293
+ finish(`errore esecuzione: ${err.message}`, true);
294
+ });
295
+ child.on("close", (code, sig) => {
296
+ const status = sig ? "killed" : code === 0 ? "done" : "failed";
297
+ mgr.finishTask(task.id, status, code ?? undefined);
298
+ const t = mgr.get(task.id);
299
+ const body = paginate(t?.output ?? "", "output bash", ctx);
300
+ finish(body || (status === "done" ? "(nessun output)" : `(uscita ${code ?? sig})`), status !== "done");
301
+ });
302
+ // Foreground: Ctrl+B lo stacca (continua a girare in background).
303
+ const detach = () => {
304
+ if (settled)
305
+ return;
306
+ mgr.markDetached(task.id);
307
+ cleanup(); // niente piu' timeout/abort del foreground sul task staccato
308
+ finish(`comando in background [${task.id}]: ${command.length > 60 ? command.slice(0, 57) + "..." : command}\n` +
309
+ `Continua a girare. Leggi l'output con bash_output id="${task.id}".`);
310
+ };
311
+ mgr.setForeground({ taskId: task.id, background: detach });
312
+ // Un agente non puo' decidere di interrompere un tool mentre ne attende il
313
+ // risultato. Una breve attesa evita un secondo giro per i comandi rapidi;
314
+ // oltre il limite bash_output permette di osservare e terminare il task.
315
+ if (options.background) {
316
+ if (options.yieldMs === 0) {
317
+ detach();
318
+ return;
319
+ }
320
+ yieldTimer = setTimeout(detach, options.yieldMs);
321
+ }
322
+ if (ctx.signal.aborted)
323
+ onAbort();
324
+ else
325
+ ctx.signal.addEventListener("abort", onAbort, { once: true });
326
+ });
327
+ }
328
+ // bash_output: canale di controllo dei comandi che l'agente o l'utente hanno
329
+ // mandato in background.
330
+ export const bashOutputTool = {
331
+ name: "bash_output",
332
+ description: "Monitora e gestisce un comando shell in background. Passa `id` (es. \"bg1\") per " +
333
+ "leggerne stato e output; senza id elenca i task. `kill: true` interrompe il processo.",
334
+ schema: {
335
+ type: "object",
336
+ properties: {
337
+ id: { type: "string", description: "id del task (es. bg1)" },
338
+ kill: { type: "boolean", description: "se true termina il task" },
339
+ },
340
+ },
341
+ handler: async (input, ctx) => {
342
+ ctx.signal.throwIfAborted();
343
+ const mgr = backgroundManager;
344
+ const id = String(input["id"] ?? "").trim();
345
+ if (!id) {
346
+ const list = mgr.list();
347
+ if (!list.length)
348
+ return "nessun task in background";
349
+ return list.map((t) => `${t.id} [${t.status}] ${t.command}`).join("\n");
350
+ }
351
+ const t = mgr.get(id);
352
+ if (!t)
353
+ return toolError(`task non trovato: ${id}`);
354
+ if (input["kill"] === true)
355
+ return mgr.kill(id) ? `terminato ${id}` : `${id} non e' piu' in esecuzione`;
356
+ const head = `${t.id} [${t.status}${t.exitCode != null ? ` · uscita ${t.exitCode}` : ""}] ${t.command}`;
357
+ const output = `${head}\n${clamp(t.output) || "(nessun output)"}`;
358
+ return t.status === "failed" || t.status === "killed" ? toolError(output) : output;
359
+ },
360
+ };
361
+ // Oltre questa taglia un file non viene MAI letto integrale: leggere e
362
+ // splittare decine di MB dentro il main di Electron e' la stessa classe di
363
+ // saturazione della grep larga (ghost "non risponde"). Si legge a finestre.
364
+ const READ_FINESTRA_BYTES = 2_000_000;
365
+ // Tetto di byte scanditi per raggiungere un offset profondo: oltre, ci si
366
+ // ferma e lo si dichiara (il rimedio e' grep per localizzare la riga).
367
+ const READ_SCAN_MAX_BYTES = 16_000_000;
368
+ const READ_CHUNK_BYTES = 262_144;
369
+ // Un read senza limit su un file grande ritorna comunque una finestra finita.
370
+ const READ_RIGHE_DEFAULT_GRANDI = 1_000;
371
+ // Lettura incrementale per righe di un file grande: chunk da 256KB, decoder
372
+ // streaming (i confini multi-byte non si spezzano), respiro periodico per
373
+ // l'event loop, stop appena la finestra richiesta e' completa.
374
+ async function leggiFinestra(abs, rel, taglia, offset, limit, ctx) {
375
+ const inizio = Math.max(0, offset - 1);
376
+ const quante = limit > 0 ? limit : READ_RIGHE_DEFAULT_GRANDI;
377
+ const fine = inizio + quante;
378
+ const decoder = new TextDecoder("utf-8");
379
+ const finestra = [];
380
+ let resto = "";
381
+ let riga = 0; // indice 0-based della prossima riga completa
382
+ let letti = 0;
383
+ let fermatoAlTetto = false;
384
+ let completa = false;
385
+ const fh = await open(abs, "r");
386
+ try {
387
+ const buffer = Buffer.alloc(READ_CHUNK_BYTES);
388
+ let fiato = Date.now();
389
+ while (!completa) {
390
+ const { bytesRead } = await fh.read(buffer, 0, READ_CHUNK_BYTES, letti);
391
+ if (bytesRead <= 0)
392
+ break;
393
+ letti += bytesRead;
394
+ const testo = resto + decoder.decode(buffer.subarray(0, bytesRead), { stream: true });
395
+ if (testo.includes("\u0000"))
396
+ throw new Error(`${rel} contiene byte NUL (binario): read_file lavora solo su testo`);
397
+ const pezzi = testo.split("\n");
398
+ resto = pezzi.pop() ?? "";
399
+ for (const pezzo of pezzi) {
400
+ if (riga >= inizio && riga < fine)
401
+ finestra.push(pezzo);
402
+ riga += 1;
403
+ if (riga >= fine) {
404
+ completa = true;
405
+ break;
406
+ }
407
+ }
408
+ if (!completa && letti >= READ_SCAN_MAX_BYTES) {
409
+ fermatoAlTetto = true;
410
+ break;
411
+ }
412
+ if (Date.now() - fiato > BREATH_EVERY_MS) {
413
+ fiato = Date.now();
414
+ await respiro();
415
+ }
416
+ }
417
+ if (!completa && !fermatoAlTetto && resto && riga >= inizio && riga < fine) {
418
+ finestra.push(resto); // ultima riga senza newline finale
419
+ riga += 1;
420
+ }
421
+ }
422
+ finally {
423
+ await fh.close();
424
+ }
425
+ const mb = (taglia / 1_000_000).toFixed(1);
426
+ const da = inizio + 1;
427
+ const a = inizio + finestra.length;
428
+ const nota = fermatoAlTetto
429
+ ? `\n[file grande (${mb} MB): scansione fermata a ${Math.round(READ_SCAN_MAX_BYTES / 1_000_000)} MB prima di raggiungere la riga ${da}. `
430
+ + "Per righe cosi' profonde localizzale prima con grep (path=file) e poi rileggi con offset vicino.]"
431
+ : `\n[file grande (${mb} MB): righe ${da}-${a}${completa ? "" : " (fine file)"}. Naviga con offset/limit; per cercare usa grep.]`;
432
+ if (!finestra.length && !fermatoAlTetto) {
433
+ return `(nessuna riga da ${da}: il file ha ${riga} righe)` + nota;
434
+ }
435
+ return paginate(finestra.join("\n"), `file ${rel}`, ctx) + nota;
436
+ }
437
+ export const readTool = {
438
+ name: "read_file",
439
+ execution: "parallel",
440
+ description: "Legge un file di testo (path libero). Per sbirciarne solo una porzione usa " +
441
+ "`offset` (riga di partenza, 1-based) e `limit` (numero di righe): NON serve bash/head/cat.",
442
+ schema: {
443
+ type: "object",
444
+ properties: {
445
+ path: { type: "string" },
446
+ offset: { type: "number", description: "riga di partenza (1-based), default 1" },
447
+ limit: { type: "number", description: "numero massimo di righe da leggere" },
448
+ },
449
+ required: ["path"],
450
+ },
451
+ handler: async (input, ctx) => {
452
+ try {
453
+ const rel = String(input["path"] ?? "").trim();
454
+ if (!rel)
455
+ return toolError("errore: campo 'path' mancante o vuoto");
456
+ const abs = await safePath(ctx.cwd, rel, ctx.fsRoots);
457
+ const conflicts = ctx.fileCoordination?.hub.observe(abs, ctx.fileCoordination.actor) ?? [];
458
+ if (conflicts.length) {
459
+ ctx.fileCoordination?.emit?.({
460
+ phase: "observed", path: abs, actor: ctx.fileCoordination.actor,
461
+ mode: "viewing", others: conflicts, at: Date.now(),
462
+ });
463
+ }
464
+ const offset = typeof input["offset"] === "number" && input["offset"] > 0 ? Math.floor(input["offset"]) : 1;
465
+ const limit = typeof input["limit"] === "number" && input["limit"] > 0 ? Math.floor(input["limit"]) : 0;
466
+ // I file grandi non si leggono mai integrali: finestra incrementale.
467
+ const taglia = (await stat(abs)).size;
468
+ const notaPresenza = conflicts.length
469
+ ? `\n[coordinamento file: ${formatFileConflicts(conflicts)}. Leggi pure, ma non scrivere finche' la modifica e' attiva.]`
470
+ : "";
471
+ if (taglia > READ_FINESTRA_BYTES)
472
+ return await leggiFinestra(abs, rel, taglia, offset, limit, ctx) + notaPresenza;
473
+ const full = await readFile(abs, "utf8");
474
+ if (offset <= 1 && limit <= 0)
475
+ return paginate(full, `file ${rel}`, ctx) + notaPresenza;
476
+ // Lettura parziale per righe (stile Claude Code): porzione [offset, offset+limit).
477
+ const lines = full.split("\n");
478
+ const start = Math.min(offset - 1, lines.length);
479
+ const slice = limit > 0 ? lines.slice(start, start + limit) : lines.slice(start);
480
+ const note = start + slice.length < lines.length || start > 0
481
+ ? `\n[righe ${start + 1}-${start + slice.length} di ${lines.length}]`
482
+ : "";
483
+ return paginate(slice.join("\n"), `file ${rel}`, ctx) + note + notaPresenza;
484
+ }
485
+ catch (err) {
486
+ return toolError(`errore lettura: ${err.message}`);
487
+ }
488
+ },
489
+ };
490
+ function acquireFileWrite(ctx, abs) {
491
+ const coordination = ctx.fileCoordination;
492
+ if (!coordination)
493
+ return "";
494
+ const outcome = coordination.hub.acquire(abs, coordination.actor);
495
+ if (!outcome.ok) {
496
+ coordination.emit?.({
497
+ phase: "blocked", path: abs, actor: coordination.actor,
498
+ mode: "writing", others: outcome.conflicts, at: Date.now(),
499
+ });
500
+ return `conflitto file: ${formatFileConflicts(outcome.conflicts)}. `
501
+ + "La scrittura non e' partita: coordina il lavoro, attendi la fine della modifica e rileggi il file prima di riprovare.";
502
+ }
503
+ coordination.emit?.({
504
+ phase: "acquired", path: abs, actor: coordination.actor,
505
+ mode: "writing", others: [], at: Date.now(),
506
+ });
507
+ return outcome.lease;
508
+ }
509
+ function validateFileWrite(ctx, lease) {
510
+ if (typeof lease === "string" || !ctx.fileCoordination)
511
+ return null;
512
+ const conflicts = ctx.fileCoordination.hub.validate(lease);
513
+ if (!conflicts.length)
514
+ return null;
515
+ ctx.fileCoordination.emit?.({
516
+ phase: "blocked", path: lease.path, actor: lease.actor,
517
+ mode: "writing", others: conflicts, at: Date.now(),
518
+ });
519
+ return `conflitto file sopraggiunto: ${formatFileConflicts(conflicts)}. `
520
+ + "La scrittura e' stata annullata senza sovrascrivere il lavoro concorrente.";
521
+ }
522
+ function releaseFileWrite(ctx, lease) {
523
+ if (typeof lease === "string" || !ctx.fileCoordination)
524
+ return;
525
+ ctx.fileCoordination.hub.release(lease);
526
+ ctx.fileCoordination.emit?.({
527
+ phase: "released", path: lease.path, actor: lease.actor,
528
+ mode: "writing", others: [], at: Date.now(),
529
+ });
530
+ }
531
+ async function fileVersion(path) {
532
+ try {
533
+ const info = await stat(path);
534
+ return { exists: true, size: info.size, mtimeMs: info.mtimeMs };
535
+ }
536
+ catch (error) {
537
+ if (error.code === "ENOENT")
538
+ return { exists: false, size: 0, mtimeMs: 0 };
539
+ throw error;
540
+ }
541
+ }
542
+ function sameFileVersion(a, b) {
543
+ return a.exists === b.exists && a.size === b.size && a.mtimeMs === b.mtimeMs;
544
+ }
545
+ function diskConflict(ctx, path) {
546
+ const actor = fileActor("external-filesystem", "Un'altra applicazione", "external");
547
+ const presence = { path, mode: "editing", actor, updatedAt: Date.now() };
548
+ ctx.fileCoordination?.emit?.({
549
+ phase: "blocked", path, actor: ctx.fileCoordination.actor,
550
+ mode: "writing", others: [presence], at: Date.now(),
551
+ });
552
+ return "conflitto file: il contenuto sul disco e' cambiato mentre l'operazione attendeva. "
553
+ + "La scrittura e' stata annullata; rileggi il file e integra la versione piu' recente prima di riprovare.";
554
+ }
555
+ // Prevenzione byte NUL: i modelli ogni tanto emettono \u0000 letterali dentro
556
+ // content/old_string/new_string. Un NUL in un file di testo lo fa trattare da
557
+ // binario a valle (grep lo salta, gli edit successivi falliscono "string not
558
+ // found" in modi misteriosi): si rimuove sempre e lo si dichiara nel risultato.
559
+ function senzaByteNul(testo) {
560
+ const pulito = testo.split("\u0000").join("");
561
+ return { testo: pulito, rimossi: testo.length - pulito.length };
562
+ }
563
+ export const writeTool = {
564
+ name: "write_file",
565
+ description: "Scrive un file. Con append=false (default) crea o sovrascrive; con append=true accoda. " +
566
+ "Per file lunghi scrivi in piu' chiamate (prima crea, poi append=true) per evitare il troncamento da limite di output. Richiede conferma.",
567
+ schema: {
568
+ type: "object",
569
+ properties: {
570
+ path: { type: "string" },
571
+ content: { type: "string" },
572
+ append: { type: "boolean", description: "se true accoda invece di sovrascrivere (default false)" },
573
+ },
574
+ required: ["path", "content"],
575
+ },
576
+ handler: async (input, ctx) => {
577
+ try {
578
+ ctx.signal.throwIfAborted();
579
+ const rel = String(input["path"] ?? "").trim();
580
+ if (!rel) {
581
+ return toolError("errore: campo 'path' mancante o vuoto. Se stavi scrivendo un file grande, " +
582
+ "l'input potrebbe essere stato TRONCATO dal limite di output: riscrivilo in piu' chiamate " +
583
+ "write_file piu' corte (la prima crea, le successive con append=true).");
584
+ }
585
+ if (input["content"] === undefined || input["content"] === null) {
586
+ return toolError("errore: campo 'content' mancante (passa il contenuto del file)");
587
+ }
588
+ const abs = await safePath(ctx.cwd, rel, ctx.fsRoots);
589
+ ctx.signal.throwIfAborted();
590
+ const lease = acquireFileWrite(ctx, abs);
591
+ if (typeof lease === "string" && lease)
592
+ return { output: lease, isError: true };
593
+ const append = input["append"] === true;
594
+ try {
595
+ const before = await fileVersion(abs);
596
+ ctx.signal.throwIfAborted();
597
+ const ok = await ctx.confirm("write_file", `${append ? "append " : ""}${rel}`);
598
+ ctx.signal.throwIfAborted();
599
+ if (!ok)
600
+ return toolError("scrittura rifiutata dall'utente");
601
+ const conflict = validateFileWrite(ctx, lease);
602
+ if (conflict)
603
+ return { output: conflict, isError: true };
604
+ if (!sameFileVersion(before, await fileVersion(abs))) {
605
+ return { output: diskConflict(ctx, abs), isError: true };
606
+ }
607
+ ctx.signal.throwIfAborted();
608
+ await mkdir(dirname(abs), { recursive: true });
609
+ ctx.signal.throwIfAborted();
610
+ const { testo: contenuto, rimossi } = senzaByteNul(String(input["content"]));
611
+ await writeFile(abs, contenuto, { encoding: "utf8", flag: append ? "a" : "w" });
612
+ const notaNul = rimossi
613
+ ? `\n[prevenzione: rimossi ${rimossi} byte NUL dal contenuto (mai legittimi in un file di testo)]`
614
+ : "";
615
+ return `${append ? "accodato" : "scritto"}: ${rel}${notaNul}`;
616
+ }
617
+ finally {
618
+ releaseFileWrite(ctx, lease);
619
+ }
620
+ }
621
+ catch (err) {
622
+ return toolError(`errore scrittura: ${err.message}`);
623
+ }
624
+ },
625
+ };
626
+ // Rileva lo stile di fine riga dominante in un testo (CRLF se prevale, altrimenti LF).
627
+ function dominantEol(text) {
628
+ let crlf = 0;
629
+ let lf = 0;
630
+ for (let i = 0; i < text.length; i += 1) {
631
+ if (text[i] === "\n") {
632
+ if (text[i - 1] === "\r")
633
+ crlf += 1;
634
+ else
635
+ lf += 1;
636
+ }
637
+ }
638
+ return crlf > lf ? "\r\n" : "\n";
639
+ }
640
+ // Normalizza tutte le terminazioni di riga a LF (per confronti EOL-insensibili).
641
+ function toLF(text) {
642
+ return text.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
643
+ }
644
+ // Rimuove gli spazi/tab a fine riga (per confronti trailing-ws-insensibili).
645
+ function stripTrailingWs(text) {
646
+ return toLF(text).replace(/[ \t]+(?=\n)/g, "").replace(/[ \t]+$/, "");
647
+ }
648
+ // Costruisce una mappa: indice nella versione LF -> indice nel testo originale.
649
+ function lfIndexToOriginal(orig) {
650
+ const map = [];
651
+ for (let i = 0; i < orig.length; i += 1) {
652
+ if (orig[i] === "\r" && orig[i + 1] === "\n")
653
+ continue;
654
+ map.push(i);
655
+ }
656
+ map.push(orig.length);
657
+ return map;
658
+ }
659
+ // Mappa: indice nella versione stripped (LF + no trailing ws) -> indice originale.
660
+ function strippedIndexToOriginal(orig) {
661
+ const map = [];
662
+ for (let i = 0; i < orig.length; i += 1) {
663
+ const c = orig[i];
664
+ if (c === "\r" && orig[i + 1] === "\n")
665
+ continue;
666
+ if (c === " " || c === "\t") {
667
+ let j = i;
668
+ while (orig[j] === " " || orig[j] === "\t")
669
+ j += 1;
670
+ const atLineEnd = j >= orig.length || orig[j] === "\n" || (orig[j] === "\r" && orig[j + 1] === "\n");
671
+ if (atLineEnd)
672
+ continue;
673
+ }
674
+ map.push(i);
675
+ }
676
+ map.push(orig.length);
677
+ return map;
678
+ }
679
+ // Trova in `cur` l'unica occorrenza di `old` e ritorna il contenuto con `next`
680
+ // al suo posto. Tollerante alle differenze che facevano fallire il match:
681
+ // 1) esatto; 2) CRLF vs LF (il file e' CRLF ma old_string arriva con LF);
682
+ // 3) whitespace a fine riga. L'EOL del file e' PRESERVATO nel risultato.
683
+ export function findEditMatch(cur, old, next) {
684
+ if (old === "")
685
+ return "errore: old_string vuota";
686
+ {
687
+ const count = cur.split(old).length - 1;
688
+ if (count === 1) {
689
+ // NB: niente cur.replace(old, next): String.prototype.replace interpreta
690
+ // i pattern $$, $&, $`, $', $n nella stringa di sostituzione, corrompendo
691
+ // un new_string che contiene '$'. Usiamo slice indicizzato (letterale).
692
+ const idx = cur.indexOf(old);
693
+ const result = cur.slice(0, idx) + next + cur.slice(idx + old.length);
694
+ return { result, via: "exact" };
695
+ }
696
+ if (count > 1)
697
+ return `errore: old_string presente ${count} volte, deve essere unica`;
698
+ }
699
+ const fileEol = dominantEol(cur);
700
+ const nextEolFixed = toLF(next).replace(/\n/g, fileEol);
701
+ {
702
+ const curLF = toLF(cur);
703
+ const oldLF = toLF(old);
704
+ const count = curLF.split(oldLF).length - 1;
705
+ if (count === 1) {
706
+ const idxLF = curLF.indexOf(oldLF);
707
+ const map = lfIndexToOriginal(cur);
708
+ const startOrig = map[idxLF];
709
+ const endOrig = map[idxLF + oldLF.length];
710
+ const result = cur.slice(0, startOrig) + nextEolFixed + cur.slice(endOrig);
711
+ return { result, via: "eol" };
712
+ }
713
+ if (count > 1)
714
+ return `errore: old_string presente ${count} volte (ignorando CRLF/LF), deve essere unica`;
715
+ }
716
+ {
717
+ const curWs = stripTrailingWs(cur);
718
+ const oldWs = stripTrailingWs(old);
719
+ const count = oldWs ? curWs.split(oldWs).length - 1 : 0;
720
+ if (count === 1) {
721
+ const idxWs = curWs.indexOf(oldWs);
722
+ const map = strippedIndexToOriginal(cur);
723
+ const startOrig = map[idxWs];
724
+ const endOrig = map[idxWs + oldWs.length];
725
+ const result = cur.slice(0, startOrig) + nextEolFixed + cur.slice(endOrig);
726
+ return { result, via: "whitespace" };
727
+ }
728
+ if (count > 1)
729
+ return `errore: old_string presente ${count} volte (ignorando spazi a fine riga), deve essere unica`;
730
+ }
731
+ return ("errore: old_string non trovata. Verifica di aver copiato il testo ESATTO dal file " +
732
+ "(inclusa l'indentazione). Questo tool tollera gia' CRLF/LF e spazi a fine riga: " +
733
+ "se ancora non trova, la stringa differisce nel contenuto, non solo negli a-capo.");
734
+ }
735
+ export const editTool = {
736
+ name: "edit_file",
737
+ description: "Sostituisce una stringa esatta in un file. La stringa deve essere unica.",
738
+ schema: {
739
+ type: "object",
740
+ properties: {
741
+ path: { type: "string" },
742
+ old_string: { type: "string" },
743
+ new_string: { type: "string" },
744
+ },
745
+ required: ["path", "old_string", "new_string"],
746
+ },
747
+ handler: async (input, ctx) => {
748
+ try {
749
+ const rel = String(input["path"] ?? "").trim();
750
+ ctx.signal.throwIfAborted();
751
+ if (!rel)
752
+ return toolError("errore: campo 'path' mancante o vuoto");
753
+ if (input["old_string"] == null || input["new_string"] == null) {
754
+ return toolError("errore: old_string e new_string sono obbligatorie");
755
+ }
756
+ const abs = await safePath(ctx.cwd, rel, ctx.fsRoots);
757
+ ctx.signal.throwIfAborted();
758
+ const lease = acquireFileWrite(ctx, abs);
759
+ if (typeof lease === "string" && lease)
760
+ return { output: lease, isError: true };
761
+ try {
762
+ const pulitoOld = senzaByteNul(String(input["old_string"]));
763
+ const pulitoNext = senzaByteNul(String(input["new_string"]));
764
+ const old = pulitoOld.testo;
765
+ const next = pulitoNext.testo;
766
+ const cur = await readFile(abs, "utf8");
767
+ // Un NUL nel file lo rende "binario" per grep e per gli editor: un edit
768
+ // testuale sopra peggiora solo le cose. Errore chiaro con il rimedio.
769
+ if (cur.includes("\u0000")) {
770
+ return toolError("errore: il file contiene byte NUL (binario o corrotto): edit_file lavora solo su testo. "
771
+ + "Risana il file rimuovendo i NUL (o riscrivilo con write_file).");
772
+ }
773
+ const match = findEditMatch(cur, old, next);
774
+ if (typeof match === "string")
775
+ return toolError(match); // messaggio d'errore (no/multi match)
776
+ ctx.signal.throwIfAborted();
777
+ const ok = await ctx.confirm("edit_file", rel);
778
+ ctx.signal.throwIfAborted();
779
+ if (!ok)
780
+ return toolError("modifica rifiutata dall'utente");
781
+ const conflict = validateFileWrite(ctx, lease);
782
+ if (conflict)
783
+ return { output: conflict, isError: true };
784
+ if (await readFile(abs, "utf8") !== cur) {
785
+ return { output: diskConflict(ctx, abs), isError: true };
786
+ }
787
+ ctx.signal.throwIfAborted();
788
+ await writeFile(abs, match.result, "utf8");
789
+ const hint = match.via === "eol" ? " (match tollerante CRLF/LF)" : match.via === "whitespace" ? " (match ignorando spazi a fine riga)" : "";
790
+ const notaNul = pulitoOld.rimossi || pulitoNext.rimossi
791
+ ? " [prevenzione: byte NUL rimossi dagli argomenti]"
792
+ : "";
793
+ return `modificato: ${rel}${hint}${notaNul}`;
794
+ }
795
+ finally {
796
+ releaseFileWrite(ctx, lease);
797
+ }
798
+ }
799
+ catch (err) {
800
+ return toolError(`errore modifica: ${err.message}`);
801
+ }
802
+ },
803
+ };
804
+ export const fileActivityTool = {
805
+ name: "file_activity",
806
+ description: "Mostra chi sta visualizzando o modificando file nel workspace. Usalo prima di coordinare lavoro concorrente; write_file ed edit_file bloccano automaticamente le sovrascritture.",
807
+ schema: {
808
+ type: "object",
809
+ properties: {
810
+ path: { type: "string", description: "file specifico; senza path mostra tutte le presenze" },
811
+ },
812
+ },
813
+ handler: async (input, ctx) => {
814
+ const coordination = ctx.fileCoordination;
815
+ if (!coordination)
816
+ return "coordinamento file non configurato dall'host";
817
+ const rawPath = String(input["path"] ?? "").trim();
818
+ const target = rawPath ? await safePath(ctx.cwd, rawPath, ctx.fsRoots) : undefined;
819
+ const rows = coordination.hub.list(target, coordination.actor.id);
820
+ if (!rows.length)
821
+ return target ? "nessun altro sta lavorando su questo file" : "nessuna attivita' file concorrente";
822
+ return rows.map((row) => `${row.path}\t${row.mode}\t${row.actor.label}\t${row.actor.kind}`).join("\n");
823
+ },
824
+ };
825
+ export const lsTool = {
826
+ name: "list_dir",
827
+ execution: "parallel",
828
+ description: "Elenca file e cartelle di una directory del workspace.",
829
+ schema: {
830
+ type: "object",
831
+ properties: { path: { type: "string", description: "default: ." } },
832
+ },
833
+ handler: async (input, ctx) => {
834
+ try {
835
+ const rel = String(input["path"] ?? ".");
836
+ const abs = await safePath(ctx.cwd, rel, ctx.fsRoots);
837
+ const names = await readdir(abs, { withFileTypes: true });
838
+ // Dirent already carries the type: no per-entry stat, and symlinks are
839
+ // not followed outside the validated directory.
840
+ const entries = names.map((entry) => entry.isDirectory() ? `${entry.name}/` : entry.name);
841
+ return entries.length ? paginate(entries.sort().join("\n"), `directory ${rel}`, ctx) : "(vuota)";
842
+ }
843
+ catch (err) {
844
+ return toolError(`errore list: ${err.message}`);
845
+ }
846
+ },
847
+ };
848
+ // recall: cerca nella memoria delle STANZE archiviate (metodo Danilov). Il
849
+ // contesto vivo resta compatto (solo l'indice delle stanze); con recall
850
+ // l'agente recupera a richiesta il dettaglio di 1+ stanze, per query o per id,
851
+ // senza inquinare il contesto con l'intero contenuto delle stanze precedenti.
852
+ export const recallTool = {
853
+ name: "recall",
854
+ description: "Cerca nella memoria precedente: attivita archiviate, piano, note, skill, vision e decisioni. " +
855
+ "Usa `query` per cercare per argomento, `rooms` per id specifici (es. [\"R1\",\"R3\"]), " +
856
+ "oppure `files` per sapere CHI HA GIA' LAVORATO su un file (es. [\"src/auth/totp.ts\"]). " +
857
+ "Con `domains` restringi a rooms/plan/notes/skills/vision, oppure a `sessions` per cercare nelle CONVERSAZIONI passate (l ho gia visto, questo errore?). Senza argomenti elenca l indice. " +
858
+ "Le attivita sono in ORDINE CRONOLOGICO: per riprendere il lavoro del turno precedente preferisci la attivita piu' RECENTE (l'ultima dell'indice, marcata), non quelle piu' vecchie (turni gia' conclusi). " +
859
+ "Il contesto resta compatto: recupera solo cio' che serve.",
860
+ schema: {
861
+ type: "object",
862
+ properties: {
863
+ query: { type: "string", description: "Testo da cercare nelle attivita archiviate" },
864
+ rooms: { type: "array", items: { type: "string" }, description: "Id attivita da recuperare integralmente" },
865
+ files: { type: "array", items: { type: "string" }, description: "Path: restituisce le attivita che hanno toccato quei file (traversata del grafo)" },
866
+ expand: { type: "boolean", description: "Se true, apre gruppi e liste fino alle singole attivita" },
867
+ domains: { type: "array", items: { type: "string" }, description: "Domini memoria: rooms, plan, notes, skills, vision, assets, decisions, contracts, sessions (conversazioni passate)" },
868
+ mode: { type: "string", enum: ["summary", "full", "evidence"], description: "Formato eventi memoria (default evidence)" },
869
+ limit: { type: "number", description: "Max risultati (default 5)" },
870
+ },
871
+ },
872
+ handler: async (input, ctx) => {
873
+ if (!ctx.sessionDir || !(ctx.roomStoreId || ctx.sessionId))
874
+ return "memoria attivita non disponibile in questo contesto";
875
+ // In Kingdoms Mode si legge lo store condiviso del regno (roomStoreId).
876
+ const scope = ctx.roomStoreId ?? ctx.sessionId;
877
+ const mem = RoomMemory.open(ctx.sessionDir, scope);
878
+ const eventStore = MemoryStore.open(ctx.sessionDir, scope);
879
+ const domains = recallDomains(input["domains"]);
880
+ const wantsRooms = domains.length === 0 || domains.includes("rooms");
881
+ const eventDomains = recallEventDomains(domains);
882
+ const mode = recallMode(input["mode"]);
883
+ const projectScopes = listMemoryScopes(ctx.sessionDir, { excludeScope: scope, limit: 6 });
884
+ const eventStores = [
885
+ { scopeId: scope, store: eventStore },
886
+ ...projectScopes.map((scopeId) => ({ scopeId, store: MemoryStore.open(ctx.sessionDir, scopeId) })),
887
+ ];
888
+ const eventCount = eventStores.reduce((sum, s) => sum + s.store.count(), 0);
889
+ if (!mem.count() && !eventCount)
890
+ return "nessuna memoria archiviata finora";
891
+ const rooms = Array.isArray(input["rooms"]) ? input["rooms"].map(String) : [];
892
+ if (rooms.length) {
893
+ const found = wantsRooms && mem.count() ? input["expand"] === true ? mem.expandRooms(rooms) : mem.byRooms(rooms) : [];
894
+ if (!found.length)
895
+ return `nessuna attivita trovata per: ${rooms.join(", ")}`;
896
+ return paginate(found.map((r) => r.body).join("\n\n---\n\n"), "attivita", ctx);
897
+ }
898
+ // TRAVERSATA DEL GRAFO: "chi ha gia' messo le mani su questo file?".
899
+ // La BM25 non puo' rispondere — due stanze possono descrivere lo stesso
900
+ // file con parole del tutto diverse — mentre l'arco stanza->file si'.
901
+ const files = Array.isArray(input["files"]) ? input["files"].map(String) : [];
902
+ if (files.length) {
903
+ if (!wantsRooms || !mem.count())
904
+ return "memoria attivita vuota: nessun grafo da percorrere";
905
+ const graph = mem.graph();
906
+ const ids = [...new Set(files.flatMap((f) => graph.roomsTouching(f)))];
907
+ if (!ids.length)
908
+ return `nessuna attivita ha toccato: ${files.join(", ")}`;
909
+ const found = mem.byRooms(ids);
910
+ return paginate(found.map((r) => r.body).join("\n\n---\n\n"), "attivita", ctx);
911
+ }
912
+ const query = typeof input["query"] === "string" ? input["query"] : "";
913
+ const limit = typeof input["limit"] === "number" ? input["limit"] : 5;
914
+ if (!query.trim()) {
915
+ const blocks = [];
916
+ if (wantsRooms && mem.count())
917
+ blocks.push(mem.indexBlock());
918
+ const events = recentMemoryEvents(eventStores, eventDomains, limit);
919
+ if (events.length)
920
+ blocks.push(formatMemoryEvents(events, mode));
921
+ return clamp(blocks.filter(Boolean).join("\n\n---\n\n") || "nessuna memoria archiviata");
922
+ }
923
+ const chunks = [];
924
+ if (wantsRooms && mem.count()) {
925
+ // searchGraph: match lessicali + un hop sugli archi. Un vicino entra solo
926
+ // se batte una coda lessicale debole, ed e' dichiarato come tale.
927
+ const hits = mem.searchGraph(query, limit);
928
+ if (hits.length) {
929
+ chunks.push(hits
930
+ .map((h) => h.viaGraph
931
+ ? `[via grafo: condivide ${(h.via ?? []).slice(0, 3).join(", ")} con un risultato diretto]\n${h.record.body}`
932
+ : h.record.body)
933
+ .join("\n\n---\n\n"));
934
+ }
935
+ }
936
+ const eventHits = searchMemoryEvents(eventStores, query, eventDomains, limit);
937
+ if (eventHits.length)
938
+ chunks.push(formatMemoryEvents(eventHits, mode));
939
+ // Ricerca esplicita sulle DECISIONI promosse (dominio decisions): aggiungi
940
+ // l'indice dedicato DecisionStore quando la query e' sulle decisioni.
941
+ if (/\bdecisi|\bdecision/i.test(query)) {
942
+ try {
943
+ const dec = DecisionStore.open(ctx.sessionDir, ctx.roomStoreId ?? ctx.sessionId);
944
+ const decHits = dec.search(query, limit);
945
+ if (decHits.length) {
946
+ chunks.push(decHits.map((h) => decisionToRoomRecord(h.event).body).join("\n\n---\n\n"));
947
+ }
948
+ }
949
+ catch {
950
+ // best-effort
951
+ }
952
+ }
953
+ // Ricerca esplicita sui CONTRATTI promossi (dominio contracts): aggiungi
954
+ // l'indice dedicato ContractStore quando la query e' su firma/API/callsite.
955
+ // E' la rete contro i callsite dimenticati dopo un cambio di firma.
956
+ if (/\bcontract|\bcallsite|\bcall\s*site|\bfirma|\bsignatur|\bapi\b/i.test(query)) {
957
+ try {
958
+ const con = ContractStore.open(ctx.sessionDir, ctx.roomStoreId ?? ctx.sessionId);
959
+ const conHits = con.search(query, limit);
960
+ if (conHits.length) {
961
+ chunks.push(conHits.map((h) => contractToRoomRecord(h.event).body).join("\n\n---\n\n"));
962
+ }
963
+ }
964
+ catch {
965
+ // best-effort
966
+ }
967
+ }
968
+ // CONVERSAZIONI PASSATE. La memoria curata risponde a "cosa ho deciso"; il
969
+ // corpus delle sessioni risponde a "l'ho gia' incontrato, questo?". Si
970
+ // cerca quando il dominio e' chiesto esplicitamente, e comunque quando la
971
+ // memoria curata non ha trovato niente: e' il momento in cui l'alternativa
972
+ // sarebbe rispondere "non ricordo" con la risposta a un metro di distanza.
973
+ const wantsSessions = domains.includes("sessions");
974
+ if (wantsSessions || !chunks.length) {
975
+ try {
976
+ const hits = searchSessions(ctx.sessionDir, query, {
977
+ limit: wantsSessions ? limit : 2,
978
+ skipId: typeof ctx.sessionId === "string" ? ctx.sessionId : undefined,
979
+ });
980
+ const block = formatSessionHits(hits);
981
+ if (block)
982
+ chunks.push(block);
983
+ }
984
+ catch {
985
+ // best-effort: l'indice e' una cache, non una dipendenza
986
+ }
987
+ }
988
+ if (!chunks.length)
989
+ return `nessuna memoria corrisponde a: ${query}`;
990
+ return paginate(chunks.join("\n\n---\n\n"), "memoria", ctx);
991
+ },
992
+ };
993
+ const DEFAULT_EVENT_DOMAINS = ["plan", "vision", "notes", "skills", "assets", "decisions", "contracts", "kingdom", "system"];
994
+ function recallDomains(value) {
995
+ if (!Array.isArray(value))
996
+ return [];
997
+ return [...new Set(value.map(String).map((d) => d.trim().toLowerCase()).filter(Boolean))];
998
+ }
999
+ function recallEventDomains(domains) {
1000
+ if (!domains.length)
1001
+ return DEFAULT_EVENT_DOMAINS;
1002
+ return domains.filter((d) => d !== "rooms");
1003
+ }
1004
+ function recallMode(value) {
1005
+ return value === "summary" || value === "full" || value === "evidence" ? value : "evidence";
1006
+ }
1007
+ function recentMemoryEvents(stores, domains, limit) {
1008
+ const pool = stores.flatMap(({ scopeId, store }) => {
1009
+ const events = domains.length
1010
+ ? domains.flatMap((domain) => [...store.query({ domain })])
1011
+ : [...store.all()];
1012
+ return events.map((event) => ({ scopeId, event, score: 0 }));
1013
+ });
1014
+ return pool.sort((a, b) => b.event.createdAt - a.event.createdAt || b.event.id.localeCompare(a.event.id)).slice(0, Math.max(1, limit));
1015
+ }
1016
+ function searchMemoryEvents(stores, query, domains, limit) {
1017
+ const hits = stores.flatMap(({ scopeId, store }) => {
1018
+ const scoped = domains.length
1019
+ ? domains.flatMap((domain) => [...store.search(query, { domain, limit })])
1020
+ : [...store.search(query, { limit })];
1021
+ return scoped.map((hit) => ({ scopeId, event: hit.event, score: hit.score }));
1022
+ });
1023
+ const seen = new Set();
1024
+ const out = [];
1025
+ for (const hit of hits.sort((a, b) => b.score - a.score)) {
1026
+ const key = `${hit.scopeId}:${hit.event.id}`;
1027
+ if (seen.has(key))
1028
+ continue;
1029
+ seen.add(key);
1030
+ out.push(hit);
1031
+ if (out.length >= Math.max(1, limit))
1032
+ break;
1033
+ }
1034
+ return out;
1035
+ }
1036
+ function formatMemoryEvents(hits, mode) {
1037
+ return hits.map((hit) => formatMemoryEvent(hit.scopeId, hit.event, hit.score, mode)).join("\n\n---\n\n");
1038
+ }
1039
+ function formatMemoryEvent(scopeId, event, score, mode) {
1040
+ const head = `[MEMORIA_EVENTO ${scopeId}/${event.id}] type=${event.type} domain=${event.domain} subject=${event.subject} score=${score.toFixed(3)}`;
1041
+ const links = event.links.length
1042
+ ? `\nLINKS\n${event.links.map((l) => `@link[${l.rel}]: ${l.domain ? `${l.domain}:` : ""}${l.target}`).join("\n")}`
1043
+ : "";
1044
+ if (mode === "summary")
1045
+ return `${head}\nSUMMARY: ${event.summary}${links}`;
1046
+ if (mode === "full")
1047
+ return `${head}\nSUMMARY: ${event.summary}\n\n${event.body || "(nessun body)"}${links}`;
1048
+ return `${head}\nINDICE\n${event.terms.map((t, i) => `${i + 1} = ${t}`).join("\n") || "1 = (nessun termine)"}\n\nDEFINIZIONI\n@evento[${scopeId}/${event.id}]: type=${event.type}, domain=${event.domain}, subject=${event.subject}\n@summary[${scopeId}/${event.id}]: "${event.summary.replace(/"/g, "'")}"\n\nBODY\n${event.body || "(nessun body)"}${links}\n\nOUTPUT: evidenza memoria recuperata.`;
1049
+ }
1050
+ // image_generate: genera un'immagine via API (OpenAI o compatibile) e la salva
1051
+ // nel workspace. L'agente puo' poi mostrarla inline con ![alt](path) (parita'
1052
+ // Pi: image generation + inline rendering). Richiede conferma (ha un costo).
1053
+ export const imageGenerateTool = {
1054
+ name: "image_generate",
1055
+ description: "Genera un'immagine da un prompt testuale e la salva nel workspace come PNG. " +
1056
+ "Per mostrarla inline, referenziala dopo con ![descrizione](path). Richiede conferma.",
1057
+ schema: {
1058
+ type: "object",
1059
+ properties: {
1060
+ prompt: { type: "string", description: "Descrizione dell'immagine da generare" },
1061
+ path: { type: "string", description: "File di output (default: generated-<ts>.png)" },
1062
+ size: { type: "string", description: "Dimensione es. 1024x1024 (default)" },
1063
+ },
1064
+ required: ["prompt"],
1065
+ },
1066
+ handler: async (input, ctx) => {
1067
+ ctx.signal.throwIfAborted();
1068
+ const prompt = String(input["prompt"] ?? "");
1069
+ if (!prompt)
1070
+ return toolError("errore: prompt vuoto");
1071
+ const env = process.env;
1072
+ const key = String(env["TALOS_OPENAI_API_KEY"] ?? env["OPENAI_API_KEY"] ?? "").trim();
1073
+ if (!key)
1074
+ return toolError("errore: nessuna chiave (imposta OPENAI_API_KEY o TALOS_OPENAI_API_KEY)");
1075
+ const rel = String(input["path"] ?? `generated-${Date.now()}.png`);
1076
+ let abs;
1077
+ try {
1078
+ abs = await safePath(ctx.cwd, rel, ctx.fsRoots);
1079
+ }
1080
+ catch (err) {
1081
+ return toolError(`errore percorso: ${err.message}`);
1082
+ }
1083
+ ctx.signal.throwIfAborted();
1084
+ const ok = await ctx.confirm("image_generate", `${prompt} -> ${rel}`);
1085
+ ctx.signal.throwIfAborted();
1086
+ if (!ok)
1087
+ return toolError("generazione rifiutata dall'utente");
1088
+ const baseUrl = String(env["TALOS_IMAGE_BASE_URL"] ?? "https://api.openai.com/v1").replace(/\/$/, "");
1089
+ const model = String(env["TALOS_IMAGE_MODEL"] ?? "gpt-image-1");
1090
+ const size = String(input["size"] ?? "1024x1024");
1091
+ try {
1092
+ const res = await fetch(`${baseUrl}/images/generations`, {
1093
+ method: "POST",
1094
+ signal: ctx.signal,
1095
+ headers: { "content-type": "application/json", authorization: `Bearer ${key}` },
1096
+ body: JSON.stringify({ model, prompt, size, n: 1 }),
1097
+ });
1098
+ if (!res.ok)
1099
+ return toolError(`errore generazione (${res.status}): ${clamp(await res.text().catch(() => res.statusText))}`);
1100
+ ctx.signal.throwIfAborted();
1101
+ const json = (await res.json());
1102
+ ctx.signal.throwIfAborted();
1103
+ const item = json.data?.[0];
1104
+ if (!item)
1105
+ return toolError("errore: risposta senza immagine");
1106
+ const bytes = item.b64_json
1107
+ ? Buffer.from(item.b64_json, "base64")
1108
+ : item.url
1109
+ ? Buffer.from(await (await fetch(item.url, { signal: ctx.signal })).arrayBuffer())
1110
+ : null;
1111
+ if (!bytes)
1112
+ return toolError("errore: nessun dato immagine");
1113
+ ctx.signal.throwIfAborted();
1114
+ await mkdir(dirname(abs), { recursive: true });
1115
+ ctx.signal.throwIfAborted();
1116
+ await writeFile(abs, bytes);
1117
+ return `immagine generata: ${rel} (${bytes.length} byte). Mostrala con ![${prompt.slice(0, 40)}](${rel})`;
1118
+ }
1119
+ catch (err) {
1120
+ return toolError(`errore generazione: ${err.message}`);
1121
+ }
1122
+ },
1123
+ };
1124
+ // Converte HTML in testo leggibile: rimuove script/style/head, strippa i tag,
1125
+ // decodifica le entita' di base, compatta gli spazi. Per la ricerca/documentazione.
1126
+ function htmlToText(html) {
1127
+ let s = html;
1128
+ s = s.replace(/<!--[\s\S]*?-->/g, " ");
1129
+ s = s.replace(/<(script|style|head|noscript|svg)[\s\S]*?<\/\1>/gi, " ");
1130
+ s = s.replace(/<\/(p|div|li|tr|h[1-6]|section|article|br|table|ul|ol)>/gi, "\n");
1131
+ s = s.replace(/<li[^>]*>/gi, "\n- ");
1132
+ s = s.replace(/<[^>]+>/g, " ");
1133
+ s = s.replace(/&nbsp;/gi, " ").replace(/&amp;/gi, "&").replace(/&lt;/gi, "<")
1134
+ .replace(/&gt;/gi, ">").replace(/&quot;/gi, '"').replace(/&#39;/gi, "'");
1135
+ s = s.replace(/[ \t]+/g, " ").replace(/\n[ \t]+/g, "\n").replace(/\n{3,}/g, "\n\n");
1136
+ return s.trim();
1137
+ }
1138
+ // web_fetch: scarica una pagina (ricerca/documentazione) e ne ritorna il testo.
1139
+ // Estensione dello "strumento fetch": read-only, fondamentale per arrivare
1140
+ // end-to-end (l'agente ricerca prima di costruire).
1141
+ export const webFetchTool = {
1142
+ name: "web_fetch",
1143
+ description: "Scarica una pagina web (URL http/https) e ne restituisce il testo leggibile (HTML ripulito). " +
1144
+ "Usalo per ricerca, documentazione e riferimenti prima di implementare.",
1145
+ schema: {
1146
+ type: "object",
1147
+ properties: {
1148
+ url: { type: "string", description: "URL http(s) da scaricare" },
1149
+ max_chars: { type: "number", description: `numero massimo di caratteri (default ${MAX_OUTPUT})` },
1150
+ },
1151
+ required: ["url"],
1152
+ },
1153
+ handler: async (input, ctx) => {
1154
+ const url = String(input["url"] ?? "").trim();
1155
+ if (!/^https?:\/\//i.test(url))
1156
+ return toolError("errore: URL non valido (richiesto http/https)");
1157
+ // Nessun cap di default: la paginazione gestisce gli output lunghi. max_chars
1158
+ // resta come hard cap opzionale se il modello lo specifica.
1159
+ const maxChars = typeof input["max_chars"] === "number" && input["max_chars"] > 0
1160
+ ? input["max_chars"] : 0;
1161
+ const timeoutMs = Number(process.env["TALOS_FETCH_TIMEOUT"]) || 20_000;
1162
+ const signals = [ctx.signal, AbortSignal.timeout(timeoutMs)];
1163
+ const signal = typeof AbortSignal.any === "function" ? AbortSignal.any(signals) : ctx.signal;
1164
+ try {
1165
+ const res = await fetch(url, {
1166
+ signal,
1167
+ redirect: "follow",
1168
+ headers: {
1169
+ "user-agent": "Mozilla/5.0 (compatible; VascendTalos/1.0; +https://vascend.it)",
1170
+ accept: "text/html,application/xhtml+xml,application/json;q=0.9,*/*;q=0.8",
1171
+ },
1172
+ });
1173
+ if (!res.ok)
1174
+ return toolError(`errore http ${res.status} ${res.statusText} su ${url}`);
1175
+ const ct = (res.headers.get("content-type") ?? "").toLowerCase();
1176
+ const raw = await res.text();
1177
+ const body = ct.includes("html") ? htmlToText(raw) : raw;
1178
+ // max_chars come hard cap opzionale, poi paginazione (no troncamento secco).
1179
+ const capped = maxChars > 0 && body.length > maxChars ? body.slice(0, maxChars) : body;
1180
+ return `# ${url}\n${paginate(capped, url, ctx) || "(pagina vuota)"}`;
1181
+ }
1182
+ catch (err) {
1183
+ const e = err;
1184
+ if (e.name === "TimeoutError" || e.name === "AbortError")
1185
+ return toolError(`errore: timeout/interruzione scaricando ${url}`);
1186
+ return toolError(`errore fetch: ${e.message}`);
1187
+ }
1188
+ },
1189
+ };
1190
+ // Directory saltate durante glob/grep (rumore/binari).
1191
+ const SEARCH_IGNORE = new Set([
1192
+ "node_modules", ".git", "dist", "build", ".next", ".turbo", ".cache",
1193
+ "coverage", ".venv", "venv", "__pycache__", "target", ".idea", ".vscode",
1194
+ ]);
1195
+ // Estensioni sicuramente binarie: inutile leggerle per una ricerca testuale.
1196
+ // Il controllo sul byte NUL a valle resta (per i binari senza estensione nota), ma
1197
+ // evitare la lettura INTERA di media e archivi taglia gran parte dell'I/O di
1198
+ // una ricerca larga.
1199
+ const BINARY_EXTENSIONS = new Set([
1200
+ "png", "jpg", "jpeg", "gif", "webp", "ico", "icns", "bmp", "tiff", "psd",
1201
+ "pdf", "zip", "gz", "tgz", "bz2", "xz", "zst", "rar", "7z", "jar",
1202
+ "exe", "dll", "so", "dylib", "node", "wasm", "class", "pyc", "o", "a",
1203
+ "woff", "woff2", "ttf", "otf", "eot",
1204
+ "mp3", "mp4", "mov", "avi", "webm", "ogg", "wav", "flac",
1205
+ "db", "sqlite", "sqlite3", "bin", "pak", "asar", "lockb", "heic",
1206
+ ]);
1207
+ function estensione(nome) {
1208
+ const dot = nome.lastIndexOf(".");
1209
+ return dot > 0 ? nome.slice(dot + 1).toLowerCase() : "";
1210
+ }
1211
+ // Respiro per l'event loop: in Horizon questi tool girano IN-PROCESS nel main
1212
+ // di Electron. Senza pause il pump dei messaggi Windows resta a secco e il
1213
+ // sistema marchia la finestra come "non risponde" (ghost window). Un
1214
+ // setImmediate ogni ~15ms tiene la UI viva senza costo misurabile.
1215
+ const BREATH_EVERY_MS = 15;
1216
+ function respiro() {
1217
+ return new Promise((resolve) => setImmediate(resolve));
1218
+ }
1219
+ // Concorrenza della lettura file in grep: abbastanza per saturare l'I/O del
1220
+ // disco, poco abbastanza da non accumulare burst di split+regex sulla CPU.
1221
+ const GREP_CONCURRENCY = 16;
1222
+ // Budget di tempo predefinito di una grep. Il respiro dell'event loop tiene
1223
+ // la UI viva comunque; il budget evita che un intero turno aspetti minuti una
1224
+ // ricerca fuori misura (misurato: ~42s per una grep alla radice del monorepo).
1225
+ const GREP_BUDGET_MS = 10_000;
1226
+ // Un match si cerca solo nella testa di una riga: oltre questa lunghezza e'
1227
+ // quasi certamente un bundle minificato/una sourcemap, e il backtracking del
1228
+ // regex JS su pattern tipo `a.*b.*c` diventa quadratico (stalli >1s misurati
1229
+ // per un singolo .test).
1230
+ const MAX_LINE_SCAN = 2_000;
1231
+ // Walk ricorsivo dei file sotto root, saltando le dir ignorate e quelle nascoste.
1232
+ // Ritorna quanti file ha VISITATO: e' il costo reale della ricerca, e serve a
1233
+ // dire al modello quando sta cercando troppo in largo.
1234
+ async function walkFiles(root, maxVisit, onFile) {
1235
+ let visited = 0;
1236
+ let fiato = Date.now();
1237
+ const stack = [root];
1238
+ while (stack.length) {
1239
+ if (Date.now() - fiato > BREATH_EVERY_MS) {
1240
+ fiato = Date.now();
1241
+ await respiro();
1242
+ }
1243
+ const dir = stack.pop();
1244
+ let entries;
1245
+ try {
1246
+ entries = await readdir(dir, { withFileTypes: true });
1247
+ }
1248
+ catch {
1249
+ continue;
1250
+ }
1251
+ for (const e of entries) {
1252
+ if (visited >= maxVisit)
1253
+ return visited;
1254
+ const full = join(dir, e.name);
1255
+ if (e.isDirectory()) {
1256
+ if (SEARCH_IGNORE.has(e.name) || e.name.startsWith("."))
1257
+ continue;
1258
+ stack.push(full);
1259
+ }
1260
+ else if (e.isFile()) {
1261
+ visited += 1;
1262
+ if (onFile(full) === false)
1263
+ return visited; // stop richiesto
1264
+ }
1265
+ }
1266
+ }
1267
+ return visited;
1268
+ }
1269
+ // Oltre questi file visitati la ricerca e' larga abbastanza da valere un
1270
+ // avviso: sotto, il costo e' irrilevante e la nota sarebbe rumore.
1271
+ export const WIDE_SEARCH_FILES = 3_000;
1272
+ const PROJECT_MARKERS = ["package.json", "pyproject.toml", "Cargo.toml", "go.mod", "pom.xml", "composer.json"];
1273
+ // Sottoprogetti di primo livello sotto `root`: una cartella con un marker di
1274
+ // progetto o un .git proprio. Un readdir e pochi stat, costo trascurabile.
1275
+ async function childProjects(root) {
1276
+ let entries;
1277
+ try {
1278
+ entries = await readdir(root, { withFileTypes: true });
1279
+ }
1280
+ catch {
1281
+ return [];
1282
+ }
1283
+ const found = [];
1284
+ for (const e of entries) {
1285
+ if (!e.isDirectory() || SEARCH_IGNORE.has(e.name) || e.name.startsWith("."))
1286
+ continue;
1287
+ const dir = join(root, e.name);
1288
+ const markers = await Promise.all(PROJECT_MARKERS.map((m) => stat(join(dir, m)).then(() => true, () => false)));
1289
+ const git = await stat(join(dir, ".git")).then(() => true, () => false);
1290
+ if (markers.some(Boolean) || git)
1291
+ found.push(e.name);
1292
+ if (found.length >= 64)
1293
+ break;
1294
+ }
1295
+ return found;
1296
+ }
1297
+ /**
1298
+ * Nota di ampiezza da appendere al risultato di una ricerca.
1299
+ *
1300
+ * Perche' esiste: con la conversazione puntata alla radice di un monorepo, una
1301
+ * grep cammina su decine di migliaia di file (misurato: 28.575 file, ~25
1302
+ * secondi, contro 137 file della sottocartella del task) e il modello non ha
1303
+ * modo di saperlo — vede solo un risultato lento e rumoroso, e continua a
1304
+ * cercare in largo. Qui il costo diventa esplicito insieme al rimedio: `path`.
1305
+ */
1306
+ export async function wideSearchNote(root, visited) {
1307
+ if (visited < WIDE_SEARCH_FILES)
1308
+ return "";
1309
+ const projects = await childProjects(root);
1310
+ if (projects.length < 3)
1311
+ return "";
1312
+ const shown = projects.slice(0, 12).join(", ");
1313
+ const more = projects.length > 12 ? `, +${projects.length - 12}` : "";
1314
+ return `\n[ricerca su ${visited} file: questa radice contiene ${projects.length} progetti (${shown}${more}). `
1315
+ + "Per essere piu' rapido e mirato ripeti con path=<progetto> quando sai gia' dove stai lavorando.]";
1316
+ }
1317
+ // Converte un glob (*, **, ?) in RegExp ancorata.
1318
+ function globToRegExp(glob) {
1319
+ let re = "";
1320
+ for (let i = 0; i < glob.length; i += 1) {
1321
+ const c = glob[i];
1322
+ if (c === "*") {
1323
+ if (glob[i + 1] === "*") {
1324
+ re += ".*";
1325
+ i += 1;
1326
+ if (glob[i + 1] === "/")
1327
+ i += 1;
1328
+ }
1329
+ else
1330
+ re += "[^/]*";
1331
+ }
1332
+ else if (c === "?")
1333
+ re += "[^/]";
1334
+ else if ("\\^$+.()|{}[]".includes(c))
1335
+ re += "\\" + c;
1336
+ else
1337
+ re += c;
1338
+ }
1339
+ return new RegExp("^" + re + "$");
1340
+ }
1341
+ function relForward(cwd, abs) {
1342
+ const r = relative(cwd, abs);
1343
+ return (r || abs).split(/[\\/]/).join("/");
1344
+ }
1345
+ // glob: trova file per pattern di nome (es. "**/*.tsx", "*.json"). Cerca da
1346
+ // `path` (default cwd), path liberi. Sostituisce il "find" via bash.
1347
+ export const globTool = {
1348
+ name: "glob",
1349
+ execution: "parallel",
1350
+ description: "Trova file per pattern di nome (glob: *, **, ?). Es. \"**/*.tsx\", \"*.json\". " +
1351
+ "Cerca a partire da `path` (default cwd; accetta path assoluto o ../). Salta node_modules/.git ecc.",
1352
+ schema: {
1353
+ type: "object",
1354
+ properties: {
1355
+ pattern: { type: "string", description: "glob del nome file (es. **/*.ts)" },
1356
+ path: { type: "string", description: "directory radice della ricerca (default cwd)" },
1357
+ limit: { type: "number", description: "max risultati (default 100)" },
1358
+ },
1359
+ required: ["pattern"],
1360
+ },
1361
+ handler: async (input, ctx) => {
1362
+ const pattern = String(input["pattern"] ?? "").trim();
1363
+ if (!pattern)
1364
+ return "errore: pattern vuoto";
1365
+ let root;
1366
+ try {
1367
+ root = await safePath(ctx.cwd, String(input["path"] ?? "."), ctx.fsRoots);
1368
+ }
1369
+ catch (err) {
1370
+ return `errore percorso: ${err.message}`;
1371
+ }
1372
+ const limit = typeof input["limit"] === "number" && input["limit"] > 0 ? input["limit"] : 100;
1373
+ const re = globToRegExp(pattern);
1374
+ const matchBase = !pattern.includes("/");
1375
+ const hits = [];
1376
+ const visited = await walkFiles(root, 50_000, (abs) => {
1377
+ const rel = relForward(root, abs);
1378
+ const target = matchBase ? (rel.split("/").pop() ?? rel) : rel;
1379
+ if (re.test(target))
1380
+ hits.push(relForward(ctx.cwd, abs));
1381
+ return hits.length < limit;
1382
+ });
1383
+ const wide = await wideSearchNote(root, visited);
1384
+ if (!hits.length)
1385
+ return `nessun file per: ${pattern} (da ${relForward(ctx.cwd, root)})${wide}`;
1386
+ const more = hits.length >= limit ? `\n…[limite ${limit}; restringi il pattern]` : "";
1387
+ return `${hits.length} file:\n${hits.join("\n")}${more}${wide}`;
1388
+ },
1389
+ };
1390
+ // grep: cerca un pattern (regex) nel contenuto dei file. Es. trova dove e'
1391
+ // definita una funzione. Sostituisce il "grep -r" via bash.
1392
+ export const grepTool = {
1393
+ name: "grep",
1394
+ execution: "parallel",
1395
+ description: "Cerca un pattern (regex) nel CONTENUTO dei file e ritorna path:riga: testo. " +
1396
+ "Opz. `glob` per filtrare i file, `path` radice (default cwd, path liberi), `ignore_case`. Salta node_modules/.git ecc.",
1397
+ schema: {
1398
+ type: "object",
1399
+ properties: {
1400
+ pattern: { type: "string", description: "regex da cercare nel contenuto" },
1401
+ path: { type: "string", description: "directory/file radice (default cwd)" },
1402
+ glob: { type: "string", description: "filtro sui nomi file (es. *.ts)" },
1403
+ ignore_case: { type: "boolean", description: "ricerca case-insensitive" },
1404
+ limit: { type: "number", description: "max righe risultato (default 80)" },
1405
+ },
1406
+ required: ["pattern"],
1407
+ },
1408
+ handler: async (input, ctx) => {
1409
+ const pattern = String(input["pattern"] ?? "");
1410
+ if (!pattern)
1411
+ return "errore: pattern vuoto";
1412
+ let re;
1413
+ let patternNotes = [];
1414
+ try {
1415
+ // I modelli scrivono regex in stile ripgrep: le forme traducibili
1416
+ // vengono tradotte invece di far fallire tutta la ricerca.
1417
+ const compiled = compileSearchPattern(pattern, { ignoreCase: input["ignore_case"] === true });
1418
+ re = compiled.regex;
1419
+ patternNotes = compiled.notes;
1420
+ }
1421
+ catch (err) {
1422
+ return toolError(`errore regex: ${err.message}`);
1423
+ }
1424
+ let root;
1425
+ try {
1426
+ root = await safePath(ctx.cwd, String(input["path"] ?? "."), ctx.fsRoots);
1427
+ }
1428
+ catch (err) {
1429
+ return `errore percorso: ${err.message}`;
1430
+ }
1431
+ const limit = typeof input["limit"] === "number" && input["limit"] > 0 ? input["limit"] : 80;
1432
+ const fileGlob = typeof input["glob"] === "string" && input["glob"] ? globToRegExp(input["glob"]) : null;
1433
+ // Budget di tempo: una grep alla radice di un monorepo leggeva ~28.500
1434
+ // file per ~42 secondi misurati, tutti dentro il main di Electron. Oltre
1435
+ // il budget si ritorna quel che c'e', con l'indicazione per restringere.
1436
+ // `max_ms` e' un override fuori schema (test e chiamate programmatiche).
1437
+ const budget = typeof input["max_ms"] === "number" && input["max_ms"] > 0
1438
+ ? Math.min(input["max_ms"], 120_000)
1439
+ : GREP_BUDGET_MS;
1440
+ const scadenza = Date.now() + budget;
1441
+ const out = [];
1442
+ let stop = false;
1443
+ let scaduto = false;
1444
+ const scanFile = async (abs) => {
1445
+ try {
1446
+ const st = await stat(abs);
1447
+ if (st.size > 2_000_000)
1448
+ return; // salta file enormi
1449
+ const text = await readFile(abs, "utf8");
1450
+ if (text.includes("\u0000"))
1451
+ return; // binario
1452
+ const rel = relForward(ctx.cwd, abs);
1453
+ const lines = text.split("\n");
1454
+ for (let i = 0; i < lines.length; i += 1) {
1455
+ const riga = lines[i] ?? "";
1456
+ // Sonda solo la testa delle righe fuori misura: vedi MAX_LINE_SCAN.
1457
+ const sonda = riga.length > MAX_LINE_SCAN ? riga.slice(0, MAX_LINE_SCAN) : riga;
1458
+ if (re.test(sonda)) {
1459
+ out.push(`${rel}:${i + 1}: ${truncateLine(riga, 200)}`);
1460
+ if (out.length >= limit) {
1461
+ stop = true;
1462
+ return;
1463
+ }
1464
+ }
1465
+ }
1466
+ }
1467
+ catch {
1468
+ // file illeggibile: ignora
1469
+ }
1470
+ };
1471
+ // Se root e' un file singolo, scansiona solo quello.
1472
+ let single = false;
1473
+ let visited = 0;
1474
+ let letti = 0;
1475
+ try {
1476
+ single = (await stat(root)).isFile();
1477
+ }
1478
+ catch { /* dir o assente */ }
1479
+ if (single) {
1480
+ await scanFile(root);
1481
+ }
1482
+ else {
1483
+ // Prima il walk (solo readdir, economico) raccoglie i candidati; poi la
1484
+ // lettura dei contenuti va a concorrenza limitata con respiri periodici.
1485
+ // Il fan-out illimitato di prima accodava decine di migliaia di readFile
1486
+ // + split + regex senza mai cedere il passo: in Horizon il main di
1487
+ // Electron restava saturo fino al ghost "non risponde" di Windows.
1488
+ const candidati = [];
1489
+ visited = await walkFiles(root, 50_000, (abs) => {
1490
+ if (stop || ctx.signal?.aborted)
1491
+ return false;
1492
+ if (Date.now() > scadenza) {
1493
+ scaduto = true;
1494
+ return false;
1495
+ }
1496
+ const rel = relForward(root, abs);
1497
+ const base = rel.split("/").pop() ?? rel;
1498
+ if (fileGlob && !fileGlob.test(base) && !fileGlob.test(rel))
1499
+ return true;
1500
+ if (BINARY_EXTENSIONS.has(estensione(base)))
1501
+ return true;
1502
+ candidati.push(abs);
1503
+ return true;
1504
+ });
1505
+ let indice = 0;
1506
+ let fiato = Date.now();
1507
+ const operaio = async () => {
1508
+ while (!stop && indice < candidati.length) {
1509
+ if (ctx.signal?.aborted)
1510
+ return;
1511
+ if (Date.now() > scadenza) {
1512
+ scaduto = true;
1513
+ return;
1514
+ }
1515
+ const abs = candidati[indice];
1516
+ indice += 1;
1517
+ letti += 1;
1518
+ await scanFile(abs);
1519
+ if (Date.now() - fiato > BREATH_EVERY_MS) {
1520
+ fiato = Date.now();
1521
+ await respiro();
1522
+ }
1523
+ }
1524
+ };
1525
+ await Promise.all(Array.from({ length: GREP_CONCURRENCY }, () => operaio()));
1526
+ }
1527
+ // Le traduzioni applicate al pattern vanno dichiarate: l'agente deve poter
1528
+ // capire perche' una ricerca ha risposto in modo diverso da come l'ha scritta.
1529
+ const note = patternNotes.length ? `[pattern: ${patternNotes.join("; ")}]\n` : "";
1530
+ const wide = single ? "" : await wideSearchNote(root, visited);
1531
+ const tempo = scaduto
1532
+ ? `\n[budget di tempo (${Math.round(budget / 1000)}s) esaurito dopo ${letti} file letti: risultati parziali. `
1533
+ + "Restringi con path=<cartella> e/o glob=<pattern>.]"
1534
+ : "";
1535
+ // La nota di ampiezza serve SOPRATTUTTO quando non si trova nulla: e' li'
1536
+ // che il modello riprova a caso invece di restringere il campo.
1537
+ if (!out.length)
1538
+ return `${note}nessuna corrispondenza per: ${pattern}${tempo}${wide}`;
1539
+ const more = out.length >= limit ? `\n\u2026[limite ${limit}; restringi pattern/glob]` : "";
1540
+ return `${note}${out.length} righe:\n${out.slice(0, limit).join("\n")}${more}${tempo}${wide}`;
1541
+ },
1542
+ };
1543
+ function truncateLine(s, max) {
1544
+ const t = s.trim();
1545
+ return t.length > max ? t.slice(0, max) + "…" : t;
1546
+ }
1547
+ // Dove vive il PIANO per questo contesto. In squadra i collaboratori hanno
1548
+ // `roomStoreId` = id del regno (memoria delle stanze condivisa), ma il piano che
1549
+ // l'utente vede sulla board e' quello della sessione che ha pianificato: senza
1550
+ // `planStoreId` le marcature di un collaboratore finivano su un file suo, e la
1551
+ // board non si muoveva mai durante un regno.
1552
+ //
1553
+ // Nessun lock: apertura, mutazione e scrittura sono sincrone dentro un solo
1554
+ // handler, quindi due collaboratori dello stesso processo non si interlacciano.
1555
+ function planScope(ctx) {
1556
+ return ctx.planStoreId ?? ctx.roomStoreId ?? ctx.sessionId;
1557
+ }
1558
+ // vascend_plan: l'agente definisce le STANZE CUSTOM del castello per il goal
1559
+ // corrente (al posto dei 4 task default). Una stanza per bit one-hot.
1560
+ export const vascendPlanTool = {
1561
+ name: "vascend_plan",
1562
+ description: "Definisci i passaggi di quello che stai facendo ADESSO (attivita di un gruppo, piano di lavoro): "
1563
+ + "compaiono sulla board mentre lavori. Se stai eseguendo un nodo di un piano a rami, questi sono i "
1564
+ + "passaggi DI QUEL NODO — il percorso lungo sta un livello sopra e non si tocca da qui. " +
1565
+ "Usalo per pianificare i passi reali invece dei task default. Ogni voce di `tasks` diventa una attivita T01, T02, ... " +
1566
+ "Per piu' workstream crea PIU' gruppi chiamandolo con `group` diversi (piano multi-gruppo). " +
1567
+ "Poi completa ogni attivita completata con vascend_mark. Usa vascend_status per riprendere; " +
1568
+ "vascend_addtasks aggiunge passi senza perdere progressi. Ridefinire un piano diverso richiede replace=true.",
1569
+ schema: {
1570
+ type: "object",
1571
+ properties: {
1572
+ group: { type: "string", description: "slug del gruppo (default \"master\"); slug diversi = gruppi diversi" },
1573
+ title: { type: "string", description: "Titolo del gruppo (sintesi del workstream)" },
1574
+ tasks: {
1575
+ type: "array",
1576
+ minItems: 1,
1577
+ maxItems: 16,
1578
+ items: { type: "string" },
1579
+ description: "Attivita in ordine (max 16), una frase ciascuna. Token inline opzionali (opzioni del piano): " +
1580
+ "@skill:slug1,slug2 (skill da usare), @file:path1,path2 (file da pre-caricare), @w:1-5 (peso), @compact (punto di compattazione).",
1581
+ },
1582
+ after: { type: "string", description: "slug di un gruppo prerequisito: questo parte dopo che quello e' completo (@after)" },
1583
+ replace: { type: "boolean", description: "Sostituisce esplicitamente un piano diverso, azzerandone stati, note e prove. Default false." },
1584
+ },
1585
+ required: ["tasks"],
1586
+ },
1587
+ handler: async (input, ctx) => {
1588
+ const sid = planScope(ctx);
1589
+ if (!ctx.sessionDir || !sid)
1590
+ return "piano non disponibile in questo contesto";
1591
+ const tasks = taskTitles(input["tasks"]);
1592
+ const store = VascendPlanStore.open(ctx.sessionDir, sid, { strict: true });
1593
+ if (ctx.planCastle)
1594
+ return { output: "il piano assegnato va conservato: usa vascend_status, vascend_mark e vascend_note; chiedi al coordinatore di ridefinirlo", isError: true };
1595
+ const slug = String(input["group"] ?? input["castle"] ?? ctx.kingdom?.self ?? "master").trim() || "master";
1596
+ const after = typeof input["after"] === "string" ? input["after"] : undefined;
1597
+ const title = String(input["title"] ?? "");
1598
+ const existing = store.castle(slug);
1599
+ if (existing && sameCastlePlan(existing, title, tasks, after)) {
1600
+ return `piano [${slug}] gia' presente: stati, note e prove conservati. Usa vascend_status per riprendere.`;
1601
+ }
1602
+ if (existing && input["replace"] !== true) {
1603
+ return { output: `piano [${slug}] conservato: usa vascend_addtasks per aggiungere passi, un altro group per un nuovo lavoro, oppure replace=true per ridefinirlo.`, isError: true };
1604
+ }
1605
+ const proposal = { slug, after: after?.trim(), tasks: [] };
1606
+ const candidate = { ...proposal, title, maskTarget: "0x0000", custom: true };
1607
+ if (planBlockers([...store.castles().filter((castle) => castle.slug !== slug), candidate], slug).some((blocker) => blocker.reason === "cycle")) {
1608
+ return { output: "errore: dipendenza ciclica tra gruppi; correggi after", isError: true };
1609
+ }
1610
+ const plan = store.setCastle(slug, title, tasks, after);
1611
+ const rows = plan.tasks.map((t) => {
1612
+ const tags = [
1613
+ t.skills?.length ? `@skill:${t.skills.join(",")}` : "",
1614
+ t.files?.length ? `@file:${t.files.join(",")}` : "",
1615
+ t.weight ? `@w:${t.weight}` : "",
1616
+ t.compact ? "@compact" : "",
1617
+ ].filter(Boolean).join(" ");
1618
+ return ` ${t.id} ${t.title}${tags ? ` ${tags}` : ""}`;
1619
+ }).join("\n");
1620
+ const total = store.castles().length;
1621
+ const dep = plan.after ? ` (dopo "${plan.after}")` : "";
1622
+ return `gruppo "${plan.title}" [${plan.slug}]${dep} pianificato (${plan.tasks.length} attivita):\n${rows}\n` +
1623
+ `piano: ${total} grupp${total === 1 ? "o" : "i"}. completa ogni attivita con vascend_mark (indica group="${plan.slug}").`;
1624
+ },
1625
+ };
1626
+ // vascend_note: annota una stanza o un castello (dossier Danilov). Le note
1627
+ // restano nel piano e compaiono in /regno; l'agente le usa per ricordare
1628
+ // analisi/decisioni/esiti senza inquinare il contesto vivo.
1629
+ export const vascendNoteTool = {
1630
+ name: "vascend_note",
1631
+ description: "Aggiungi una nota (dossier) a una attivita o a un gruppo del piano, nel piano di lavoro. " +
1632
+ "Indica `task` (es. \"T02\") per annotare la attivita, oppure ometti `task` per annotare il gruppo. " +
1633
+ "Usa note compatte: analisi, decisione, esito. Le note restano nel piano e si rileggono in /piano.",
1634
+ schema: {
1635
+ type: "object",
1636
+ properties: {
1637
+ group: { type: "string", description: "slug del gruppo (default \"master\")" },
1638
+ task: { type: "string", description: "id attivita (T01) o bit; ometti per annotare il gruppo" },
1639
+ note: { type: "string", description: "testo della nota (compatto, notazione Danilov)" },
1640
+ },
1641
+ required: ["note"],
1642
+ },
1643
+ handler: async (input, ctx) => {
1644
+ const sid = planScope(ctx);
1645
+ if (!ctx.sessionDir || !sid)
1646
+ return "piano non disponibile in questo contesto";
1647
+ const store = VascendPlanStore.open(ctx.sessionDir, sid);
1648
+ if (!store.hasAny())
1649
+ return "nessun gruppo pianificato: usa prima vascend_plan";
1650
+ const slug = String(input["group"] ?? input["castle"] ?? "master");
1651
+ const task = typeof input["task"] === "string" && input["task"] ? input["task"] : null;
1652
+ const ok = store.addNote(slug, task, String(input["note"] ?? ""));
1653
+ if (!ok)
1654
+ return `destinazione non trovata: ${slug}${task ? `/${task}` : ""}`;
1655
+ return `nota aggiunta a ${slug}${task ? `/${task}` : " (gruppo)"}`;
1656
+ },
1657
+ };
1658
+ // vascend_skill: abbina delle skill a una stanza/castello "in anticipo".
1659
+ // Quando poi la stanza viene avviata (vascend_mark running), il contenuto delle
1660
+ // skill abbinate viene riportato cosi' l'agente le applica subito.
1661
+ export const vascendSkillTool = {
1662
+ name: "vascend_skill",
1663
+ description: "Abbina una o piu' skill a una attivita o gruppo IN ANTICIPO (piano di lavoro). " +
1664
+ "Indica `task` per la attivita, oppure ometti per il gruppo. Le skill abbinate vengono " +
1665
+ "richiamate automaticamente quando avvii la attivita con vascend_mark (status running). " +
1666
+ "Passa `load: true` per ricevere subito il contenuto delle skill risolte.",
1667
+ schema: {
1668
+ type: "object",
1669
+ properties: {
1670
+ group: { type: "string", description: "slug del gruppo (default \"master\")" },
1671
+ task: { type: "string", description: "id attivita (T01) o bit; ometti per il gruppo" },
1672
+ skills: { type: "array", items: { type: "string" }, description: "slug delle skill da abbinare" },
1673
+ load: { type: "boolean", description: "se true, restituisce subito il contenuto delle skill risolte" },
1674
+ },
1675
+ required: ["skills"],
1676
+ },
1677
+ handler: async (input, ctx) => {
1678
+ const sid = planScope(ctx);
1679
+ if (!ctx.sessionDir || !sid)
1680
+ return "piano non disponibile in questo contesto";
1681
+ const store = VascendPlanStore.open(ctx.sessionDir, sid);
1682
+ if (!store.hasAny())
1683
+ return "nessun gruppo pianificato: usa prima vascend_plan";
1684
+ const slug = String(input["group"] ?? input["castle"] ?? "master");
1685
+ const task = typeof input["task"] === "string" && input["task"] ? input["task"] : null;
1686
+ const skills = Array.isArray(input["skills"]) ? input["skills"].map(String) : [];
1687
+ if (!skills.length)
1688
+ return "errore: nessuna skill indicata";
1689
+ const ok = store.bindSkills(slug, task, skills);
1690
+ if (!ok)
1691
+ return `destinazione non trovata: ${slug}${task ? `/${task}` : ""}`;
1692
+ const head = `skill abbinate a ${slug}${task ? `/${task}` : " (gruppo)"}: ${skills.join(", ")}`;
1693
+ if (input["load"] !== true)
1694
+ return head + "\nverranno richiamate all'avvio della attivita (vascend_mark running).";
1695
+ const body = resolveSkillBodies(ctx.cwd, skills);
1696
+ return body ? `${head}\n\n${body}` : `${head}\n(nessuna skill risolta sul disco: verifica gli slug)`;
1697
+ },
1698
+ };
1699
+ // vascend_addtasks: aggiunge nuove stanze a un castello SENZA resettare quelle
1700
+ // gia' fatte. Usato dal revisore per "aggiungerne altre" in corso d'opera.
1701
+ export const vascendAddRoomsTool = {
1702
+ name: "vascend_addtasks",
1703
+ description: "Aggiunge nuove attivita (pending) a un gruppo esistente senza toccare quelle gia' completate. " +
1704
+ "I bit proseguono dopo l'ultimo e MASK_TARGET si estende. Stessi token inline di vascend_plan (@skill,@file,@w,@compact).",
1705
+ schema: {
1706
+ type: "object",
1707
+ properties: {
1708
+ group: { type: "string", description: "slug del gruppo (default \"master\")" },
1709
+ tasks: { type: "array", items: { type: "string" }, description: "nuove attivita in ordine" },
1710
+ },
1711
+ required: ["tasks"],
1712
+ },
1713
+ handler: async (input, ctx) => {
1714
+ const sid = planScope(ctx);
1715
+ if (!ctx.sessionDir || !sid)
1716
+ return "piano non disponibile in questo contesto";
1717
+ const store = VascendPlanStore.open(ctx.sessionDir, sid, { strict: true });
1718
+ if (!store.hasAny())
1719
+ return "nessun gruppo pianificato: usa prima vascend_plan";
1720
+ const slug = String(input["group"] ?? input["castle"] ?? "master");
1721
+ const current = store.castle(slug);
1722
+ if (!current)
1723
+ return { output: `gruppo non trovato: ${slug}`, isError: true };
1724
+ if (ctx.planCastle)
1725
+ return { output: "chiedi al coordinatore di estendere il piano condiviso; il tuo mandato copre le attivita gia' assegnate", isError: true };
1726
+ const tasks = taskTitles(input["tasks"], 16 - current.tasks.length);
1727
+ const added = store.addTasks(slug, tasks);
1728
+ if (!added.length)
1729
+ return `gruppo non trovato o limite attivita raggiunto: ${slug}`;
1730
+ const castle = store.castle(slug);
1731
+ const rows = added.map((t) => ` ${t.id} ${t.title}`).join("\n");
1732
+ return `aggiunte ${added.length} attivita a [${slug}]:\n${rows}`;
1733
+ },
1734
+ };
1735
+ function clampNum(v, def, min, max) {
1736
+ const n = typeof v === "number" && Number.isFinite(v) ? v : def;
1737
+ return Math.min(max, Math.max(min, Math.floor(n)));
1738
+ }
1739
+ // Attesa interrompibile (Esc -> ctx.signal). Rigetta se abortita.
1740
+ function sleep(ms, signal) {
1741
+ return new Promise((resolve, reject) => {
1742
+ const onAbort = () => { cleanup(); reject(new Error("interrotto")); };
1743
+ const cleanup = () => { clearTimeout(t); signal.removeEventListener("abort", onAbort); };
1744
+ if (signal.aborted)
1745
+ return reject(new Error("interrotto"));
1746
+ const t = setTimeout(() => { cleanup(); resolve(); }, ms);
1747
+ signal.addEventListener("abort", onAbort, { once: true });
1748
+ });
1749
+ }
1750
+ // Esegue un comando e ritorna il solo exit code (per il polling di wait).
1751
+ function runExitCode(command, ctx) {
1752
+ ctx.signal.throwIfAborted();
1753
+ const { shell, args } = resolveShell(command);
1754
+ return new Promise((resolve) => {
1755
+ const child = spawn(shell, args, { cwd: ctx.cwd, windowsHide: true, env: shellEnvironment() });
1756
+ const onAbort = () => { try {
1757
+ child.kill();
1758
+ }
1759
+ catch { /* gia' morto */ } };
1760
+ ctx.signal.addEventListener("abort", onAbort, { once: true });
1761
+ const done = (code) => { ctx.signal.removeEventListener("abort", onAbort); resolve(code); };
1762
+ child.on("error", () => done(1));
1763
+ child.on("exit", (code) => done(code ?? 1));
1764
+ });
1765
+ }
1766
+ // wait: attende (deploy/CI/servizi). Con `until` fa POLLING con retry: ripete un
1767
+ // comando di check a intervalli finche' non riesce (exit 0). Senza, dorme `seconds`.
1768
+ export const waitTool = {
1769
+ name: "wait",
1770
+ description: "Attende prima di riprovare (utile dopo un deploy/push, per CI o servizi che si avviano). " +
1771
+ "Con `until` esegue un comando di check a intervalli finche' non riesce (exit 0): retry automatico. " +
1772
+ "Senza `until`, attende solo `seconds`. Interrompibile con Esc.",
1773
+ schema: {
1774
+ type: "object",
1775
+ properties: {
1776
+ seconds: { type: "number", description: "secondi di attesa se non c'e' until (default 5, max 600)" },
1777
+ until: { type: "string", description: "comando shell di check: si ripete finche' exit 0" },
1778
+ interval: { type: "number", description: "secondi tra i tentativi (default 10, max 300)" },
1779
+ attempts: { type: "number", description: "tentativi massimi (default 12, max 120)" },
1780
+ },
1781
+ },
1782
+ handler: async (input, ctx) => {
1783
+ if (ctx.signal.aborted)
1784
+ return toolError("attesa interrotta");
1785
+ const until = typeof input["until"] === "string" ? input["until"].trim() : "";
1786
+ if (!until) {
1787
+ const secs = clampNum(input["seconds"], 5, 1, 600);
1788
+ try {
1789
+ await sleep(secs * 1000, ctx.signal);
1790
+ return `atteso ${secs}s`;
1791
+ }
1792
+ catch {
1793
+ return toolError("attesa interrotta");
1794
+ }
1795
+ }
1796
+ const ok = await ctx.confirm("bash", `wait until: ${until}`);
1797
+ ctx.signal.throwIfAborted();
1798
+ if (!ok)
1799
+ return toolError("attesa con check rifiutata dall'utente");
1800
+ const interval = clampNum(input["interval"], 10, 1, 300);
1801
+ const attempts = clampNum(input["attempts"], 12, 1, 120);
1802
+ for (let i = 1; i <= attempts; i += 1) {
1803
+ const code = await runExitCode(until, ctx);
1804
+ ctx.signal.throwIfAborted();
1805
+ if (code === 0)
1806
+ return `pronto al tentativo ${i}/${attempts} (check OK dopo ~${(i - 1) * interval}s)`;
1807
+ if (i < attempts) {
1808
+ try {
1809
+ await sleep(interval * 1000, ctx.signal);
1810
+ }
1811
+ catch {
1812
+ return toolError(`attesa interrotta al tentativo ${i}/${attempts}`);
1813
+ }
1814
+ }
1815
+ }
1816
+ return `non pronto dopo ${attempts} tentativi (~${attempts * interval}s): «${until}» non e' ancora riuscito (riprova o aumenta attempts/interval)`;
1817
+ },
1818
+ };
1819
+ function fmtMsg(m) {
1820
+ return `[${m.from}${m.to ? `→${m.to}` : "→tutti"}] ${m.text}`;
1821
+ }
1822
+ // Risolve il corpo di una lista di skill dal disco (parita' hook plugin che
1823
+ // inietta la skill custom al volo). Ritorna i corpi delimitati, o "" se nessuna.
1824
+ function resolveSkillBodies(cwd, slugs) {
1825
+ const all = discoverSkills(cwd);
1826
+ const parts = [];
1827
+ for (const slug of slugs) {
1828
+ const s = findSkill(all, slug);
1829
+ if (s)
1830
+ parts.push(`----- BEGIN SKILL ${s.name} -----\n${s.body}\n----- END SKILL ${s.name} -----`);
1831
+ }
1832
+ return clamp(parts.join("\n\n"));
1833
+ }
1834
+ // Pre-carica il CONTENUTO dei file abbinati a una stanza (@file): l'utente li ha
1835
+ // allegati al piano perche' servono ALLA stanza, quindi vengono iniettati come
1836
+ // se gia' letti (paginati in base al budget di contesto) invece di costringere
1837
+ // l'agente a un read_file (spreco di token). Il resto e' raggiungibile con
1838
+ // output_page senza rileggere il file.
1839
+ async function resolveFileBodies(cwd, files, fsRoots, ctx) {
1840
+ const parts = [];
1841
+ for (const raw of files) {
1842
+ try {
1843
+ const abs = await safePath(cwd, raw, fsRoots);
1844
+ if (!existsSync(abs)) {
1845
+ parts.push(`----- FILE ${raw} (non trovato) -----`);
1846
+ continue;
1847
+ }
1848
+ const content = await readFile(abs, "utf8");
1849
+ parts.push(`----- BEGIN FILE ${raw} -----\n${paginate(content, `file ${raw}`, ctx)}\n----- END FILE ${raw} -----`);
1850
+ }
1851
+ catch (err) {
1852
+ parts.push(`----- FILE ${raw} (errore: ${err.message}) -----`);
1853
+ }
1854
+ }
1855
+ return parts.join("\n\n");
1856
+ }
1857
+ // vascend_mark: illumina una stanza (o la segna fallita).
1858
+ export const vascendMarkTool = {
1859
+ name: "vascend_mark",
1860
+ description: "Aggiorna una attivita: running avvia/riprende, completed conclude, failed registra un blocco, pending rimette in attesa. " +
1861
+ "Un solo running per gruppo; le dipendenze after devono essere concluse. Riaprire richiede nuove prove. " +
1862
+ "Per completed devi allegare evidence concreta (file modificati, test/comando e risultato osservato). " +
1863
+ "Indica la attivita per id (es. \"T02\") o numero bit (0-based), e il `group` se non e' \"master\". Richiede un piano creato con vascend_plan.",
1864
+ schema: {
1865
+ type: "object",
1866
+ properties: {
1867
+ group: { type: "string", description: "slug del gruppo (default \"master\")" },
1868
+ task: { type: "string", description: "id attivita (T01) o numero bit (0-based)" },
1869
+ status: { type: "string", enum: ["pending", "running", "completed", "failed"], description: "default: completed" },
1870
+ evidence: { type: "string", description: "Prova concreta: file/path, test o comando eseguito e relativo risultato. Obbligatoria per completed." },
1871
+ },
1872
+ required: ["task"],
1873
+ },
1874
+ handler: async (input, ctx) => {
1875
+ const sid = planScope(ctx);
1876
+ if (!ctx.sessionDir || !sid)
1877
+ return "piano non disponibile in questo contesto";
1878
+ const store = VascendPlanStore.open(ctx.sessionDir, sid, { strict: true });
1879
+ if (!store.hasAny())
1880
+ return "nessun gruppo pianificato: usa prima vascend_plan";
1881
+ const st = String(input["status"] ?? "completed");
1882
+ const valid = ["pending", "running", "completed", "failed"];
1883
+ if (!valid.includes(st))
1884
+ return { output: "errore: status deve essere pending, running, completed o failed", isError: true };
1885
+ const slug = String(input["group"] ?? input["castle"] ?? ctx.planCastle ?? ctx.kingdom?.self ?? "master").trim() || "master";
1886
+ const taskRef = String(input["task"] ?? "");
1887
+ const evidence = String(input["evidence"] ?? "").trim();
1888
+ const existing = store.castle(slug)?.tasks.find((task) => task.id.toLowerCase() === taskRef.trim().toLowerCase() || String(task.bit) === taskRef.trim());
1889
+ if (!existing)
1890
+ return { output: `attivita non trovata: ${slug}/${taskRef}`, isError: true };
1891
+ if (ctx.planCastle && (ctx.planCastle !== slug || (ctx.planTasks?.length && !ctx.planTasks.includes(existing.id)))) {
1892
+ return { output: "attivita fuori dal mandato assegnato: coordina il cambiamento con team_say", isError: true };
1893
+ }
1894
+ if (st === "running" || st === "completed") {
1895
+ const blockers = planBlockers(store.castles(), slug.trim() || "master");
1896
+ if (blockers.length)
1897
+ return { output: `attivita bloccata dalle dipendenze: ${blockers.map((blocker) => `${blocker.castle} (${blocker.reason})`).join(", ")}. Usa vascend_status.`, isError: true };
1898
+ }
1899
+ if (st === "running") {
1900
+ const running = store.castle(slug)?.tasks.find((task) => task.status === "running" && task.id !== existing.id
1901
+ && (!ctx.planTasks?.length || ctx.planTasks.includes(task.id)));
1902
+ if (running)
1903
+ return { output: `attivita ${running.id} gia' running: completala oppure rimettila pending prima di avviare ${existing.id}`, isError: true };
1904
+ }
1905
+ if (st === "completed" && !isConcreteTaskEvidence(evidence) && !hasTaskEvidence(existing)) {
1906
+ return "attivita NON completata: `evidence` obbligatoria e concreta (indica file/path, test/comando e risultato osservato)";
1907
+ }
1908
+ const hit = store.mark(slug, taskRef, st, evidence || undefined);
1909
+ if (!hit)
1910
+ return `attivita non trovata: ${slug}/${taskRef}`;
1911
+ let out = `[${slug}] ${hit.id} ${hit.mask} -> ${hit.status} (${hit.title})`;
1912
+ // All'avvio della stanza: richiama skill abbinate (in anticipo) e file da
1913
+ // pre-caricare, cosi' l'agente le applica subito (parita' hook plugin).
1914
+ if (st === "running") {
1915
+ const castle = store.castle(slug);
1916
+ const bound = [...new Set([...(hit.skills ?? []), ...(castle?.skills ?? [])])];
1917
+ if (bound.length) {
1918
+ const bodies = resolveSkillBodies(ctx.cwd, bound);
1919
+ out += `\nskill abbinate: ${bound.join(", ")}`;
1920
+ if (bodies)
1921
+ out += `\n${bodies}`;
1922
+ else
1923
+ out += " (non risolte sul disco: caricane il contenuto se servono)";
1924
+ }
1925
+ if (hit.files?.length) {
1926
+ const bodies = await resolveFileBodies(ctx.cwd, hit.files, ctx.fsRoots, ctx);
1927
+ out += `\nfile pre-caricati per questa attivita (GIA' in contesto qui sotto, NON rileggerli con read_file):\n${bodies}`;
1928
+ }
1929
+ }
1930
+ // The runtime handles this checkpoint after the complete tool batch.
1931
+ if (st === "completed" && hit.compact) {
1932
+ out += "\nCheckpoint @compact raggiunto: il runtime valuta la compattazione dopo questo batch, conservando piano aperto e lavoro recente.";
1933
+ }
1934
+ return out;
1935
+ },
1936
+ };
1937
+ // Ranking per keyword: punteggio = quante parole della query compaiono in
1938
+ // name+description (case-insensitive). Deterministico, zero dipendenze.
1939
+ function keywordScore(query, text) {
1940
+ const terms = query.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length > 1);
1941
+ if (!terms.length)
1942
+ return 1;
1943
+ const hay = text.toLowerCase();
1944
+ let score = 0;
1945
+ for (const t of terms)
1946
+ if (hay.includes(t))
1947
+ score += 1;
1948
+ return score;
1949
+ }
1950
+ // tool_search cerca nell'inventario completo. Anche i tool diretti devono
1951
+ // risultare: il modello puo' cercare una capability gia' nel prompt per
1952
+ // verificarne il nome, e "nessun match" in quel caso sarebbe una bugia.
1953
+ // Firma leggibile: nome piu' parametri (quelli opzionali con "?"). Al modello
1954
+ // serve sapere COSA passare, non l'intero JSONSchema: quello arriva nella lista
1955
+ // strumenti dopo la promozione, dove sta nel prefisso cacheabile.
1956
+ function toolSignature(def) {
1957
+ const schema = (def.inputSchema ?? {});
1958
+ const required = new Set((schema.required ?? []).map(String));
1959
+ const keys = Object.keys(schema.properties ?? {});
1960
+ const params = keys.slice(0, 8).map((key) => (required.has(key) ? key : `${key}?`));
1961
+ if (keys.length > params.length)
1962
+ params.push("…");
1963
+ return `${def.name}(${params.join(", ")})`;
1964
+ }
1965
+ function toolHeadline(def, max = 150) {
1966
+ const text = String(def.description ?? "").replace(/\s+/g, " ").trim();
1967
+ const sentence = text.split(/(?<=[.!?])\s/)[0] ?? text;
1968
+ const line = sentence.length >= 24 ? sentence : text;
1969
+ return line.length > max ? `${line.slice(0, max - 1)}…` : line;
1970
+ }
1971
+ export const toolSearchTool = {
1972
+ name: "tool_search",
1973
+ description: "Trova e attiva strumenti: names per nomi esatti, query per capacita'. " +
1974
+ "Carica in una chiamata quelli che servono, poi chiamali direttamente; gli schemi " +
1975
+ "entrano nella lista strumenti, senza richiederli di nuovo. Senza names/query elenca " +
1976
+ "l'inventario paginato senza attivarlo; usa offset=next_offset per continuare.",
1977
+ schema: {
1978
+ type: "object",
1979
+ properties: {
1980
+ names: {
1981
+ type: "array",
1982
+ items: { type: "string" },
1983
+ description: "nomi esatti da caricare in un colpo solo, se li conosci",
1984
+ },
1985
+ query: { type: "string", description: "cosa ti serve fare, se non conosci il nome" },
1986
+ limit: { type: "integer", minimum: 1, description: "risultati: query default 5/max 10; elenco default 40/max 100" },
1987
+ offset: { type: "integer", minimum: 0, description: "solo elenco: riprendi da next_offset" },
1988
+ },
1989
+ },
1990
+ handler: async (input, ctx) => {
1991
+ if (ctx.signal?.aborted)
1992
+ return { output: "ricerca annullata", isError: true };
1993
+ if (!ctx.tools)
1994
+ return "catalogo non disponibile in questo contesto";
1995
+ const inventory = toolInventory(ctx.tools);
1996
+ const names = Array.isArray(input["names"])
1997
+ ? [...new Set(input["names"].filter((value) => typeof value === "string")
1998
+ .map((value) => value.trim()).filter(Boolean))].slice(0, 24)
1999
+ : [];
2000
+ const query = String(input["query"] ?? "").trim();
2001
+ if (!names.length && !query) {
2002
+ const requested = input["limit"];
2003
+ const limit = typeof requested === "number" && Number.isFinite(requested)
2004
+ ? Math.max(1, Math.min(100, Math.floor(requested))) : 40;
2005
+ return toolListTool.handler({ limit, offset: input["offset"] ?? 0 }, ctx);
2006
+ }
2007
+ const requested = input["limit"];
2008
+ const limit = typeof requested === "number" && Number.isFinite(requested)
2009
+ ? Math.max(1, Math.min(10, Math.floor(requested))) : 5;
2010
+ const byName = new Map(inventory.map((def) => [def.name, def]));
2011
+ const exact = names.map((name) => byName.get(name)).filter((def) => !!def);
2012
+ const unknown = names.filter((name) => !byName.has(name));
2013
+ const ranked = query
2014
+ ? inventory
2015
+ .map((def) => ({ def, score: def.name.toLowerCase() === query.toLowerCase()
2016
+ ? Number.MAX_SAFE_INTEGER : keywordScore(query, `${def.name} ${def.description}`) }))
2017
+ .filter((row) => row.score > 0)
2018
+ .sort((a, b) => b.score - a.score)
2019
+ .slice(0, limit)
2020
+ .map((row) => row.def)
2021
+ : [];
2022
+ const found = [...new Map([...exact, ...ranked].map((def) => [def.name, def])).values()].slice(0, 24);
2023
+ if (!found.length) {
2024
+ const what = names.length ? names.join(", ") : query;
2025
+ return `nessuno strumento per "${what.slice(0, 400)}". `
2026
+ + "Usa tool_search senza names/query per consultare l'inventario paginato.";
2027
+ }
2028
+ // Promozione: da qui in avanti sono tool come tutti gli altri. Se l'host non
2029
+ // la supporta si torna al vecchio comportamento (schema nella risposta),
2030
+ // altrimenti il modello non saprebbe come chiamarli.
2031
+ const canPromote = !ctx.tools.compact && typeof ctx.tools.promote === "function";
2032
+ const promoted = [];
2033
+ if (canPromote) {
2034
+ for (const def of found) {
2035
+ if (ctx.tools.promote(def.name))
2036
+ promoted.push(def.name);
2037
+ }
2038
+ }
2039
+ const head = canPromote
2040
+ ? (found.length === 1
2041
+ ? "1 strumento pronto all'uso: chiamalo direttamente per nome."
2042
+ : `${found.length} strumenti pronti all'uso: chiamali direttamente per nome.`)
2043
+ : `${found.length === 1 ? "1 strumento trovato" : `${found.length} strumenti trovati`}.`;
2044
+ const lines = found.map((def) => `- ${toolSignature(def)} — ${toolHeadline(def)}`);
2045
+ const tail = unknown.length ? [`Nomi non trovati: ${unknown.join(", ")}.`] : [];
2046
+ if (!canPromote) {
2047
+ return JSON.stringify(found.map((def) => ({
2048
+ name: def.name,
2049
+ description: def.description,
2050
+ inputSchema: def.inputSchema,
2051
+ ...(def.outputSchema ? { outputSchema: def.outputSchema } : {}),
2052
+ ...(def.annotations ? { annotations: def.annotations } : {}),
2053
+ ...(def._meta ? { _meta: def._meta } : {}),
2054
+ })), null, 2);
2055
+ }
2056
+ return [head, ...lines, ...tail].join("\n");
2057
+ },
2058
+ };
2059
+ // tool_invoke: esegue un tool del catalogo (deferred) per nome. Il tool gira con
2060
+ // lo stesso ToolContext (permessi/conferme inclusi).
2061
+ export const toolInvokeTool = {
2062
+ name: "tool_invoke",
2063
+ description: "Esegue uno strumento trovato con tool_search, passando i suoi argomenti. " +
2064
+ "Usa `name` (es. \"web_fetch\") e `args` (oggetto conforme allo schema dello strumento).",
2065
+ schema: {
2066
+ type: "object",
2067
+ properties: {
2068
+ name: { type: "string", description: "nome dello strumento da eseguire" },
2069
+ args: { type: "object", description: "argomenti dello strumento" },
2070
+ },
2071
+ required: ["name"],
2072
+ },
2073
+ handler: async (input, ctx) => {
2074
+ if (ctx.signal.aborted)
2075
+ return { output: "chiamata annullata prima dell'esecuzione", isError: true };
2076
+ if (!ctx.tools)
2077
+ return "catalogo non disponibile in questo contesto";
2078
+ const name = String(input["name"] ?? "").trim();
2079
+ if (!name) {
2080
+ return toolError(missingParamError("name", input, '{"name":"read_file","args":{"path":"src/index.ts"}}'));
2081
+ }
2082
+ if (["tool_list", "tool_search", "tool_describe", "tool_invoke"].includes(name)) {
2083
+ return { output: "errore: non si puo' invocare un meta-tool", isError: true };
2084
+ }
2085
+ const tool = ctx.tools.get(name);
2086
+ if (!tool)
2087
+ return { output: `strumento sconosciuto: ${name} (usa tool_search per trovarlo)`, isError: true };
2088
+ const resolved = resolveInvokeArgs(input);
2089
+ if (typeof resolved === "string")
2090
+ return { output: resolved, isError: true };
2091
+ // Gli argomenti passano dalla stessa normalizzazione del dispatch diretto:
2092
+ // un `args` stringificato o tipi stringati non devono far fallire il tool.
2093
+ const args = normalizeToolInput(resolved, tool.inputSchema);
2094
+ return invokeToolCall({ type: "tool_call", id: "invoke-" + crypto.randomUUID(), name, input: args }, ctx);
2095
+ },
2096
+ };
2097
+ // Ricava gli argomenti da passare al tool bersaglio. Accetta le tre forme che i
2098
+ // modelli producono davvero: `args` oggetto, `args` stringa JSON, e argomenti
2099
+ // PIATTI accanto a `name`. Ritorna una stringa se l'input non e' interpretabile:
2100
+ // in quel caso e' un errore che deve spiegare il formato, non un {} silenzioso
2101
+ // che porterebbe il tool a lamentare un parametro mancante.
2102
+ function resolveInvokeArgs(input) {
2103
+ const raw = input["args"] ?? input["arguments"] ?? input["parameters"] ?? input["params"] ?? input["input"];
2104
+ if (raw === undefined || raw === null || raw === "") {
2105
+ const { name: _name, ...flat } = input;
2106
+ return flat;
2107
+ }
2108
+ if (typeof raw === "object" && !Array.isArray(raw))
2109
+ return raw;
2110
+ if (typeof raw === "string") {
2111
+ try {
2112
+ const parsed = JSON.parse(raw);
2113
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
2114
+ return parsed;
2115
+ }
2116
+ }
2117
+ catch {
2118
+ /* gestito sotto */
2119
+ }
2120
+ return ("errore: `args` deve essere un oggetto conforme allo schema dello strumento " +
2121
+ "(o una stringa JSON valida che lo rappresenti). Ricevuto: " +
2122
+ `${JSON.stringify(raw).slice(0, 120)}. ` +
2123
+ 'Esempio: {"name":"read_file","args":{"path":"src/index.ts"}}');
2124
+ }
2125
+ return `errore: \`args\` non valido (${typeof raw}): serve un oggetto con gli argomenti dello strumento.`;
2126
+ }
2127
+ // skill_search: trova le skill per query (name+description), senza caricarne il corpo.
2128
+ export const skillSearchTool = {
2129
+ name: "skill_search",
2130
+ description: "Cerca tra le skill disponibili (capacita' on-demand documentate). Ritorna nome e descrizione; " +
2131
+ "carica il contenuto con skill_load quando serve.",
2132
+ schema: {
2133
+ type: "object",
2134
+ properties: {
2135
+ query: { type: "string", description: "argomento della skill" },
2136
+ limit: { type: "number", description: "max risultati (default 5)" },
2137
+ },
2138
+ required: ["query"],
2139
+ },
2140
+ handler: async (input, ctx) => {
2141
+ const query = String(input["query"] ?? "");
2142
+ const limit = typeof input["limit"] === "number" && input["limit"] > 0 ? input["limit"] : 5;
2143
+ const ranked = discoverSkills(ctx.cwd)
2144
+ .filter((s) => !s.disableModelInvocation)
2145
+ .map((s) => ({ s, score: keywordScore(query, `${s.name} ${s.description}`) }))
2146
+ .filter((x) => x.score > 0)
2147
+ .sort((a, b) => b.score - a.score)
2148
+ .slice(0, limit)
2149
+ .map((x) => ({ name: x.s.name, description: x.s.description }));
2150
+ if (!ranked.length)
2151
+ return `nessuna skill per "${query}"`;
2152
+ return JSON.stringify(ranked, null, 2);
2153
+ },
2154
+ };
2155
+ // skill_load: carica il CORPO di una skill (markdown) on-demand.
2156
+ export const skillLoadTool = {
2157
+ name: "skill_load",
2158
+ description: "Carica il contenuto completo di una skill (trovata con skill_search) e applicalo.",
2159
+ schema: {
2160
+ type: "object",
2161
+ properties: { name: { type: "string", description: "nome della skill" } },
2162
+ required: ["name"],
2163
+ },
2164
+ handler: async (input, ctx) => {
2165
+ const name = String(input["name"] ?? "").trim();
2166
+ if (!name)
2167
+ return "errore: nome skill mancante";
2168
+ const skill = findSkill(discoverSkills(ctx.cwd), name);
2169
+ if (!skill)
2170
+ return `skill sconosciuta: ${name} (usa skill_search)`;
2171
+ return clamp(`# Skill: ${skill.name}\n\n${skill.body}`);
2172
+ },
2173
+ };
2174
+ // Tool DEFERRED: nel catalogo (scopribili via tool_search), NON nel system
2175
+ // prompt. Primitive di lavoro, domande, stato del piano e discovery restano CORE.
2176
+ export const DEFERRED_TOOLS = new Set([
2177
+ ...LEGACY_DISCOVERY_TOOL_NAMES,
2178
+ "web_fetch",
2179
+ "wait",
2180
+ "image_generate",
2181
+ "user_profile",
2182
+ "work_queue",
2183
+ "architecture_map",
2184
+ "vascend_note",
2185
+ "vascend_skill",
2186
+ "vascend_addtasks",
2187
+ "market_compare",
2188
+ // Attese/report vengono promossi dopo l'ingaggio; i piani a rami sono opzionali.
2189
+ "team_wait",
2190
+ "team_report",
2191
+ "ram_plan",
2192
+ "ram_take",
2193
+ "ram_submit",
2194
+ "ram_ask",
2195
+ "ram_judge",
2196
+ "ram_propose",
2197
+ "ram_decide",
2198
+ "ram_tell",
2199
+ "ram_hear",
2200
+ ]);
2201
+ // Quante volte lo STESSO recupero e' stato servito, per sessione. Rete di
2202
+ // sicurezza al ciclo: rileggere due volte e' legittimo (fra i due la finestra
2203
+ // puo' essersi compattata), dalla terza il contenuto e' certamente gia' li' e
2204
+ // ri-servirlo alimenta soltanto il giro a vuoto.
2205
+ const pageServed = new Map();
2206
+ const REPEAT_LIMIT = 2;
2207
+ function countRepeat(sessionId, id, page) {
2208
+ const key = `${sessionId ?? "-"}|${id}|${page}`;
2209
+ const next = (pageServed.get(key) ?? 0) + 1;
2210
+ pageServed.set(key, next);
2211
+ // Tetto alla mappa: e' un contatore di processo, non uno storico.
2212
+ if (pageServed.size > 512) {
2213
+ const oldest = pageServed.keys().next().value;
2214
+ if (oldest !== undefined)
2215
+ pageServed.delete(oldest);
2216
+ }
2217
+ return next;
2218
+ }
2219
+ // Esposto per i test: il contatore e' di processo e va azzerabile.
2220
+ export function resetOutputPageCounters() {
2221
+ pageServed.clear();
2222
+ }
2223
+ // output_page: recupera una pagina di un output lungo paginato (invece del
2224
+ // troncamento). Sempre CORE: il marker di paginazione cita questo tool.
2225
+ export const outputPageTool = {
2226
+ name: "output_page",
2227
+ description: "Recupera una pagina di un output lungo (paginato invece che troncato) oppure " +
2228
+ "il testo integrale di un risultato archiviato. Usa l'`id` indicato nel marker " +
2229
+ "(\"pg3\" per un output paginato, \"tr7\" per un risultato archiviato) e la `page`.",
2230
+ schema: {
2231
+ type: "object",
2232
+ properties: {
2233
+ id: { type: "string", description: "id della paginazione (pg3) o dell'archivio (tr7)" },
2234
+ page: { type: "number", description: "numero di pagina (default 2; per gli id tr* default 1)" },
2235
+ scope: { type: "string", description: "scope dell'artefatto condiviso, indicato in team_report" },
2236
+ query: { type: "string", description: "ricerca letterale nel testo integrale; restituisce estratti con offset" },
2237
+ offset: { type: "number", description: "offset carattere iniziale, anche per proseguire una ricerca" },
2238
+ limit: { type: "number", description: "caratteri massimi di testo per ricerca/slice (default 8000, max 30000)" },
2239
+ format: { type: "string", enum: ["text", "metadata", "structured"], description: "structured espone dati originali a tools.$result; metadata espone hash e dimensione" },
2240
+ },
2241
+ required: ["id"],
2242
+ },
2243
+ handler: async (input, ctx) => {
2244
+ const id = String(input["id"] ?? "").trim();
2245
+ if (!id)
2246
+ return "errore: id mancante";
2247
+ const requested = typeof input["page"] === "number" && input["page"] > 0 ? input["page"] : 0;
2248
+ // Gli id `tr*` sono risultati retrocessi a riferimento: il default e' la
2249
+ // PRIMA pagina, perche' del corpo non c'e' piu' niente nel contesto.
2250
+ if (/^tr\d+$/.test(id)) {
2251
+ const page = requested || 1;
2252
+ if (!input["scope"] && !input["query"] && !input["format"] && input["offset"] === undefined && input["limit"] === undefined
2253
+ && ctx.visibleOutputPages?.has(id + ":" + page) && countRepeat(ctx.sessionId, id, page) > REPEAT_LIMIT) {
2254
+ return `${id} pagina ${page} e' gia' stato recuperato in questa sessione e il contenuto e' piu' su `
2255
+ + "nella conversazione: non viene ripetuto. Se il materiale che ti serve c'e' gia', passa all'azione "
2256
+ + "(edit_file / write_file); se ti serve altro, leggi la fonte con read_file o grep invece di "
2257
+ + "rileggere l'archivio.";
2258
+ }
2259
+ return outputArtifact(input, ctx);
2260
+ }
2261
+ const page = requested || 2;
2262
+ if (ctx.visibleOutputPages?.has(id + ":" + page) && countRepeat(ctx.sessionId, id, page) > REPEAT_LIMIT) {
2263
+ return `${id} pagina ${page} e' gia' stato recuperato in questa sessione: il contenuto e' piu' su nella `
2264
+ + "conversazione e non viene ripetuto. Passa all'azione, oppure rileggi la fonte con read_file/grep.";
2265
+ }
2266
+ const out = pageStore.page(id, page);
2267
+ return out ?? `pagina non disponibile: ${id} (scaduta o inesistente; rigenera l'output)`;
2268
+ },
2269
+ };
2270
+ // architecture_map: mappa architetturale del workspace, disponibile on demand.
2271
+ // con cui l'agente si orienta in un progetto sconosciuto e verifica l'impatto
2272
+ // architetturale (cicli, confini fra moduli) prima e dopo una modifica.
2273
+ export const architectureMapTool = {
2274
+ name: "architecture_map",
2275
+ description: "Mappa architetturale del progetto: moduli (per directory), dipendenze interne, cicli, " +
2276
+ "entry point, hotspot e dipendenze esterne. Usala per orientarti in un codebase o per " +
2277
+ "verificare l'impatto architetturale di una modifica (nuovi cicli, confini violati).",
2278
+ schema: {
2279
+ type: "object",
2280
+ properties: {
2281
+ path: { type: "string", description: "radice da analizzare (default cwd)" },
2282
+ depth: { type: "number", description: "profondita' del raggruppamento in moduli (default 2)" },
2283
+ top: { type: "number", description: "quanti hotspot/dipendenze esterne mostrare (default 10)" },
2284
+ },
2285
+ },
2286
+ handler: async (input, ctx) => {
2287
+ let root;
2288
+ try {
2289
+ root = await safePath(ctx.cwd, String(input["path"] ?? "."), ctx.fsRoots);
2290
+ }
2291
+ catch (err) {
2292
+ return `errore percorso: ${err.message}`;
2293
+ }
2294
+ const depth = typeof input["depth"] === "number" && input["depth"] >= 1 ? Math.floor(input["depth"]) : undefined;
2295
+ const top = typeof input["top"] === "number" && input["top"] >= 3 ? Math.floor(input["top"]) : undefined;
2296
+ try {
2297
+ const map = scanArchitectureMap(root, {
2298
+ ...(depth !== undefined ? { moduleDepth: depth } : {}),
2299
+ ...(top !== undefined ? { top } : {}),
2300
+ });
2301
+ if (!map.files)
2302
+ return `nessun file sorgente riconosciuto sotto: ${root}`;
2303
+ return paginate(renderArchitectureMap(map), `arch ${root}`, ctx) || "(mappa vuota)";
2304
+ }
2305
+ catch (err) {
2306
+ return toolError(`errore analisi architettura: ${err.message}`);
2307
+ }
2308
+ },
2309
+ };
2310
+ // market_compare: confronta un output (set di feature/capabilities) con gli
2311
+ // standard di mercato e le best practice di settore per un dominio, e ritorna
2312
+ // score, gap e riferimenti ai concorrenti. DEFERRED (scopribile via
2313
+ // tool_search): non occupa il system prompt finche' non serve.
2314
+ export const marketCompareTool = {
2315
+ name: "market_compare",
2316
+ description: "Confronta l'output prodotto con gli standard di mercato e le best practice di settore. " +
2317
+ "Passa `domain` (es. auth, billing, observability, saas-backend) e `features` (lista di " +
2318
+ "capabilities/feature prodotte, per id come \"auth.oauth2\" o testo libero). " +
2319
+ "Senza `features` ritorna lo standard del dominio (best practice + concorrenti). " +
2320
+ "Senza `domain` elenca i domini coperti dalla knowledge base.",
2321
+ schema: {
2322
+ type: "object",
2323
+ properties: {
2324
+ domain: { type: "string", description: "dominio di mercato (es. auth, billing, observability)" },
2325
+ features: {
2326
+ type: "array",
2327
+ items: { type: "string" },
2328
+ description: "feature/capabilities prodotte da confrontare con lo standard",
2329
+ },
2330
+ },
2331
+ },
2332
+ handler: async (input) => {
2333
+ const domain = typeof input["domain"] === "string" ? input["domain"].trim() : "";
2334
+ if (!domain) {
2335
+ const doms = knowledgeBase.domains();
2336
+ return `domini coperti: ${doms.join(", ")}\n(passa { domain, features } per il confronto)`;
2337
+ }
2338
+ const features = Array.isArray(input["features"])
2339
+ ? input["features"].map(String).map((s) => s.trim()).filter(Boolean)
2340
+ : [];
2341
+ if (!features.length) {
2342
+ const std = knowledgeBase.marketStandard(domain);
2343
+ if (!std.practices.length && !std.competitors.length)
2344
+ return `nessuno standard noto per il dominio "${domain}"`;
2345
+ const lines = [];
2346
+ lines.push(`@standard[${domain}]: ${std.practices.length} best practice, ${std.competitors.length} concorrenti`);
2347
+ for (const pr of std.practices)
2348
+ lines.push(` practice ${pr.id} (${pr.importance}) "${pr.title}" -> ${pr.expectation}`);
2349
+ for (const c of std.competitors)
2350
+ lines.push(` concorrente ${c.id} "${c.name}": ${c.summary}`);
2351
+ return lines.join("\n");
2352
+ }
2353
+ const v = knowledgeBase.compareToMarket(domain, features);
2354
+ return v.summary;
2355
+ },
2356
+ };
2357
+ // Profilo dell'utente: i fatti che non vanno riscoperti a ogni sessione. Sta nel
2358
+ // catalogo (si usa raramente) ma il nudge di persistenza lo nomina, quindi il
2359
+ // modello lo trova quando serve.
2360
+ export const userProfileTool = {
2361
+ name: "user_profile",
2362
+ description: "Ricorda (o dimentica) un fatto DUREVOLE sulla persona con cui lavori: preferenze di lavoro, "
2363
+ + "decisioni gia' prese da non ridiscutere, vincoli che valgono sempre. Non usarlo per lo stato "
2364
+ + "del progetto (quello va in vascend_note) ne' per dati sensibili. I fatti ricordati entrano "
2365
+ + "all'inizio delle sessioni successive: scrivi frasi brevi e verificabili, non impressioni.",
2366
+ schema: {
2367
+ type: "object",
2368
+ properties: {
2369
+ action: { type: "string", enum: ["remember", "list", "forget"], description: "default remember" },
2370
+ fact: { type: "string", description: "il fatto, in una frase (per remember)" },
2371
+ kind: { type: "string", description: "preference | decision | constraint | context (default preference)" },
2372
+ match: { type: "string", description: "testo o id del fatto da dimenticare (per forget)" },
2373
+ },
2374
+ },
2375
+ handler: async (input, ctx) => {
2376
+ if (!ctx.sessionDir)
2377
+ return "profilo utente non disponibile in questo contesto";
2378
+ const action = String(input["action"] ?? "remember").toLowerCase();
2379
+ if (action === "list") {
2380
+ const profile = readUserProfile(ctx.sessionDir);
2381
+ if (!profile.facts.length)
2382
+ return "nessun fatto ricordato sulla persona";
2383
+ return formatUserProfile(profile, 40);
2384
+ }
2385
+ if (action === "forget") {
2386
+ const match = String(input["match"] ?? input["fact"] ?? "");
2387
+ const done = forgetUserFact(ctx.sessionDir, match);
2388
+ if (!done.removed)
2389
+ return `nessun fatto corrisponde a: ${match}`;
2390
+ return `dimenticat${done.removed === 1 ? "o 1 fatto" : `i ${done.removed} fatti`}`;
2391
+ }
2392
+ const fact = String(input["fact"] ?? "");
2393
+ if (!fact.trim())
2394
+ return "indica `fact`: il fatto da ricordare, in una frase";
2395
+ const done = rememberUserFact(ctx.sessionDir, fact, String(input["kind"] ?? "preference"));
2396
+ if (!done.ok)
2397
+ return "non riesco a scrivere il profilo utente";
2398
+ return done.reinforced
2399
+ ? `fatto rinforzato (${done.profile.facts.length} in totale)`
2400
+ : `fatto ricordato (${done.profile.facts.length} in totale)`;
2401
+ },
2402
+ };
2403
+ // work_queue: la coda del lavoro autonomo. Serve a due cose opposte e
2404
+ // complementari: mettere in coda cio' che emerge (una scoperta, un gate rosso,
2405
+ // una richiesta dell'utente) senza interrompere quello che si sta facendo, e
2406
+ // guardare lo stato del ciclo. Non esegue: l'esecuzione e' del runner, che apre
2407
+ // un agente nuovo per ogni unita'.
2408
+ export const workQueueTool = {
2409
+ name: "work_queue",
2410
+ description: "Coda del lavoro autonomo: accoda un'unita' di lavoro (`add`) o guarda lo stato (`list`). "
2411
+ + "Ogni unita' viene eseguita da un agente NUOVO che vede solo il suo brief, quindi il brief "
2412
+ + "deve bastare a lavorare da zero: obiettivo, file coinvolti, come si verifica. "
2413
+ + "Usala quando trovi lavoro che non appartiene a quello che stai facendo adesso.",
2414
+ schema: {
2415
+ type: "object",
2416
+ properties: {
2417
+ action: { type: "string", enum: ["add", "list"], description: "default add" },
2418
+ title: { type: "string", description: "titolo breve dell'unita' (per add)" },
2419
+ brief: { type: "string", description: "briefing autosufficiente: obiettivo, contesto, criterio di verifica" },
2420
+ after: { type: "array", items: { type: "string" }, description: "id delle unita' che devono concludersi prima" },
2421
+ source: { type: "string", description: "da dove viene: utente, verifica, scoperta" },
2422
+ },
2423
+ },
2424
+ handler: async (input, ctx) => {
2425
+ if (!ctx.sessionDir)
2426
+ return "coda di lavoro non disponibile in questo contesto";
2427
+ const scope = ctx.planStoreId ?? ctx.roomStoreId ?? ctx.sessionId ?? "default";
2428
+ const queue = WorkQueue.open(ctx.sessionDir, scope);
2429
+ const action = String(input["action"] ?? "add").toLowerCase();
2430
+ if (action === "list") {
2431
+ queue.sweep();
2432
+ return formatQueue(queue.items());
2433
+ }
2434
+ const title = String(input["title"] ?? "").trim();
2435
+ const brief = String(input["brief"] ?? "").trim();
2436
+ if (!title)
2437
+ return "indica `title`: il titolo dell'unita' di lavoro";
2438
+ if (brief.length < 20) {
2439
+ return "il `brief` deve bastare a un agente che non ha visto questa conversazione: "
2440
+ + "obiettivo, file coinvolti, come si verifica";
2441
+ }
2442
+ const after = Array.isArray(input["after"]) ? input["after"].map(String).slice(0, 12) : [];
2443
+ const item = queue.add({
2444
+ title,
2445
+ brief,
2446
+ ...(after.length ? { after } : {}),
2447
+ source: String(input["source"] ?? "agente"),
2448
+ });
2449
+ const tally = queue.counts();
2450
+ return `${item.id} in coda: ${item.title} (${tally.ready} pronti, ${tally.total} in totale)`;
2451
+ },
2452
+ };
2453
+ const legacyBuiltinTools = [
2454
+ ...teamTools,
2455
+ ...ramificationTools,
2456
+ userProfileTool,
2457
+ workQueueTool,
2458
+ toolSearchTool,
2459
+ marketCompareTool,
2460
+ toolInvokeTool,
2461
+ skillSearchTool,
2462
+ skillLoadTool,
2463
+ outputPageTool,
2464
+ bashTool,
2465
+ bashOutputTool,
2466
+ runProgramTool,
2467
+ workspaceSessionsTool,
2468
+ webFetchTool,
2469
+ askUserTool,
2470
+ waitTool,
2471
+ globTool,
2472
+ grepTool,
2473
+ readTool,
2474
+ fileActivityTool,
2475
+ writeTool,
2476
+ editTool,
2477
+ lsTool,
2478
+ recallTool,
2479
+ architectureMapTool,
2480
+ imageGenerateTool,
2481
+ vascendPlanTool,
2482
+ vascendStatusTool,
2483
+ vascendMarkTool,
2484
+ vascendNoteTool,
2485
+ vascendSkillTool,
2486
+ vascendAddRoomsTool,
2487
+ ];
2488
+ // Anche l'export SDK e' MCP-native. I singoli literal mantengono `schema` per
2489
+ // compatibilita' sorgente, ma il catalogo pubblico offre sempre inputSchema.
2490
+ export const builtinTools = legacyBuiltinTools.map((tool) => {
2491
+ const inputSchema = toolInputSchema(tool);
2492
+ return { ...tool, inputSchema, schema: inputSchema,
2493
+ capabilities: BUILTIN_CAPABILITIES[tool.name], continuations: BUILTIN_CONTINUATIONS[tool.name] };
2494
+ });