synabun 2.1.0 → 2.2.1

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 (332) hide show
  1. package/CHANGELOG.md +139 -0
  2. package/README.md +1 -1
  3. package/docs/whatsapp.md +11 -11
  4. package/lib/client-config-repair.js +13 -9
  5. package/lib/external-tools.js +6 -5
  6. package/lib/update-snapshot.js +77 -3
  7. package/lib/virustotal.mjs +12 -1
  8. package/mcp-server/dist/index.js +1 -1
  9. package/mcp-server/dist/index.js.map +1 -1
  10. package/mcp-server/dist/services/budget-judgments.d.ts +1 -1
  11. package/mcp-server/dist/services/budget-judgments.d.ts.map +1 -1
  12. package/mcp-server/dist/services/budget-judgments.js +5 -3
  13. package/mcp-server/dist/services/budget-judgments.js.map +1 -1
  14. package/mcp-server/dist/services/identity.d.ts +2 -2
  15. package/mcp-server/dist/services/identity.js +2 -2
  16. package/mcp-server/dist/services/memory-sessions.d.ts +1 -1
  17. package/mcp-server/dist/services/memory-sessions.d.ts.map +1 -1
  18. package/mcp-server/dist/services/memory-sessions.js +28 -2
  19. package/mcp-server/dist/services/memory-sessions.js.map +1 -1
  20. package/mcp-server/dist/services/neural-interface.d.ts +2 -1
  21. package/mcp-server/dist/services/neural-interface.d.ts.map +1 -1
  22. package/mcp-server/dist/services/neural-interface.js.map +1 -1
  23. package/mcp-server/dist/services/server-instructions.d.ts.map +1 -1
  24. package/mcp-server/dist/services/server-instructions.js +2 -1
  25. package/mcp-server/dist/services/server-instructions.js.map +1 -1
  26. package/mcp-server/dist/services/tool-catalog.js +1 -1
  27. package/mcp-server/dist/services/tool-catalog.js.map +1 -1
  28. package/mcp-server/dist/services/tool-list.d.ts +1 -1
  29. package/mcp-server/dist/services/tool-list.d.ts.map +1 -1
  30. package/mcp-server/dist/services/tool-runtime.d.ts +2 -2
  31. package/mcp-server/dist/services/tool-runtime.d.ts.map +1 -1
  32. package/mcp-server/dist/services/tool-runtime.js +1 -1
  33. package/mcp-server/dist/services/tool-runtime.js.map +1 -1
  34. package/mcp-server/dist/services/typesafe-config.js +1 -1
  35. package/mcp-server/dist/services/typesafe-config.js.map +1 -1
  36. package/mcp-server/dist/tools/agents.d.ts +8 -8
  37. package/mcp-server/dist/tools/agents.d.ts.map +1 -1
  38. package/mcp-server/dist/tools/agents.js +10 -9
  39. package/mcp-server/dist/tools/agents.js.map +1 -1
  40. package/mcp-server/dist/tools/choice.d.ts +6 -0
  41. package/mcp-server/dist/tools/choice.d.ts.map +1 -1
  42. package/mcp-server/dist/tools/choice.js +18 -13
  43. package/mcp-server/dist/tools/choice.js.map +1 -1
  44. package/mcp-server/dist/tools/git-tools.js +2 -2
  45. package/mcp-server/dist/tools/git-tools.js.map +1 -1
  46. package/mcp-server/dist/tools/morelogin.d.ts.map +1 -1
  47. package/mcp-server/dist/tools/morelogin.js +9 -6
  48. package/mcp-server/dist/tools/morelogin.js.map +1 -1
  49. package/mcp-server/dist/tools/skill-tools.d.ts +8 -8
  50. package/mcp-server/dist/tools/skill-tools.d.ts.map +1 -1
  51. package/mcp-server/dist/tools/skill-tools.js +11 -8
  52. package/mcp-server/dist/tools/skill-tools.js.map +1 -1
  53. package/mcp-server/package-lock.json +2 -2
  54. package/mcp-server/package.json +1 -1
  55. package/mcp-server/src/index.ts +1 -1
  56. package/mcp-server/src/services/budget-judgments.ts +5 -3
  57. package/mcp-server/src/services/identity.ts +2 -2
  58. package/mcp-server/src/services/memory-sessions.ts +24 -3
  59. package/mcp-server/src/services/neural-interface.ts +2 -1
  60. package/mcp-server/src/services/server-instructions.ts +2 -1
  61. package/mcp-server/src/services/tool-catalog.ts +1 -1
  62. package/mcp-server/src/services/tool-list.ts +1 -1
  63. package/mcp-server/src/services/tool-runtime.ts +2 -2
  64. package/mcp-server/src/services/typesafe-config.ts +1 -1
  65. package/mcp-server/src/tools/agents.ts +10 -9
  66. package/mcp-server/src/tools/choice.ts +17 -12
  67. package/mcp-server/src/tools/git-tools.ts +2 -2
  68. package/mcp-server/src/tools/morelogin.ts +6 -4
  69. package/mcp-server/src/tools/skill-tools.ts +11 -8
  70. package/neural-interface/i18n/en.json +504 -39
  71. package/neural-interface/i18n/pt-BR.json +504 -39
  72. package/neural-interface/lib/agent-sandbox-elevation.js +6 -0
  73. package/neural-interface/lib/antigravity/acp-client-host.js +554 -0
  74. package/neural-interface/lib/antigravity/acp-connection.js +299 -0
  75. package/neural-interface/lib/antigravity/bypass.js +100 -0
  76. package/neural-interface/lib/antigravity/catalog.js +273 -0
  77. package/neural-interface/lib/antigravity/home.js +260 -0
  78. package/neural-interface/lib/antigravity/migrate-gemini.js +139 -0
  79. package/neural-interface/lib/antigravity/panel-bridge.js +1058 -0
  80. package/neural-interface/lib/antigravity/policy-hook.mjs +78 -0
  81. package/neural-interface/lib/antigravity/policy.js +74 -0
  82. package/neural-interface/lib/antigravity/tool-call.js +178 -0
  83. package/neural-interface/lib/antigravity/transcripts.js +252 -0
  84. package/neural-interface/lib/antigravity/usage.js +43 -0
  85. package/neural-interface/lib/assistant-api.js +1 -0
  86. package/neural-interface/lib/assistant-brains/antigravity.js +423 -0
  87. package/neural-interface/lib/assistant-brains/codex.js +6 -1
  88. package/neural-interface/lib/assistant-budget-planner.js +7 -3
  89. package/neural-interface/lib/assistant-budget-split.js +42 -15
  90. package/neural-interface/lib/assistant-catalog.js +76 -9
  91. package/neural-interface/lib/assistant-config.js +6 -3
  92. package/neural-interface/lib/assistant-dispatch.js +211 -50
  93. package/neural-interface/lib/assistant-envelope.js +153 -1
  94. package/neural-interface/lib/assistant-evidence.js +80 -0
  95. package/neural-interface/lib/assistant-hidden-models.js +7 -5
  96. package/neural-interface/lib/assistant-persona.js +16 -11
  97. package/neural-interface/lib/assistant-plan-permissions.js +15 -0
  98. package/neural-interface/lib/assistant-router.js +237 -35
  99. package/neural-interface/lib/assistant-runtime.js +158 -12
  100. package/neural-interface/lib/assistant-task-prompt.js +10 -8
  101. package/neural-interface/lib/assistant-tools.js +2 -2
  102. package/neural-interface/lib/assistant-usage.js +29 -0
  103. package/neural-interface/lib/backup-service.js +6 -1
  104. package/neural-interface/lib/browser-guard-cdp.js +5 -4
  105. package/neural-interface/lib/browser-network-guard.js +274 -82
  106. package/neural-interface/lib/browser-tool-policy.js +12 -7
  107. package/neural-interface/lib/claude-agent-bridge.js +7 -0
  108. package/neural-interface/lib/claude-history-cache.js +210 -0
  109. package/neural-interface/lib/claude-model-catalog.js +5 -1
  110. package/neural-interface/lib/cli-install-api.js +1 -1
  111. package/neural-interface/lib/cli-install.js +160 -31
  112. package/neural-interface/lib/cli-resolver.js +9 -2
  113. package/neural-interface/lib/cli-update-planner.js +25 -15
  114. package/neural-interface/lib/codex-rollout-reader.js +52 -0
  115. package/neural-interface/lib/codex-worker-app-server.js +25 -6
  116. package/neural-interface/lib/editor-git-api.js +3 -20
  117. package/neural-interface/lib/exec-loop-command.js +9 -2
  118. package/neural-interface/lib/file-explorer-api.js +74 -40
  119. package/neural-interface/lib/file-explorer-watch.js +9 -3
  120. package/neural-interface/lib/git-bash/install.js +52 -6
  121. package/neural-interface/lib/git-bash/manager.js +59 -17
  122. package/neural-interface/lib/git-bash/pins.js +12 -0
  123. package/neural-interface/lib/git-read.js +2 -2
  124. package/neural-interface/lib/git-status-runner.js +390 -0
  125. package/neural-interface/lib/git-status-watchdog.js +40 -0
  126. package/neural-interface/lib/greeting/budgets.js +1 -0
  127. package/neural-interface/lib/greeting/engine.js +2 -2
  128. package/neural-interface/lib/greeting/sources.js +10 -12
  129. package/neural-interface/lib/greeting/writer.js +1 -1
  130. package/neural-interface/lib/mcp-client-config.js +37 -6
  131. package/neural-interface/lib/mcp-external-servers.js +40 -7
  132. package/neural-interface/lib/mcp-installer.js +2 -1
  133. package/neural-interface/lib/memory-session-capture.js +17 -3
  134. package/neural-interface/lib/morelogin.js +147 -25
  135. package/neural-interface/lib/native-loop-providers.js +423 -23
  136. package/neural-interface/lib/native-loop-runtime.js +23 -2
  137. package/neural-interface/lib/opencode-native-boundary.js +165 -22
  138. package/neural-interface/lib/owner-authority.js +1 -1
  139. package/neural-interface/lib/path-confine.js +12 -9
  140. package/neural-interface/lib/peer-identity.js +80 -6
  141. package/neural-interface/lib/pty-host/core.js +28 -1
  142. package/neural-interface/lib/pty-host/lifetime.js +12 -0
  143. package/neural-interface/lib/pty-host/manager.js +12 -1
  144. package/neural-interface/lib/remote-policy.js +1 -1
  145. package/neural-interface/lib/request-gate.js +3 -0
  146. package/neural-interface/lib/rulesets/api.js +6 -5
  147. package/neural-interface/lib/rulesets/fs-safe.js +1 -1
  148. package/neural-interface/lib/rulesets/installer.js +89 -10
  149. package/neural-interface/lib/rulesets/render.js +1 -1
  150. package/neural-interface/lib/session-cache-builder.js +64 -1
  151. package/neural-interface/lib/skills/catalog.js +5 -1
  152. package/neural-interface/lib/skills/creation-brief.js +1 -1
  153. package/neural-interface/lib/skills/creation-sources.js +19 -1
  154. package/neural-interface/lib/skills/deploy.js +9 -4
  155. package/neural-interface/lib/skills/draft-confinement.js +3 -1
  156. package/neural-interface/lib/skills/eval-confinement.js +3 -1
  157. package/neural-interface/lib/skills/eval-trace.js +16 -0
  158. package/neural-interface/lib/skills/export.js +5 -2
  159. package/neural-interface/lib/skills/known-bridge-digests.json +58 -4
  160. package/neural-interface/lib/skills/ownership.js +1 -1
  161. package/neural-interface/lib/skills/roots.js +10 -3
  162. package/neural-interface/lib/skills/runtime-checks.js +6 -3
  163. package/neural-interface/lib/skills/service.js +15 -7
  164. package/neural-interface/lib/skills/telemetry.js +6 -0
  165. package/neural-interface/lib/synabun-version.js +1 -1
  166. package/neural-interface/lib/system-activity.js +1 -1
  167. package/neural-interface/lib/whatsapp/brain.js +7 -3
  168. package/neural-interface/lib/whatsapp/cards.js +22 -9
  169. package/neural-interface/lib/whatsapp/config.js +1 -1
  170. package/neural-interface/lib/worker-confinement.js +77 -1
  171. package/neural-interface/lib/worker-question.js +74 -0
  172. package/neural-interface/lib/worker-resume.js +13 -4
  173. package/neural-interface/lib/worker-sandbox.js +46 -16
  174. package/neural-interface/package-lock.json +18 -2
  175. package/neural-interface/package.json +2 -1
  176. package/neural-interface/public/onboarding.html +1 -1
  177. package/neural-interface/public/shared/agent-runtime-options.js +89 -11
  178. package/neural-interface/public/shared/agy/agy-bypass.js +47 -0
  179. package/neural-interface/public/shared/agy/agy-client.js +266 -0
  180. package/neural-interface/public/shared/agy/agy-commands.js +120 -0
  181. package/neural-interface/public/shared/agy/agy-copy.js +326 -0
  182. package/neural-interface/public/shared/agy/agy-diff.js +161 -0
  183. package/neural-interface/public/shared/agy/agy-events.js +469 -0
  184. package/neural-interface/public/shared/agy/agy-greeting.js +26 -0
  185. package/neural-interface/public/shared/agy/agy-options.js +232 -0
  186. package/neural-interface/public/shared/agy/agy-panel.js +2543 -0
  187. package/neural-interface/public/shared/agy/agy-render.js +392 -0
  188. package/neural-interface/public/shared/agy/agy-setup.js +149 -0
  189. package/neural-interface/public/shared/agy/agy-styles.js +604 -0
  190. package/neural-interface/public/shared/agy/agy-tabs-logic.js +181 -0
  191. package/neural-interface/public/shared/api.js +4 -4
  192. package/neural-interface/public/shared/assistant/asst-agents-dock.js +3 -4
  193. package/neural-interface/public/shared/assistant/asst-api.js +15 -3
  194. package/neural-interface/public/shared/assistant/asst-brain-picker.js +56 -9
  195. package/neural-interface/public/shared/assistant/asst-budget.js +5 -4
  196. package/neural-interface/public/shared/assistant/asst-computer.js +26 -2
  197. package/neural-interface/public/shared/assistant/asst-control.js +48 -0
  198. package/neural-interface/public/shared/assistant/asst-hosts.js +16 -7
  199. package/neural-interface/public/shared/assistant/asst-limit-dock.js +6 -1
  200. package/neural-interface/public/shared/assistant/asst-limit-wait.js +38 -5
  201. package/neural-interface/public/shared/assistant/asst-motion-gate.js +80 -0
  202. package/neural-interface/public/shared/assistant/asst-panel.js +46 -9
  203. package/neural-interface/public/shared/assistant/asst-render.js +112 -18
  204. package/neural-interface/public/shared/assistant/asst-route.js +237 -57
  205. package/neural-interface/public/shared/assistant/asst-routes-editor.js +9 -8
  206. package/neural-interface/public/shared/assistant/asst-scene-styles.js +55 -23
  207. package/neural-interface/public/shared/assistant/asst-scenes.js +279 -80
  208. package/neural-interface/public/shared/assistant/asst-sidepanel-styles.js +4 -4
  209. package/neural-interface/public/shared/assistant/asst-sidepanel.js +51 -7
  210. package/neural-interface/public/shared/assistant/asst-slash.js +8 -2
  211. package/neural-interface/public/shared/assistant/asst-state.js +76 -9
  212. package/neural-interface/public/shared/assistant/asst-style-cache.js +44 -0
  213. package/neural-interface/public/shared/assistant/asst-styles.js +58 -4
  214. package/neural-interface/public/shared/assistant/asst-tool-kinds.js +34 -7
  215. package/neural-interface/public/shared/assistant/asst-usage.js +2 -1
  216. package/neural-interface/public/shared/cdx/cdx-output.js +26 -3
  217. package/neural-interface/public/shared/cdx/cdx-panel.js +14 -2
  218. package/neural-interface/public/shared/cdx/cdx-preview.js +12 -0
  219. package/neural-interface/public/shared/cdx/cdx-protocol.js +11 -10
  220. package/neural-interface/public/shared/cdx/cdx-render.js +10 -9
  221. package/neural-interface/public/shared/cdx/cdx-requests.js +11 -1
  222. package/neural-interface/public/shared/cdx/cdx-styles.js +4 -0
  223. package/neural-interface/public/shared/cdx/cdx-tabs.js +75 -5
  224. package/neural-interface/public/shared/cli-install-controller.js +218 -0
  225. package/neural-interface/public/shared/cli-install-view.js +7 -4
  226. package/neural-interface/public/shared/cli-status.js +9 -8
  227. package/neural-interface/public/shared/constants.js +3 -2
  228. package/neural-interface/public/shared/context-menu-model.js +1 -0
  229. package/neural-interface/public/shared/cp/cp-permissions.js +12 -0
  230. package/neural-interface/public/shared/cp/cp-restore.js +162 -0
  231. package/neural-interface/public/shared/cp/cp-tool-cards.js +10 -3
  232. package/neural-interface/public/shared/desktop-bridge.js +17 -0
  233. package/neural-interface/public/shared/desktop-titlebar-view.js +12 -0
  234. package/neural-interface/public/shared/desktop-titlebar.js +13 -4
  235. package/neural-interface/public/shared/desktop-update-view.js +1 -0
  236. package/neural-interface/public/shared/explorer/fx-api.js +4 -4
  237. package/neural-interface/public/shared/explorer/fx-links.js +3 -3
  238. package/neural-interface/public/shared/explorer/fx-logic.js +1 -1
  239. package/neural-interface/public/shared/explorer/fx-main.js +48 -16
  240. package/neural-interface/public/shared/explorer/fx-tree.js +1 -0
  241. package/neural-interface/public/shared/greeting/greeting-card.js +2 -2
  242. package/neural-interface/public/shared/greeting/greeting-view.js +1 -0
  243. package/neural-interface/public/shared/html-shell.js +21 -4
  244. package/neural-interface/public/shared/loading-teardown.js +34 -0
  245. package/neural-interface/public/shared/ocp-v2/ocp-v2-childpanel.js +6 -1
  246. package/neural-interface/public/shared/ocp-v2/ocp-v2-panel.js +28 -7
  247. package/neural-interface/public/shared/ocp-v2/ocp-v2-rehydrate.js +13 -4
  248. package/neural-interface/public/shared/ocp-v2/ocp-v2-render.js +17 -1
  249. package/neural-interface/public/shared/ocp-v2/ocp-v2-send.js +6 -0
  250. package/neural-interface/public/shared/ocp-v2/ocp-v2-styles.js +101 -68
  251. package/neural-interface/public/shared/ocp-v2/ocp-v2-ws.js +46 -3
  252. package/neural-interface/public/shared/provider-icons.js +4 -2
  253. package/neural-interface/public/shared/settings/settings-rules.js +2 -2
  254. package/neural-interface/public/shared/settings/settings-skill-folders-pane.js +1 -1
  255. package/neural-interface/public/shared/settings/settings-skill-folders.js +2 -2
  256. package/neural-interface/public/shared/showcase-privacy.js +6 -1
  257. package/neural-interface/public/shared/sidepanel-session-status.js +3 -3
  258. package/neural-interface/public/shared/skills/ss-creation.js +6 -3
  259. package/neural-interface/public/shared/skills/ss-dialog.js +5 -2
  260. package/neural-interface/public/shared/skills/ss-lint.js +9 -1
  261. package/neural-interface/public/shared/skills/ss-profiles.js +44 -6
  262. package/neural-interface/public/shared/skills/ss-review-view.js +2 -2
  263. package/neural-interface/public/shared/skills/studio/sx-actions.js +1 -1
  264. package/neural-interface/public/shared/skills/studio/sx-create-ai-view.js +1 -1
  265. package/neural-interface/public/shared/skills/studio/sx-create-ai.js +2 -1
  266. package/neural-interface/public/shared/skills/studio/sx-dom.js +2 -1
  267. package/neural-interface/public/shared/skills/studio/sx-fixes-view.js +359 -0
  268. package/neural-interface/public/shared/skills/studio/sx-fixes.js +327 -0
  269. package/neural-interface/public/shared/skills/studio/sx-inspector.js +1 -1
  270. package/neural-interface/public/shared/skills/studio/sx-install-flow.js +9 -6
  271. package/neural-interface/public/shared/skills/studio/sx-install.js +20 -3
  272. package/neural-interface/public/shared/skills/studio/sx-library-view.js +144 -35
  273. package/neural-interface/public/shared/skills/studio/sx-library.js +6 -2
  274. package/neural-interface/public/shared/skills/studio/sx-main.js +11 -2
  275. package/neural-interface/public/shared/skills/studio/sx-review-view.js +1 -1
  276. package/neural-interface/public/shared/skills/studio/sx-subject.js +7 -6
  277. package/neural-interface/public/shared/skills/studio/sx-workspace-view.js +6 -5
  278. package/neural-interface/public/shared/state.js +1 -1
  279. package/neural-interface/public/shared/storage-retry.js +47 -0
  280. package/neural-interface/public/shared/storage.js +25 -4
  281. package/neural-interface/public/shared/streaming-markdown.js +58 -0
  282. package/neural-interface/public/shared/styles.css +362 -35
  283. package/neural-interface/public/shared/synabun-mascot.js +27 -8
  284. package/neural-interface/public/shared/synabun-ticker.js +147 -12
  285. package/neural-interface/public/shared/text-cache.js +29 -0
  286. package/neural-interface/public/shared/tool-list-picker.js +2 -2
  287. package/neural-interface/public/shared/tool-list-view.js +2 -2
  288. package/neural-interface/public/shared/topright-overflow-logic.js +54 -0
  289. package/neural-interface/public/shared/ui-antigravity-panel.js +19 -0
  290. package/neural-interface/public/shared/ui-assistant.js +2 -0
  291. package/neural-interface/public/shared/ui-automation-studio.js +20 -12
  292. package/neural-interface/public/shared/ui-claude-panel.js +374 -134
  293. package/neural-interface/public/shared/ui-cli-banner.js +23 -167
  294. package/neural-interface/public/shared/ui-file-explorer.js +48 -6
  295. package/neural-interface/public/shared/ui-file-links.js +2 -2
  296. package/neural-interface/public/shared/ui-games.js +2 -2
  297. package/neural-interface/public/shared/ui-judgments.js +1 -1
  298. package/neural-interface/public/shared/ui-keybinds.js +8 -2
  299. package/neural-interface/public/shared/ui-link.js +3 -3
  300. package/neural-interface/public/shared/ui-loading.js +5 -4
  301. package/neural-interface/public/shared/ui-menubar.js +2 -2
  302. package/neural-interface/public/shared/ui-native-loop-router.js +4 -0
  303. package/neural-interface/public/shared/ui-navbar.js +45 -0
  304. package/neural-interface/public/shared/ui-notifications.js +1 -0
  305. package/neural-interface/public/shared/ui-resume.js +9 -7
  306. package/neural-interface/public/shared/ui-schedules-studio.js +41 -10
  307. package/neural-interface/public/shared/ui-scroll-follow.js +16 -5
  308. package/neural-interface/public/shared/ui-sessions.js +22 -12
  309. package/neural-interface/public/shared/ui-settings.js +90 -65
  310. package/neural-interface/public/shared/ui-sidepanel-session-entry.js +9 -2
  311. package/neural-interface/public/shared/ui-sidepanel-sessions.js +3 -3
  312. package/neural-interface/public/shared/ui-sidepanel-tray.js +48 -7
  313. package/neural-interface/public/shared/ui-sidepanel-windows.js +3 -3
  314. package/neural-interface/public/shared/ui-terminal.js +146 -10
  315. package/neural-interface/public/shared/ui-tool-updates.js +3 -3
  316. package/neural-interface/public/shared/ui-tooltip.js +71 -46
  317. package/neural-interface/public/shared/ui-topright-overflow.js +233 -0
  318. package/neural-interface/public/shared/ui-whiteboard.js +1 -1
  319. package/neural-interface/public/shared/ui-worker-end.js +1 -1
  320. package/neural-interface/public/shared/whatsapp/wa-view.js +2 -2
  321. package/neural-interface/public/shared/worker-questions.js +34 -0
  322. package/neural-interface/public/sidepanel-session.html +1 -1
  323. package/neural-interface/server.js +731 -466
  324. package/neural-interface/templates/rulesets/README.md +1 -1
  325. package/neural-interface/templates/rulesets/legacy-hashes.json +5 -5
  326. package/neural-interface/templates/rulesets/manifest.json +1 -1
  327. package/npm-shrinkwrap.json +18 -2
  328. package/package.json +2 -1
  329. package/skills/skill-forge/SKILL.md +4 -4
  330. package/skills/synabun/SKILL.md +1 -1
  331. package/skills/synabun/modules/schedule.md +3 -3
  332. /package/neural-interface/templates/rulesets/{gemini.md → antigravity.md} +0 -0
@@ -37,7 +37,7 @@ import { effectiveRouting, normalizeRouteMode } from './assistant-config.js';
37
37
  import { normalizeEffort } from './effort-levels.js';
38
38
  import { hasRouteAccounts, routeAccountText } from './assistant-route-accounts.js';
39
39
  import { chooseUsageAccount } from './account-usage.js';
40
- import { PROVIDER_LABELS, coversWork, dryProviders, isFreePrice, listPriceOf, rowOf } from './assistant-budget-split.js';
40
+ import { PROVIDER_LABELS, coversWork, dryProviders, isFreePrice, isUnpricedProvider, listPriceOf, rowOf } from './assistant-budget-split.js';
41
41
 
42
42
  export const TASK_CLASS_META = Object.freeze({
43
43
  chat: { label: 'Chat', description: 'Conversation and questions answered from knowledge or memory', defaultKind: 'direct' },
@@ -75,6 +75,8 @@ export function requiresVision(taskClass) { return TASK_CLASS_META[taskClass]?.v
75
75
  export const CANNOT_SEE_TEXT = "This model can't see images.";
76
76
  export const UNKNOWN_SIGHT_TEXT = "This model isn't known to see images.";
77
77
  export const UNAVAILABLE_TEXT = "This model isn't available right now.";
78
+ /** A "here" on an account other than this conversation's: provider histories belong to their account. */
79
+ export const WORKER_FOR_ACCOUNT_TEXT = 'Runs on a worker because this conversation belongs to another account.';
78
80
  /** The card / answer text for a model design cannot use (a designFit() verdict). */
79
81
  function fitText(fit) { return fit === 'blind' ? CANNOT_SEE_TEXT : fit === 'unavailable' ? UNAVAILABLE_TEXT : UNKNOWN_SIGHT_TEXT; }
80
82
  /** What a run of the class collects into its media: the medium it makes, else what it gathers (Design's screenshots). */
@@ -185,7 +187,8 @@ export function normalizeConfidence(value) {
185
187
 
186
188
  /**
187
189
  * Normalize the brain's proposal (agent_route body, or a dispatch spec reshaped
188
- * into one). A "direct" proposal on another provider is really a dispatch; for a
190
+ * into one). A "direct" proposal on another provider is really a dispatch
191
+ * (`coercedProvider` records the first, so the result can say so); for a
189
192
  * dispatch-only class (image / video creation, design) every proposal is a dispatch,
190
193
  * a "here" one on the brain's own model (`coercedDirect` records the first).
191
194
  * Design (`visionOnly`) also always needs a model that can see.
@@ -201,14 +204,17 @@ export function normalizeRouteRequest(body = {}, { brain = {} } = {}) {
201
204
  const dispatchOnly = meta.dispatchOnly === true;
202
205
  const proposals = [];
203
206
  let coercedDirect = false;
207
+ let coercedProvider = null;
204
208
  for (const raw of Array.isArray(body.proposals) ? body.proposals.slice(0, 3) : []) {
205
209
  if (!raw || typeof raw !== 'object') continue;
206
210
  let kind = raw.kind === 'direct' ? 'direct' : 'dispatch';
207
211
  let provider = CATALOG_PROVIDERS.includes(raw.provider) ? raw.provider : null;
208
212
  let model = raw.model ? String(raw.model).trim().slice(0, 200) : null;
209
213
  if (kind === 'direct') {
210
- if (provider && provider !== brain.provider) kind = 'dispatch';
211
- else provider = brain.provider || provider;
214
+ if (provider && provider !== brain.provider) {
215
+ kind = 'dispatch';
216
+ if (!proposals.length && !dispatchOnly) coercedProvider = provider;
217
+ } else provider = brain.provider || provider;
212
218
  }
213
219
  if (!provider) continue;
214
220
  if (dispatchOnly && kind === 'direct') {
@@ -230,9 +236,18 @@ export function normalizeRouteRequest(body = {}, { brain = {} } = {}) {
230
236
  const request = { taskClass, taskClassLabel: meta.label, summary: clip(body.summary || '', 400), confidence, needsVision, proposals };
231
237
  if (needsOutput || dispatchOnly) Object.assign(request, { needsOutput, dispatchOnly, coercedDirect });
232
238
  if (visionOnly) request.visionOnly = true;
239
+ if (coercedProvider) request.coercedProvider = coercedProvider;
233
240
  return request;
234
241
  }
235
242
 
243
+ /** Why a "here" on `provider` runs on a worker: only the brain's own provider runs in this conversation. */
244
+ function otherProviderCorrection(provider, brain = {}) {
245
+ return {
246
+ field: 'kind', from: 'direct', to: 'dispatch', reason: 'other_provider', provider,
247
+ text: `${providerLabel(provider)} is not this conversation's provider${brain.provider ? ` (${providerLabel(brain.provider)})` : ''}, so it runs on a worker`,
248
+ };
249
+ }
250
+
236
251
  /** Catalog-backed description of a target: label, tier, vision, outputs, price, context. */
237
252
  export function describeTarget(target, { catalog = null, brain = {}, brainInfo = null } = {}) {
238
253
  if (!target) return null;
@@ -499,6 +514,8 @@ export function evaluateRoute({ mode, routing = {}, request, brain = {}, brainIn
499
514
  if (request.coercedDirect) {
500
515
  corrections.push({ field: 'kind', from: 'direct', to: 'dispatch', reason: 'dispatch_only', text: `${label} always runs on a worker, not here` });
501
516
  }
517
+ // "Here" on another provider was always a worker: the result now says so instead of changing the kind silently.
518
+ if (request.coercedProvider) corrections.push(otherProviderCorrection(request.coercedProvider, brain));
502
519
  const sees = (target) => describeTarget(target, { catalog, brain, brainInfo })?.vision;
503
520
  const fit = (target) => designFit(catalog, target, { brain, brainInfo });
504
521
  // Too blind for this task: design needs a confirmed true (null never counts) on an available row; other vision work refuses a confirmed false.
@@ -643,7 +660,8 @@ export function evaluateRoute({ mode, routing = {}, request, brain = {}, brainIn
643
660
  if (dispatchOnly && auto.kind !== 'dispatch') auto = { ...auto, kind: 'dispatch' };
644
661
  // A corrected model or provider re-checks the effort.
645
662
  auto = effortChecked(auto, catalog, corrections);
646
- if (!bound && corrections.some((c) => c.reason !== 'remembered_disabled' && c.reason !== 'remembered_incapable')) source = 'corrected';
663
+ // The provider rule only names what always happened (a "here" elsewhere is a worker): it corrects nothing.
664
+ if (!bound && corrections.some((c) => c.reason !== 'remembered_disabled' && c.reason !== 'remembered_incapable' && c.reason !== 'other_provider')) source = 'corrected';
647
665
  // With a budget the saved route yields: a provider with no share left moves the task on.
648
666
  const budgeted = applyBudget(auto);
649
667
  if (budgeted.declined) return budgetDeclined(budgeted);
@@ -671,11 +689,13 @@ function optionFor(id, rawTarget, badge, { catalog, brain, brainInfo, needsVisio
671
689
  else if (fit !== 'ok') { option.disabled = true; option.disabledReason = fitText(fit); }
672
690
  else if (needsVision && d.vision === false) { option.disabled = true; option.disabledReason = 'This model cannot see images, and this task needs to.'; }
673
691
  else if (needsVision && d.vision === null) option.warning = 'Image support unknown for this model.';
674
- // The session budget: a provider with no share left cannot take the task (a free model still can).
692
+ // The session budget: a provider with no share left cannot take the task (a free model still can), and one
693
+ // that reports no prices cannot run under a budget at all.
675
694
  if (!option.disabled && coverage && !budgetCovers(coverage, catalog, target)) {
695
+ const unpriced = coverage.leftUsd < 0.01 ? null : unpricedText(target.provider);
676
696
  option.disabled = true;
677
- option.budget = 'share_used_up';
678
- option.disabledReason = coverage.leftUsd < 0.01 ? "Budget: this session's budget is spent." : `Budget: the ${providerLabel(target.provider)} share of this session's budget is used up.`;
697
+ option.budget = unpriced ? 'unpriced' : 'share_used_up';
698
+ option.disabledReason = unpriced || (coverage.leftUsd < 0.01 ? "Budget: this session's budget is spent." : `Budget: the ${providerLabel(target.provider)} share of this session's budget is used up.`);
679
699
  }
680
700
  if (here && brainInfo?.contextWindow && d.contextWindow && d.contextWindow < brainInfo.contextWindow) {
681
701
  option.warning = `Smaller context (${Math.round(d.contextWindow / 1000)}k) than the current model; a long conversation may not fit.`;
@@ -684,7 +704,12 @@ function optionFor(id, rawTarget, badge, { catalog, brain, brainInfo, needsVisio
684
704
  }
685
705
 
686
706
  export function providerLabel(provider) {
687
- return provider === 'claude-code' ? 'Claude Code' : provider === 'codex' ? 'Codex' : provider === 'opencode' ? 'OpenCode' : String(provider || 'Assistant');
707
+ return provider === 'claude-code' ? 'Claude Code' : provider === 'codex' ? 'Codex' : provider === 'opencode' ? 'OpenCode' : provider === 'antigravity' ? 'Antigravity' : String(provider || 'Assistant');
708
+ }
709
+
710
+ /** Why a session with a budget cannot run `provider` at all (it reports tokens, never prices), else null. */
711
+ export function unpricedText(provider) {
712
+ return isUnpricedProvider(provider) ? `Budget: ${providerLabel(provider)} reports tokens, not prices, so a session with a budget cannot run it. Pick another option, or switch the budget to No limit.` : null;
688
713
  }
689
714
 
690
715
  /** `seeingOnly` (design): only rows with a confirmed vision: true, where `needsVision` only drops a confirmed false. */
@@ -725,9 +750,17 @@ function strongestClaude(catalog) {
725
750
  * Image / video creation: no "here" (always a worker), models that cannot make
726
751
  * the medium disabled, and — when the suggestion cannot — the model that would
727
752
  * run it instead (badge "capable"). Design: the same with "can see".
753
+ *
754
+ * Both sides, always: the card switches between Here (kind "direct": this
755
+ * conversation, on its model or another of its provider) and Dispatch (a worker).
756
+ * A side the options leave without one that can run gets one: a worker on the
757
+ * brain's own model ("w1", badge "current"), else a capable model of its provider,
758
+ * else elsewhere; a "here" on a model of the brain's provider that can see when the
759
+ * brain cannot ("h1", badge "capable"). Worker-only classes get no "here".
760
+ * → { options, defaultOptionId, sides, defaults } (routeSides).
728
761
  */
729
762
  export function buildRouteOptions({ request, evaluation, brain = {}, brainInfo = null, catalog = null, routing = {}, coverage = null }) {
730
- if (evaluation.reasons?.includes('always-ask')) routing = { ...routing, preferences: {} };
763
+ const alwaysAsk = evaluation.reasons?.includes('always-ask') === true;
731
764
  const medium = request.needsOutput || null;
732
765
  const dispatchOnly = request.dispatchOnly === true;
733
766
  const visionOnly = request.visionOnly === true;
@@ -735,16 +768,26 @@ export function buildRouteOptions({ request, evaluation, brain = {}, brainInfo =
735
768
  const options = [];
736
769
  const push = (option) => { if (!options.some((o) => sameTarget(o, option))) options.push(option); };
737
770
  const hereTarget = { kind: 'direct', provider: brain.provider, model: brain.model || null, effort: brain.effort || null };
771
+ // A direct pick on the brain's own model with no effort is "here" as the brain runs now: one row, not two.
772
+ const onBrainModel = (t) => t.kind === 'direct' && t.provider === brain.provider && String(t.model || '') === String(brain.model || '');
738
773
  request.proposals.forEach((p, index) => {
739
774
  const target = { kind: p.kind, provider: p.provider, model: p.model || (index === 0 ? evaluation.proposal?.model || null : null), effort: p.effort };
740
775
  if (index === 0 && evaluation.proposal) Object.assign(target, evaluation.proposal);
776
+ if (onBrainModel(target) && !target.effort && brain.effort) target.effort = brain.effort;
741
777
  push(optionFor(`s${index + 1}`, target, 'suggested', { ...ctx, reason: p.reason }));
742
778
  });
743
779
  // The session budget moved the suggestion: the brain's own pick stays on the card, disabled with why.
744
780
  if (evaluation.budgetFrom && coverage) push(optionFor('b0', evaluation.budgetFrom, 'suggested', { ...ctx, reason: request.proposals[0]?.reason || null }));
745
781
  if (!dispatchOnly) push(optionFor('here', hereTarget, 'current', ctx));
746
- const pref = evaluation.reasons?.includes('always-ask') ? null : preferenceTarget(routing.preferences?.[request.taskClass], brain, catalog, { dispatchOnly });
747
- if (pref) push(optionFor('r1', pref, 'remembered', ctx));
782
+ const pref = preferenceTarget(routing.preferences?.[request.taskClass], brain, catalog, { dispatchOnly });
783
+ // Always ask: the saved route is offered and its side starts on it (never binding: the card still asks).
784
+ if (pref) {
785
+ const saved = optionFor('r1', pref, 'remembered', ctx);
786
+ // A proposal of the same target keeps its row and carries the mark.
787
+ const row = options.find((o) => sameTarget(o, saved)) || saved;
788
+ if (alwaysAsk && !saved.disabled) Object.assign(row, { preferred: true, saved: true });
789
+ push(saved);
790
+ }
748
791
  const anchor = evaluation.proposal?.kind === 'dispatch' || dispatchOnly ? evaluation.proposal || request.proposals[0] : hereTarget;
749
792
  const cheaper = neighbour(catalog, anchor, 'cheaper', { needsVision: request.needsVision, seeingOnly: visionOnly, needsOutput: medium });
750
793
  if (cheaper) push(optionFor('c1', { kind: anchor.provider === brain.provider && anchor.kind === 'direct' ? 'direct' : 'dispatch', provider: anchor.provider, model: cheaper.id, effort: null }, 'cheaper', ctx));
@@ -766,11 +809,103 @@ export function buildRouteOptions({ request, evaluation, brain = {}, brainInfo =
766
809
  const option = seer ? optionFor('g1', seer, 'capable', ctx) : null;
767
810
  if (option && !option.disabled) push(option);
768
811
  }
812
+ // Both sides, always: a side with nothing that can run it gets the first candidate that can
813
+ // (or, with none, the first candidate disabled with why, so the card can say it).
814
+ // A saved route on an account is checked later (it may be signed out): it does not count, so its side keeps a fallback.
815
+ const runnable = (kind) => options.some((o) => o.kind === kind && !o.disabled && !(o.saved && o.accountId));
816
+ const offer = (kind, id, candidates) => {
817
+ let first = null;
818
+ for (const [target, badge] of candidates) {
819
+ if (!target?.provider) continue;
820
+ const option = optionFor(id, target, badge, ctx);
821
+ if (options.some((o) => sameTarget(o, option))) continue;
822
+ if (!option.disabled) { push(option); return; }
823
+ first = first || option;
824
+ }
825
+ if (first && !options.some((o) => o.kind === kind)) push(first);
826
+ };
827
+ if (request.taskClass !== 'chat' && !dispatchOnly && !runnable('direct')) offer('direct', 'h1', hereCandidates({ request, catalog, brain }));
828
+ if (request.taskClass !== 'chat' && !runnable('dispatch')) offer('dispatch', 'w1', workerCandidates({ request, catalog, brain, brainInfo }));
769
829
  // Order: suggested first, then here, remembered, a capable stand-in, cheaper, stronger.
770
830
  const order = { suggested: 0, current: 1, remembered: 2, capable: 3, cheaper: 4, stronger: 5 };
771
831
  options.sort((a, b) => (order[a.badge] ?? 9) - (order[b.badge] ?? 9) || String(a.id).localeCompare(String(b.id)));
772
- const defaultOption = options.find((o) => o.badge === 'suggested' && !o.disabled) || options.find((o) => !o.disabled) || options[0];
773
- return { options, defaultOptionId: defaultOption?.id || 'here' };
832
+ return { options, ...routeSides({ options, request }) };
833
+ }
834
+
835
+ /**
836
+ * "Here" on another model of the brain's provider when "here" itself cannot run the task
837
+ * (the brain cannot see a computer task's screen, or the user hid its model): the
838
+ * cheapest one of that provider that can (one that can see first for vision work).
839
+ */
840
+ function hereCandidates({ request, catalog, brain }) {
841
+ if (!brain.provider) return [];
842
+ const rows = visibleRows(catalog, brain.provider)
843
+ .filter((row) => row.status !== 'unavailable' && row.toolcall !== false && row.id !== brain.model && (!request.needsVision || row.vision === true))
844
+ .sort((a, b) => outPrice(a) - outPrice(b) || tierRank(a.tier) - tierRank(b.tier));
845
+ return rows.map((row) => [{ kind: 'direct', provider: brain.provider, model: row.id, effort: null }, request.needsVision ? 'capable' : null]);
846
+ }
847
+
848
+ /**
849
+ * A worker for a card whose options have none that can run: the brain's own model on a
850
+ * worker (the same model, off this conversation), else a model of its provider that can
851
+ * (the brain's tier first, one that can see for vision work), else elsewhere (one that
852
+ * can see, or the strongest Claude model).
853
+ */
854
+ function workerCandidates({ request, catalog, brain, brainInfo }) {
855
+ const candidates = [];
856
+ if (brain.provider) candidates.push([{ kind: 'dispatch', provider: brain.provider, model: brain.model || null, effort: brain.effort || null }, 'current']);
857
+ const brainTier = brainInfo?.tier || (brain.model ? findModel(catalog, brain.provider, brain.model)?.row?.tier : null) || null;
858
+ const fits = (row) => row.status !== 'unavailable' && row.toolcall !== false && (!request.needsVision || row.vision === true);
859
+ const closeness = (row) => (brainTier ? Math.abs(tierRank(row.tier) - tierRank(brainTier)) : 0);
860
+ const badge = request.needsVision ? 'capable' : null;
861
+ if (brain.provider) {
862
+ visibleRows(catalog, brain.provider).filter(fits).filter((row) => row.id !== brain.model)
863
+ .sort((a, b) => closeness(a) - closeness(b) || outPrice(a) - outPrice(b))
864
+ .forEach((row) => candidates.push([{ kind: 'dispatch', provider: brain.provider, model: row.id, effort: null }, badge]));
865
+ }
866
+ if (request.needsVision) {
867
+ CATALOG_PROVIDERS.filter((p) => p !== brain.provider)
868
+ .flatMap((p) => visibleRows(catalog, p).filter(fits).map((row) => ({ provider: p, row })))
869
+ .sort((a, b) => outPrice(a.row) - outPrice(b.row) || tierRank(a.row.tier) - tierRank(b.row.tier))
870
+ .forEach(({ provider, row }) => candidates.push([{ kind: 'dispatch', provider, model: row.id, effort: null }, 'capable']));
871
+ } else if (brain.provider !== 'claude-code') {
872
+ const opus = strongestClaude(catalog);
873
+ if (opus) candidates.push([{ kind: 'dispatch', provider: 'claude-code', model: opus.id, effort: null }, null]);
874
+ }
875
+ return candidates;
876
+ }
877
+
878
+ /**
879
+ * Which side of the card's Here / Dispatch switch can run the task, and the option each side
880
+ * starts on: { sides: { direct, dispatch }, defaults: { direct, dispatch }, defaultOptionId }.
881
+ * A side is { available, reason, code } (code: worker_only, account_worker, disabled, none).
882
+ * Read from the options as they are sent, after the account check (a "here" that moved to a
883
+ * worker belongs to the dispatch side: its kind is "dispatch").
884
+ */
885
+ export function routeSides({ options = [], request = {}, defaultOptionId = null } = {}) {
886
+ const usable = (o) => o && !o.disabled;
887
+ const chosen = options.find((o) => o.id === defaultOptionId && usable(o))
888
+ || options.find((o) => o.badge === 'suggested' && usable(o)) || options.find(usable) || options[0] || null;
889
+ // A side starts on the saved route when Always ask marked it (preferred), else on the chosen option, else its first.
890
+ const firstOf = (kind) => options.find((o) => o.preferred === true && o.kind === kind && usable(o))
891
+ || (chosen?.kind === kind && usable(chosen) ? chosen : null) || options.find((o) => o.kind === kind && usable(o)) || null;
892
+ const label = request.taskClassLabel || TASK_CLASS_META[request.taskClass]?.label || 'This task';
893
+ const side = (kind) => {
894
+ if (firstOf(kind)) return { available: true, reason: null, code: null };
895
+ if (kind === 'direct' && request.dispatchOnly) return { available: false, code: 'worker_only', reason: `${label} always runs on a worker.` };
896
+ if (kind === 'direct' && options.some((o) => o.kind === 'dispatch' && o.accountKind === 'direct')) {
897
+ return { available: false, code: 'account_worker', reason: "This conversation's account cannot take it, so it runs on a worker." };
898
+ }
899
+ const blocked = options.find((o) => o.kind === kind && o.disabled);
900
+ if (blocked) return { available: false, code: 'disabled', reason: blocked.disabledReason || null };
901
+ return { available: false, code: 'none', reason: kind === 'direct' ? 'Nothing in this conversation can run this task.' : 'No worker model can run this task.' };
902
+ };
903
+ return {
904
+ // The card opens on the chosen option's side, on that side's default.
905
+ defaultOptionId: (chosen && usable(chosen) ? firstOf(chosen.kind)?.id : null) || chosen?.id || 'here',
906
+ sides: { direct: side('direct'), dispatch: side('dispatch') },
907
+ defaults: { direct: firstOf('direct')?.id || null, dispatch: firstOf('dispatch')?.id || null },
908
+ };
774
909
  }
775
910
 
776
911
  /** Validate a route-card answer; returns { declined } or { target, optionId, remember }. */
@@ -779,14 +914,19 @@ export function targetFromAnswer({ card, response = {}, catalog = null, brain =
779
914
  const optionId = String(response.optionId || response.option_id || '').trim();
780
915
  const remember = response.remember === true || (typeof response.remember === 'string' && response.remember.length > 0);
781
916
  const option = optionId && optionId !== 'other' ? (card.options || []).find((o) => o.id === optionId) : null;
917
+ // The model this conversation already runs on needs no catalog entry to stay on (an OpenCode model
918
+ // no longer listed as connected, an id the list spells otherwise): "here" on it changes nothing.
919
+ // A hidden one is still refused, as everywhere.
920
+ const onBrain = (target) => target.kind === 'direct' && !!brain.model && target.provider === brain.provider && String(target.model || '') === String(brain.model);
782
921
  // A card can remain open while the catalog or hidden-model list changes.
783
922
  // Check the chosen target again instead of trusting the option's old state.
784
923
  const ensureAvailable = (target) => {
785
- if (target.model && findModel(catalog, target.provider, target.model) === null) {
924
+ const own = onBrain(target);
925
+ if (target.model && (own ? isDisabled(catalog, target) : findModel(catalog, target.provider, target.model) === null)) {
786
926
  if (isDisabled(catalog, target)) throw routeError('MODEL_DISABLED', `${target.model} is disabled in the Assistant's Models list. Pick an enabled model.`);
787
927
  throw routeError('ROUTE_INVALID', `Unknown model ${target.model} for ${target.provider}.`);
788
928
  }
789
- if (findModel(catalog, target.provider, target.model)?.row?.status === 'unavailable') throw routeError('ROUTE_INVALID', UNAVAILABLE_TEXT);
929
+ if (!own && findModel(catalog, target.provider, target.model)?.row?.status === 'unavailable') throw routeError('ROUTE_INVALID', UNAVAILABLE_TEXT);
790
930
  if (!target.model && isDisabled(catalog, target)) throw routeError('MODEL_DISABLED', `Every ${target.provider} model is disabled in the Assistant's Models list. Pick another provider.`);
791
931
  // Design: only a confirmed vision: true on an available row (unknown or unavailable is refused too);
792
932
  // other vision work: a confirmed false.
@@ -800,10 +940,16 @@ export function targetFromAnswer({ card, response = {}, catalog = null, brain =
800
940
  };
801
941
  // Image / video creation and design always run on a worker: a "here" pick is a dispatch onto that model.
802
942
  const dispatchOnly = card.dispatchOnly === true;
943
+ // Only the brain's own provider runs in this conversation: a "here" on another one (an option made
944
+ // before the brain changed provider, or an "other" pick) is a worker, and the answer says so.
945
+ const elsewhere = (provider) => !!brain.provider && provider !== brain.provider;
803
946
  if (option) {
804
947
  if (option.disabled && !option.accountError) throw routeError('ROUTE_INVALID', option.disabledReason || 'That option is not available for this task.');
805
948
  const corrections = [];
806
- const target = effortChecked({ kind: dispatchOnly ? 'dispatch' : option.kind, provider: option.provider, model: option.model || (dispatchOnly && option.kind === 'direct' ? brain.model || null : null), effort: option.effort, ...(option.accountId ? { accountId: option.accountChoice || option.accountId, ...(option.accountKind ? { accountKind: dispatchOnly ? 'dispatch' : option.accountKind } : {}) } : {}) }, catalog, corrections);
949
+ const moved = !dispatchOnly && option.kind === 'direct' && elsewhere(option.provider);
950
+ if (moved) corrections.push(otherProviderCorrection(option.provider, brain));
951
+ const kind = dispatchOnly || moved ? 'dispatch' : option.kind;
952
+ const target = effortChecked({ kind, provider: option.provider, model: option.model || (dispatchOnly && option.kind === 'direct' ? brain.model || null : null), effort: option.effort, ...(option.accountId ? { accountId: option.accountChoice || option.accountId, ...(option.accountKind ? { accountKind: dispatchOnly || moved ? 'dispatch' : option.accountKind } : {}) } : {}) }, catalog, corrections);
807
953
  ensureAvailable(target);
808
954
  return { target, optionId, remember, corrections };
809
955
  }
@@ -811,6 +957,11 @@ export function targetFromAnswer({ card, response = {}, catalog = null, brain =
811
957
  if (!t || typeof t !== 'object' || !CATALOG_PROVIDERS.includes(t.provider)) throw routeError('ROUTE_INVALID', 'Pick one of the options or a catalog model.');
812
958
  const kind = t.kind === 'direct' && t.provider === brain.provider && !dispatchOnly ? 'direct' : 'dispatch';
813
959
  const corrections = [];
960
+ if (t.kind === 'direct' && kind === 'dispatch') {
961
+ corrections.push(dispatchOnly
962
+ ? { field: 'kind', from: 'direct', to: 'dispatch', reason: 'dispatch_only', text: `${TASK_CLASS_META[card.taskClass]?.label || 'This task'} always runs on a worker, not here` }
963
+ : otherProviderCorrection(t.provider, brain));
964
+ }
814
965
  const target = effortChecked({ kind, provider: t.provider, model: t.model ? String(t.model) : null, effort: t.effort ? String(t.effort).trim().slice(0, 40) : null, accountId: t.accountId ? String(t.accountId).trim() : null }, catalog, corrections);
815
966
  ensureAvailable(target);
816
967
  return { target, optionId: 'other', remember, corrections };
@@ -1017,9 +1168,13 @@ export function createAssistantRouter({
1017
1168
  : row.signedIn === false ? `Account ${row.label || row.id} (${row.id}) is signed out. Sign it in from the ${providerLabel(target.provider)} account menu, or save another Account in Model routes.` : '';
1018
1169
  const accountError = message ? { code: row ? 'ACCOUNT_SIGNED_OUT' : choice === 'auto' ? 'ACCOUNT_NO_SUBSCRIPTION' : 'ACCOUNT_NOT_FOUND', message } : null;
1019
1170
  // Provider histories are account-owned. Another account can do the task on
1020
- // a worker; it cannot resume this account's conversation.
1171
+ // a worker; it cannot resume this account's conversation. That move is always
1172
+ // said (a pinned account the user picked carries no usage reason of its own).
1021
1173
  const moved = target.kind === 'direct' && accountId !== (brain.accountId || 'default');
1022
- const accountCorrection = why ? { reason, from: before?.label || choice, to: row?.label || accountId, usage, workerForAccount: moved, message: why + (moved ? ' Runs on a worker because this conversation belongs to another account.' : '') } : null;
1174
+ const accountCorrection = why
1175
+ ? { reason, from: before?.label || choice, to: row?.label || accountId, usage, workerForAccount: moved, message: why + (moved ? ` ${WORKER_FOR_ACCOUNT_TEXT}` : '') }
1176
+ : moved ? { field: 'kind', reason: 'account_worker', from: rows.find(a => a.id === (brain.accountId || 'default'))?.label || brain.accountId || 'default', to: row?.label || accountId, usage, workerForAccount: true, message: `${WORKER_FOR_ACCOUNT_TEXT.replace(/\.$/, '')} (${row?.label || accountId}).` }
1177
+ : null;
1023
1178
  const enriched = { ...target, accountKind: target.accountKind || target.kind, ...(moved ? { kind: 'dispatch' } : {}), accountId, account, accountError, accountChoice: target.accountChoice || choice, accountCorrection };
1024
1179
  if (check && accountError) throw routeError(accountError.code, message, 409, { accountId, target: enriched });
1025
1180
  return enriched;
@@ -1085,8 +1240,14 @@ export function createAssistantRouter({
1085
1240
  return call('continueDirect', sessionId, { target, route: source, text: source.summary || '' }) !== false;
1086
1241
  }
1087
1242
 
1088
- function resultFor(status, { routeId, mode, source = null, target = null, card = null, runIds = [], corrections = [], cat = null, brain = {}, brainInfo = null, remembered = false, continuation = false, sessionId = null } = {}) {
1243
+ function resultFor(status, { routeId, mode, source = null, target = null, card = null, runIds = [], corrections = [], cat = null, brain = {}, brainInfo = null, remembered = false, continuation = false, sessionId = null, switched = null, droppedRuns = [] } = {}) {
1089
1244
  const described = target ? { ...describeTarget(target, { catalog: cat, brain, brainInfo }), continuation } : null;
1245
+ // The user's pick was the other kind than the brain proposed; a "here" pick on another account runs on a worker.
1246
+ const kindLead = source !== 'user' ? ''
1247
+ : target?.kind === 'dispatch' && target.accountKind === 'direct'
1248
+ ? 'The user chose to do this here, but on another account than this conversation\'s, so it runs on a worker. '
1249
+ : switched === 'direct' ? `The user chose to do this here, not on a worker${droppedRuns.length ? ` (the held run${droppedRuns.length === 1 ? '' : 's'} ${droppedRuns.join(', ')} ${droppedRuns.length === 1 ? 'was' : 'were'} not started)` : ''}. `
1250
+ : switched === 'dispatch' ? 'The user chose a worker for this, not here. ' : '';
1090
1251
  // A saved route that replaced the brain's pick (never mode) says so first.
1091
1252
  const binding = (corrections || []).find((c) => c?.reason === 'remembered_route');
1092
1253
  const skipped = (corrections || []).find((c) => c?.reason === 'remembered_disabled');
@@ -1103,7 +1264,7 @@ export function createAssistantRouter({
1103
1264
  ok: true, status, routeId, mode, source, target: described, remembered, continuation,
1104
1265
  corrections, runIds, expiresAt: card?.expiresAt ? new Date(card.expiresAt).toISOString() : null,
1105
1266
  ...(shares ? { budget: shares } : {}),
1106
- next: `${lead}${nextText(status, described, { routeId, label: described?.label })}`,
1267
+ next: `${lead}${status === 'approved' ? kindLead : ''}${nextText(status, described, { routeId, label: described?.label })}`,
1107
1268
  };
1108
1269
  }
1109
1270
 
@@ -1155,13 +1316,23 @@ export function createAssistantRouter({
1155
1316
  const routeId = `route-${randomId()}`;
1156
1317
  const requestId = routeId;
1157
1318
  const r = routing();
1158
- const { options, defaultOptionId } = buildRouteOptions({ request, evaluation, brain, brainInfo, catalog: cat, routing: r, coverage: evaluation.coverage || null });
1319
+ const built = buildRouteOptions({ request, evaluation, brain, brainInfo, catalog: cat, routing: r, coverage: evaluation.coverage || null });
1320
+ const { options } = built;
1159
1321
  for (const option of options) {
1160
1322
  const first = request.proposals[0];
1161
1323
  const fallback = origin === 'dispatch' && option.badge !== 'remembered' && option.provider === first?.provider ? first.accountId : null;
1162
1324
  Object.assign(option, accountTarget(option, brain, cat, { check: false, fallback }));
1163
1325
  if (option.accountError) { option.disabled = true; option.disabledReason = option.accountError.message; }
1326
+ // A "here" whose account cannot take it (this conversation's is out of usage) runs on a worker
1327
+ // on another account: the card names it as a worker and says why, never "Do it here".
1328
+ if (option.kind === 'dispatch' && option.accountKind === 'direct') {
1329
+ const described = describeTarget(option, { catalog: cat, brain, brainInfo });
1330
+ option.label = `${providerLabel(option.provider)} · ${described?.label || option.model || 'default'}${option.effort ? ` · ${option.effort}` : ''}`;
1331
+ option.warning = option.accountCorrection?.message || WORKER_FOR_ACCOUNT_TEXT;
1332
+ }
1164
1333
  }
1334
+ // The sides and defaults as the options are sent (the account check can disable or move one).
1335
+ const { defaultOptionId, sides, defaults } = routeSides({ options, request, defaultOptionId: built.defaultOptionId });
1165
1336
  const computer = computerConsent({ session, request, options, defaultOptionId, brain, origin });
1166
1337
  const expiresAt = now() + r.cardTimeoutMinutes * 60_000;
1167
1338
  const pref = r.preferences?.[request.taskClass];
@@ -1169,7 +1340,7 @@ export function createAssistantRouter({
1169
1340
  routeId, requestId, sessionId, origin, status: 'pending', mode, workflowId,
1170
1341
  taskClass: request.taskClass, summary: request.summary, confidence: request.confidence, needsVision: request.needsVision,
1171
1342
  needsOutput: request.needsOutput || null, dispatchOnly: request.dispatchOnly === true, visionOnly: request.visionOnly === true,
1172
- reasons: evaluation.reasons, request, options, defaultOptionId, runIds: new Set(runIds), waiters: new Set(),
1343
+ reasons: evaluation.reasons, request, options, defaultOptionId, sides, defaults, runIds: new Set(runIds), waiters: new Set(),
1173
1344
  createdAt: now(), expiresAt, timer: null, mailboxOnDecision: origin === 'dispatch', brain,
1174
1345
  // The option whose approval is also the computer approval (computerConsent), as this card was sent.
1175
1346
  computer,
@@ -1188,6 +1359,8 @@ export function createAssistantRouter({
1188
1359
  visionOnly: request.visionOnly === true,
1189
1360
  brain: { provider: brain.provider, model: brain.model || null, label: brainInfo?.label || brain.model || providerLabel(brain.provider), vision: brainInfo?.vision ?? null },
1190
1361
  options, defaultOptionId,
1362
+ // The card's Here / Dispatch switch: which side can run the task (and why not), and the option each side starts on.
1363
+ sides, defaults,
1191
1364
  other: { allowed: true, providers: CATALOG_PROVIDERS, catalogPath: '/api/assistant/catalog' },
1192
1365
  remember: { available: request.taskClass !== 'chat', taskClass: request.taskClass, label: request.taskClassLabel, current: pref ? (pref.label || [pref.provider, pref.model].filter(Boolean).join('/')) : null },
1193
1366
  decline: { allowed: true, label: 'Cancel' },
@@ -1396,21 +1569,28 @@ export function createAssistantRouter({
1396
1569
  try { await budget.ensureCovered(card.sessionId, { provider: target.provider, forBrain: target.kind === 'direct', free: targetIsFree(cat, target) }); } catch { /* decided on the shares as they are */ }
1397
1570
  if (cards.get(routeId) !== card || card.status !== 'pending') openCard(routeId, { sessionId });
1398
1571
  if (!budgetCovers(coverageOf(card.sessionId), cat, target)) {
1572
+ const unpriced = unpricedText(target.provider);
1573
+ if (unpriced) throw routeError('BUDGET_UNPRICED', unpriced, 409, { provider: target.provider });
1399
1574
  throw routeError('BUDGET_SHARE_EXHAUSTED', `Budget: the ${providerLabel(target.provider)} share of this session's budget is used up. Pick another option (or a free model), or raise the budget.`, 409, { provider: target.provider });
1400
1575
  }
1401
1576
  }
1577
+ // "Here" on a held dispatch's card: the brain does the task in this conversation, so the held
1578
+ // worker runs are not started (they stop, route_here) and the brain is told to do it itself.
1579
+ const keptHere = card.runIds.size > 0 && target.kind === 'direct';
1402
1580
  // Every held run must be able to start on the pick (the dispatcher's checkHeld: a design run's
1403
1581
  // sight and browser settings on that provider). A pick one would refuse is not approved, saved
1404
- // or reported started: the card stays open with the reason.
1405
- for (const runId of card.runIds) {
1406
- const refusal = call('checkHeld', runId, target);
1407
- if (refusal) throw routeError(refusal.code || 'ROUTE_INVALID', refusal.message || 'The held run cannot start on that model.', Number(refusal.status) || 400);
1582
+ // or reported started: the card stays open with the reason. A "here" pick starts none.
1583
+ if (!keptHere) {
1584
+ for (const runId of card.runIds) {
1585
+ const refusal = call('checkHeld', runId, target);
1586
+ if (refusal) throw routeError(refusal.code || 'ROUTE_INVALID', refusal.message || 'The held run cannot start on that model.', Number(refusal.status) || 400);
1587
+ }
1408
1588
  }
1409
1589
  // "Do it here" on another model continues in the turn the card was raised in,
1410
1590
  // handed over first. The runtime refuses a pick that turn no longer owns (a
1411
1591
  // newer prompt, a Stop or the session closing came after it): then nothing is
1412
1592
  // approved or remembered, and the card expires on every host.
1413
- const continuation = !card.runIds.size && continuesHere(target, brain);
1593
+ const continuation = (keptHere || !card.runIds.size) && continuesHere(target, brain);
1414
1594
  // The decision is claimed here, before the first side effect (the runtime's continuation, the
1415
1595
  // held runs, the mailbox): nothing a sink does can close or decide this card again meanwhile.
1416
1596
  card.status = 'approved';
@@ -1428,21 +1608,23 @@ export function createAssistantRouter({
1428
1608
  remembered = true;
1429
1609
  } catch (error) { log('router:remember-error', error?.message || String(error)); }
1430
1610
  }
1611
+ const heldRuns = keptHere ? [] : [...card.runIds];
1612
+ const droppedRuns = keptHere ? [...card.runIds] : [];
1431
1613
  const route = {
1432
1614
  routeId, status: 'approved', sessionId: card.sessionId, taskClass: card.taskClass, summary: card.summary, confidence: card.confidence,
1433
- target, decidedBy: 'user', remembered, optionId, corrections, mode: card.mode, runIds: [...card.runIds],
1615
+ target, decidedBy: 'user', remembered, optionId, corrections, mode: card.mode, runIds: heldRuns,
1434
1616
  reason: card.options.find((o) => o.id === optionId)?.reason || null, alternatives: card.alternatives, expiresAt: now() + DECIDED_TTL_MS,
1435
1617
  };
1436
1618
  decided.set(routeId, route);
1437
1619
  noteBudgetTask(card.sessionId, { taskClass: card.taskClass, summary: card.summary }, target);
1438
- const heldRuns = [...card.runIds];
1439
1620
  // A remote session: the held run this card was for is its one dispatch.
1440
1621
  if (heldRuns.length && session.remote === true) route.consumed = true;
1622
+ for (const runId of droppedRuns) call('declineHeld', runId, 'route_here');
1441
1623
  for (const runId of heldRuns) call('startHeld', runId, target, { routeId, decidedBy: 'user', remembered, optionId });
1442
1624
  const view = routeView(route, { catalog: cat, brain, brainInfo });
1443
1625
  call('routeEvent', card.sessionId, 'decided', view);
1444
1626
  const inTurnWaiter = card.waiters.size > 0;
1445
- const viaMailbox = !inTurnWaiter && !continuation && (card.mailboxOnDecision || heldRuns.length > 0);
1627
+ const viaMailbox = !inTurnWaiter && !continuation && (card.mailboxOnDecision || card.runIds.size > 0);
1446
1628
  // The card said the Mac will be controlled, and this is the option it said it for: the yes is
1447
1629
  // also the computer approval of the turn that carries the route out (this one, or the mailbox
1448
1630
  // turn that executes the decided route). From what the card recorded when it was sent; the
@@ -1451,9 +1633,19 @@ export function createAssistantRouter({
1451
1633
  // `authority` is passed on untouched and never kept: the runtime alone can tell whose it is.
1452
1634
  call('computerApproved', card.sessionId, { routeId, origin, viaMailbox, authority });
1453
1635
  }
1454
- const result = resultFor('approved', { routeId, mode: card.mode, source: 'user', target, corrections, cat, brain, brainInfo, remembered, runIds: heldRuns, continuation, sessionId: card.sessionId });
1636
+ // The brain proposed one kind and the user picked the other (the card's Here / Dispatch switch):
1637
+ // the result says so first, so the brain follows the pick, not its proposal.
1638
+ const proposed = card.request?.proposals?.[0]?.kind || null;
1639
+ const picked = target.accountKind || target.kind;
1640
+ const switched = proposed && proposed !== picked ? picked : null;
1641
+ const result = resultFor('approved', { routeId, mode: card.mode, source: 'user', target, corrections, cat, brain, brainInfo, remembered, runIds: heldRuns, continuation, sessionId: card.sessionId, switched, droppedRuns });
1455
1642
  if (viaMailbox) {
1456
- call('mailbox', card.sessionId, { kind: 'route_decided', route: view, runIds: heldRuns, text: heldRuns.length ? `The user picked ${view.target?.label || 'a model'}; the held run${heldRuns.length === 1 ? '' : 's'} started.` : `The user picked ${view.target?.label || 'a model'}. ${result.next}` });
1643
+ const label = view.target?.label || 'a model';
1644
+ const what = card.summary ? `"${card.summary}"` : 'this task';
1645
+ const text = heldRuns.length ? `The user picked ${label}; the held run${heldRuns.length === 1 ? '' : 's'} started.`
1646
+ : droppedRuns.length ? `The user chose to do ${what} here with ${label} instead of on a worker: the held run${droppedRuns.length === 1 ? '' : 's'} ${droppedRuns.join(', ')} ${droppedRuns.length === 1 ? 'was' : 'were'} not started. Do that task here yourself now; do not dispatch it again.`
1647
+ : `The user picked ${label}. ${result.next}`;
1648
+ call('mailbox', card.sessionId, { kind: 'route_decided', route: view, runIds: heldRuns, ...(droppedRuns.length ? { droppedRunIds: droppedRuns } : {}), text });
1457
1649
  }
1458
1650
  settle(card, { status: 'approved', result });
1459
1651
  closeCard(card);
@@ -1483,6 +1675,12 @@ export function createAssistantRouter({
1483
1675
  const remote = session.remote === true;
1484
1676
  if (routeId) {
1485
1677
  const approved = decided.get(routeId);
1678
+ // The user picked "here" on this route's card: it runs in this conversation, never on a worker
1679
+ // (a brain's own direct route, or a saved one in Never ask, keeps its old dispatch behaviour).
1680
+ if (approved && approved.sessionId === sessionId && approved.target?.kind === 'direct' && approved.decidedBy === 'user') {
1681
+ const where = approved.target.model ? ` on ${approved.target.model}` : '';
1682
+ throw routeError('ROUTE_IS_DIRECT', `Route ${routeId} was decided by the user to run here, in this conversation${where}, not on a worker. Do the task yourself now; do not dispatch it. If it should go to a worker after all, ask the user, then call agent_route again.`, 409, { routeId, target: approved.target });
1683
+ }
1486
1684
  if (approved && approved.sessionId === sessionId && approved.target && !approved.consumed) {
1487
1685
  // A remote session's route is claimed here, so a second dispatch cannot take it meanwhile.
1488
1686
  // `claimed` tells the dispatcher to give it back (releaseRoute) if it refuses the launch
@@ -1495,7 +1693,11 @@ export function createAssistantRouter({
1495
1693
  let runTarget = t;
1496
1694
  let budgetCorrection = null;
1497
1695
  if (budgeted() && !budgetCovers(coverageOf(sessionId), cat, t)) {
1498
- if (approved.mode === 'always-ask' && approved.decidedBy === 'user') throw routeError('BUDGET_SHARE_EXHAUSTED', 'The selected model has no session budget left. Choose a new task route or raise the budget.', 409, { routeId, provider: t.provider });
1696
+ if (approved.mode === 'always-ask' && approved.decidedBy === 'user') {
1697
+ const unpriced = unpricedText(t.provider);
1698
+ if (unpriced) throw routeError('BUDGET_UNPRICED', `${unpriced} Choose a new task route.`, 409, { routeId, provider: t.provider });
1699
+ throw routeError('BUDGET_SHARE_EXHAUSTED', 'The selected model has no session budget left. Choose a new task route or raise the budget.', 409, { routeId, provider: t.provider });
1700
+ }
1499
1701
  try { await budget.ensureCovered(sessionId, { provider: t.provider, forBrain: false, free: targetIsFree(cat, t), task: { taskClass: approved.taskClass, summary: approved.summary || null, provider: t.provider, model: t.model || null } }); } catch { /* decided on the shares as they are */ }
1500
1702
  const request = normalizeRouteRequest({ task_class: approved.taskClass, summary: approved.summary || '', confidence: 1, needs_vision: !!spec.usesComputer, proposals: [{ kind: 'dispatch', provider: t.provider, model: t.model || null, effort: t.effort || null }] }, { brain });
1501
1703
  const moved = budgetMove({ target: { ...t, kind: 'dispatch' }, request, catalog: cat, brain, brainInfo, routing: r, coverage: coverageOf(sessionId) });
@@ -1635,7 +1837,7 @@ export function createAssistantRouter({
1635
1837
  const mode = modeFor(sessionId);
1636
1838
  const prefs = Object.entries(r.preferences || {}).filter(([, p]) => p?.provider)
1637
1839
  .map(([cls, p]) => `${cls} → ${p.kind === 'direct' ? 'here' : p.provider}${p.model ? `/${p.model}` : ''}${p.effort ? ` (${p.effort})` : ''}${hasRouteAccounts(p.provider) ? ` as ${routeAccountText(p, { accounts: liveAccounts(catalog.peek?.()), brain: getSession(sessionId)?.brain, workers: { codex: configStore?.workers?.()?.codexAccount, 'claude-code': configStore?.workers?.()?.claudeAccount } })}` : ''}`);
1638
- const modeLine = mode === 'always-ask' ? 'always ask (call agent_route before every actionable task; the user picks the model)'
1840
+ const modeLine = mode === 'always-ask' ? 'always ask (call agent_route before every actionable task; the user picks the model, and whether it runs here or on a worker)'
1639
1841
  : mode === 'never' ? 'never ask (agent_route approves at once; the saved routes below are binding and agent_route applies them over your pick, so classify the task correctly)'
1640
1842
  : `ask when unsure (agent_route asks the user below ${Math.round(r.askBelow * 100)}% confidence or for unknown/missing models)`;
1641
1843
  const text = `[SynaBun Router] Route mode: ${modeLine}.${prefs.length ? ` ${mode === 'never' ? 'Saved routes (binding)' : 'Remembered routes'}: ${prefs.join('; ')}.` : ''}`;