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
@@ -1,5 +1,5 @@
1
1
  import { readStoredToolListRecord, toolListRecordForUpdate } from './lib/tool-lists.js';
2
- import { externalServerInventory, selectExternalServers, externalCatalog, EXTERNAL_SERVER_LIMITATIONS, scopeOpenCodeServers, retainExternalRefs, writePrivateMcpConfig, redactMcpValues, mcpDefinitionValues } from './lib/mcp-external-servers.js';
2
+ import { externalServerInventory, selectExternalServers, externalCatalog, EXTERNAL_SERVER_LIMITATIONS, scopeOpenCodeServers, retainExternalRefs, writePrivateMcpConfig, redactMcpValues, mcpSecretValues } from './lib/mcp-external-servers.js';
3
3
  import { openCodeWorkerData } from './lib/opencode-worker-data.js';
4
4
  import { resumeSandboxPlan } from './lib/worker-resume.js';
5
5
  import { waitForSynabunTools, synabunConfigured, invalidateSynabunTools, synabunToolsTimeoutText } from './lib/opencode-mcp-ready.js';
@@ -25,7 +25,7 @@ import { SANDBOX_EXEC, buildWorkerSandboxProfile, canonicalPath, sandboxExecArgv
25
25
  import { createWorkerMcpChannel } from './lib/worker-mcp-channel.js';
26
26
  import { createPageSession, PAGE_SECRET_PATHS, PAGE_SECRET_REFRESH_PATH, SERVICE_WORKER_PATH } from './lib/page-session.js';
27
27
  import { normalizeViewUrl } from './public/shared/view-variant.js';
28
- import { createPeerIdentity } from './lib/peer-identity.js';
28
+ import { createPeerIdentity, debugClientPolicy } from './lib/peer-identity.js';
29
29
  import { createOwnerAuthority } from './lib/owner-authority.js';
30
30
  import { collectRelayListenerPorts, localProxyPorts } from './lib/worker-relay-ports.js';
31
31
  import { machineNames } from './lib/machine-names.js';
@@ -56,10 +56,11 @@ import { createEventLoopMonitor } from './lib/event-loop-monitor.js';
56
56
  import { createRulesetInstaller } from './lib/rulesets/installer.js';
57
57
  import { registerRulesetRoutes, registerLegacyRulesetRoutes } from './lib/rulesets/api.js';
58
58
  import { createPtyHostManager } from './lib/pty-host/manager.js';
59
- import { AGENT_HEADERS, hostNameOf, isAllowedHost, isAllowedWebSocketOrigin, parseCookies, refuseUpgrade } from './lib/http-guards.js';
59
+ import { AGENT_HEADERS, hostNameOf, isAllowedHost, isAllowedWebSocketOrigin, parseCookies, refuseUpgrade, requireLocal as requireLoopbackSocket } from './lib/http-guards.js';
60
60
  import { createRequestGates, extraHostNames, hostsOf, savedValueOf, startedValueOf } from './lib/request-gate.js';
61
61
  import { createGitDiffRouter } from './lib/git-diff-api.js';
62
- import { gitRead, gitReadSync, setGitFallbackFolder, withGitFallback } from './lib/git-read.js';
62
+ import { setGitFallbackFolder } from './lib/git-read.js';
63
+ import { gitStatusRunner, projectStatus } from './lib/git-status-runner.js';
63
64
  import { createPhoneAuthority } from './lib/remote-policy.js';
64
65
  import { brainCredentialOf } from './lib/virtual-socket.js';
65
66
  import { buildTerminalEnv, interactiveShellArgs, cliWrapperArgs } from './lib/terminal-env.js';
@@ -104,6 +105,8 @@ import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, unlink
104
105
  import * as fsp from 'fs/promises';
105
106
  import { createInterface as createReadlineInterface } from 'readline';
106
107
  import { jsonlLines } from './lib/jsonl-lines.js';
108
+ import { createHistoryCache } from './lib/claude-history-cache.js';
109
+ import { createRolloutReader } from './lib/codex-rollout-reader.js';
107
110
  import { randomBytes, randomUUID, createHash, pbkdf2Sync, createDecipheriv } from 'crypto';
108
111
  import { exec, execFile, execSync, spawn, spawnSync } from 'child_process';
109
112
  import { promisify } from 'util';
@@ -192,6 +195,7 @@ import { codexReasoningEffort } from './lib/effort-levels.js';
192
195
  import {
193
196
  createClaudeNativeLoopAdapter,
194
197
  createCodexNativeLoopAdapter,
198
+ createAntigravityNativeLoopAdapter,
195
199
  createOpenCodeNativeLoopAdapter,
196
200
  openCodeRunConfigFor,
197
201
  openCodeRunEnvFor,
@@ -201,9 +205,9 @@ import {
201
205
  } from './lib/native-loop-providers.js';
202
206
  import { createOpenCodeActivityRelay } from './lib/opencode-worker-activity.js';
203
207
  import { browserServePortDenial } from './lib/browser-tool-policy.js';
204
- import { createNativeServeBoundary, nativeServeCapability } from './lib/opencode-native-boundary.js';
208
+ import { createNativeServeBoundary, nativeServeCapability, nativeServeReason, narrowOffenders } from './lib/opencode-native-boundary.js';
205
209
  const nativeServeBoundary = createNativeServeBoundary();
206
- import { setBrowserNetworkRule, usesNativeBrowserNetworkGuard, disposeBrowserNetworkGuard, MONEY_PATTERNS, SERVE_PORT_PATTERNS } from './lib/browser-network-guard.js';
210
+ import { setBrowserNetworkRule, usesNativeBrowserNetworkGuard, disposeBrowserNetworkGuard, recoverBrowserNetworkGuard, MONEY_PATTERNS, SERVE_PORT_PATTERNS } from './lib/browser-network-guard.js';
207
211
  import { browserPolicyRoots } from './lib/browser-policy-roots.js';
208
212
  import {
209
213
  mergeCodexModelContextCapabilities,
@@ -234,12 +238,21 @@ import { initBlobStore, blobGetNamespace, blobGet, blobPut, blobDelete, blobClea
234
238
  import { createUiBlobRouter } from './lib/ui-blob-api.js';
235
239
  import { memoCache, asyncPool } from './lib/memo-cache.js';
236
240
  import {
241
+ buildAntigravityMcpEntry,
237
242
  buildClaudeMcpEntry,
238
243
  buildCodexMcpKv,
239
- buildGeminiMcpEntry,
244
+ describeAntigravityMcpEntry,
245
+ mergeAntigravityMcpConfig,
240
246
  mergeClaudeMcpConfig,
241
- mergeGeminiMcpConfig,
247
+ parseAntigravityMcpConfig,
248
+ removeAntigravityMcpServer,
242
249
  } from './lib/mcp-client-config.js';
250
+ import { ANTIGRAVITY_PANEL_CAPABILITIES, ANTIGRAVITY_WS_PATH, createAntigravityPanelBridge, validateRunHome } from './lib/antigravity/panel-bridge.js';
251
+ import { migrateGeminiFiles, migrateUiStateValues as migrateGeminiUiState } from './lib/antigravity/migrate-gemini.js';
252
+ import { groupSessionsByProject, listAntigravityModels, listAntigravitySessions } from './lib/antigravity/catalog.js';
253
+ import { defaultAntigravityPolicy } from './lib/antigravity/policy.js';
254
+ import { antigravityHomeDir, antigravityHomesRoot, pruneAntigravityHomes } from './lib/antigravity/home.js';
255
+ import { antigravityStores, findUserConversation, parseAntigravityTranscript, readTranscriptText } from './lib/antigravity/transcripts.js';
243
256
  import {
244
257
  buildAgentDisallowedTools,
245
258
  agentAccessFrom,
@@ -304,7 +317,7 @@ import { registerSkillEvalRoutes } from './lib/skills/evals-api.js';
304
317
  import { evalConfinementSupport } from './lib/skills/eval-confinement.js';
305
318
  import { evalShellReadPaths, evalShellSupported, prepareEvalShell } from './lib/skills/eval-sandbox.js';
306
319
  import { urlProblem as skillSourceUrlProblem } from './public/shared/skills/ss-creation.js';
307
- import { claudeRowsToMessages, codexRolloutToMessages, opencodeRowsToMessages } from './lib/skills/creation-sources.js';
320
+ import { antigravityMessagesToMessages, claudeRowsToMessages, codexRolloutToMessages, opencodeRowsToMessages } from './lib/skills/creation-sources.js';
308
321
  import { renderSkillBody } from './public/shared/skills/ss-args.js';
309
322
  import { claudeBoolean } from './public/shared/skills/ss-profiles.js';
310
323
  import { generateSessionTitle } from './lib/session-title-generator.js';
@@ -355,6 +368,7 @@ import {
355
368
  import { ensureProjectCategories as ensureProjectCategoriesInFile, ensureCategories as ensureCategoriesInFile, FEATURE_CATEGORIES } from '../hooks/claude-code/shared.mjs';
356
369
  import { getDataHome, getDataHomeDiagnostics, isGlobalInstall, pathIsInside } from '../lib/paths.js';
357
370
  import { launcherLogPath } from '../lib/first-launch-protection.js';
371
+ import { prunePreUpdateSnapshots, resolvePreUpdateKeep } from '../lib/update-snapshot.js';
358
372
  import { launcherRegistrationAllowed, registerStartLauncher, startLauncherState } from '../lib/start-launcher.js';
359
373
  import { effectiveNpmPrefix } from '../lib/child-path.js';
360
374
  import { checkRuntimeDependencies, fallbackInstallArgs, fallbackInstallEnv, findInstalled, mcpCompiler, npmCommand, npmInvocation } from '../lib/runtime-dependencies.js';
@@ -381,7 +395,7 @@ import {
381
395
  getFbActionBudget, getFbEngagementTier, FB_ACTION_TYPES,
382
396
  } from './lib/db.js';
383
397
  import {
384
- rebuildClaudeCache, rebuildCodexCache, rebuildOpencodeCache,
398
+ rebuildClaudeCache, rebuildCodexCache, rebuildOpencodeCache, rebuildAntigravityCache,
385
399
  } from './lib/session-cache-builder.js';
386
400
  import {
387
401
  clearOpenCodeHistory,
@@ -433,6 +447,9 @@ try {
433
447
  // allowed until a restart. What the environment itself gives is noted first and put back after the load; the
434
448
  // file's own line is read when a request asks (publicWebOrigins).
435
449
  const startedAllowedHosts = startedValueOf(process.env);
450
+ // The profiling exemption (lib/peer-identity.js debugClientPolicy) is read from the started environment only, before
451
+ // the data home's .env can add to it.
452
+ const startedDebugClient = debugClientPolicy(process.env);
436
453
  dotenvConfig({ path: resolve(getDataHome(), '.env') });
437
454
  startedAllowedHosts.restore();
438
455
 
@@ -542,7 +559,7 @@ app.use(requestGates.features);
542
559
  // in front of every route (the page secret's pieces are made further down and read only when a request comes).
543
560
  // This install's root (PACKAGE_ROOT, defined further down): SynaBun's own MCP server over HTTP is the one listening
544
561
  // program the lookup knows, by its main script under it (mcp-server/dist/http.js, `npm run start:http`).
545
- const _peerIdentity = createPeerIdentity({ log: (line) => console.warn(line), disabled: String(process.env.SYNABUN_PEER_IDENTITY || '').toLowerCase() === 'off', synabunRoots: [resolve(__dirname, '..')] });
562
+ const _peerIdentity = createPeerIdentity({ log: (line) => console.warn(line), disabled: String(process.env.SYNABUN_PEER_IDENTITY || '').toLowerCase() === 'off', synabunRoots: [resolve(__dirname, '..')], debugClient: startedDebugClient });
546
563
  // The one decision for the owner's routes and sockets (lib/owner-authority.js): every write to /api but the hooks'
547
564
  // and the agents' feature data, the two reads that hand out a secret, and the owner's sockets are refused to the
548
565
  // automation browser (its marker or its process), relays, other SynaBun servers, remote logins and an agent's tree
@@ -2388,6 +2405,7 @@ function gracefulShutdown(reason = 'shutdown') {
2388
2405
  }
2389
2406
 
2390
2407
  async function runGracefulShutdown(reason) {
2408
+ gitStatusRunner.shutdown();
2391
2409
  console.log(`[server] gracefulShutdown(${reason}) — releasing children...`);
2392
2410
 
2393
2411
  // Native automation providers own SDK subprocesses/isolated serves that are
@@ -2399,6 +2417,8 @@ async function runGracefulShutdown(reason) {
2399
2417
  try { keepRuns = _assistantDispatcher?.suspendForShutdown?.(reason) || []; } catch (err) { console.warn(' assistant suspend err:', err?.message); }
2400
2418
  try { await _nativeLoopRuntime?.shutdown(reason, { keep: keepRuns }); } catch (err) { console.warn(' native loop stop err:', err?.message); }
2401
2419
  try { stopAllIsolatedServes(); } catch (err) { console.warn(' isolated opencode stop err:', err?.message); }
2420
+ // The Antigravity panel's `agy acp` processes (one per open panel socket).
2421
+ try { await antigravityPanel.closeAll(); } catch (err) { console.warn(' antigravity panel stop err:', err?.message); }
2402
2422
  // The code editor's completion serve, and today's autocomplete counters.
2403
2423
  try { await editorCompletion.stop(); } catch (err) { console.warn(' editor completion stop err:', err?.message); }
2404
2424
 
@@ -2818,7 +2838,17 @@ app.post('/api/system/run-update', async (req, res) => {
2818
2838
  appVersion: installPlan.current,
2819
2839
  additionalEntries: getBackupAdditionalEntries(),
2820
2840
  }));
2821
- applyBackupRetention({ folderPath: updateBackupDir });
2841
+ // Older pre-update snapshots are pruned only now, after this one is written and verified: a
2842
+ // failed createVerifiedBackup above threw and never reached here.
2843
+ try {
2844
+ const configuredKeep = loadAutoBackupConfig()?.retention?.preUpdateKeep ?? null;
2845
+ prunePreUpdateSnapshots({
2846
+ folderPath: updateBackupDir,
2847
+ keep: resolvePreUpdateKeep(configuredKeep),
2848
+ keepPath: snapshot.path,
2849
+ log: (message) => console.log(`[update-backup] ${message}`),
2850
+ });
2851
+ } catch (error) { console.error(`[update-backup] retention cleanup failed: ${error?.message || error}`); }
2822
2852
  const handoffPath = resolve(DATA_HOME, 'data', 'update-handoff.json');
2823
2853
  writeFileSync(handoffPath, JSON.stringify({
2824
2854
  version: 1,
@@ -3656,6 +3686,28 @@ function getClaudeSdkExecutable() {
3656
3686
  });
3657
3687
  }
3658
3688
 
3689
+ // Google Antigravity's CLI (`agy`): what the side panel (`agy acp`), the workers, the model list and the session list
3690
+ // start. The resolver's answer, as a program and the words before its own arguments: a binary as it is, a script
3691
+ // through node, a PowerShell script through PowerShell. A Windows command file (cmd.exe) cannot take words after the
3692
+ // fact, so it is reported as not runnable here (the terminal profile still runs it). null when none is installed.
3693
+ function antigravityLaunch() {
3694
+ const found = cliResolver.launcher('antigravity');
3695
+ _noteCliLookup('antigravity', found, 'Antigravity CLI');
3696
+ const spec = found.spec;
3697
+ if (!found.path || !spec || found.state === 'broken') return null;
3698
+ if (spec.kind === 'direct') return { command: spec.launchPath || found.path, args: [], version: found.version || null, path: found.path };
3699
+ if (spec.kind === 'node-script') return { command: process.execPath, args: [spec.launchPath], version: found.version || null, path: found.path };
3700
+ if (spec.kind === 'powershell') return { command: 'powershell.exe', args: ['-NoProfile', '-NonInteractive', '-File', spec.launchPath], version: found.version || null, path: found.path };
3701
+ return null;
3702
+ }
3703
+ // The environment an Antigravity process gets: the PATH a new terminal has (agy runs the user's tools), never the
3704
+ // owner's live data paths or a brain's credential.
3705
+ function antigravityChildEnv() {
3706
+ const env = withPathEnv({ ...process.env }, getAugmentedPath());
3707
+ for (const key of ['SQLITE_DB_PATH', 'MEMORY_DATA_DIR', 'SYNABUN_BRAIN_TOKEN', 'SYNABUN_ASSISTANT_SESSION', 'SYNABUN_ASSISTANT_THREADS']) delete env[key];
3708
+ return env;
3709
+ }
3710
+
3659
3711
  // Codex: what the SDK and the app-server start. On Windows the native codex.exe
3660
3712
  // an npm command file leads to (the SDK spawns without a shell); the file found
3661
3713
  // otherwise. SynaBun carries no Codex (lib/external-tools.js): without one,
@@ -3706,22 +3758,25 @@ function opencodeServeLaunch(port, { env = process.env, password = null, passwor
3706
3758
  _noteCliLookup('opencode', found, 'OpenCode');
3707
3759
  const line = configuredCommandLine('opencode');
3708
3760
  const nativeWebSocketSafe = nativeServeCapability({ version: found.version, commandLine: !!line, passwordFrom });
3709
- nativeServeBoundary.assertLaunch(nativeWebSocketSafe);
3761
+ // What the browser boundary names when this serve is the one that blocks a native (MoreLogin) context. A command
3762
+ // line from Settings runs whatever it names: the resolver's version is of another binary, so none is reported.
3763
+ const nativeServe = { version: line ? null : (found.version || null), path: line || found.launchPath || found.path || null, reason: nativeServeReason({ version: found.version, commandLine: !!line, passwordFrom }) };
3764
+ nativeServeBoundary.assertLaunch(nativeWebSocketSafe, { port, ...nativeServe });
3710
3765
  const spec = serveSpecFor(port, { env, password, passwordFrom });
3711
3766
  const words = spec.args.join(' ');
3712
3767
  if (IS_WIN) {
3713
3768
  const spawnEnv = withPathEnv(spec.env, getAugmentedPath());
3714
3769
  if (!line && found.spec) {
3715
3770
  const launch = spawnArgsFor(found.spec, spec.args, { env: spawnEnv });
3716
- return { command: launch.command, args: launch.args, options: { ...launch.options, env: spawnEnv, windowsHide: true }, label: `${found.launchPath} ${words}`, password: spec.password, passwordFrom: spec.passwordFrom, nativeWebSocketSafe };
3771
+ return { command: launch.command, args: launch.args, options: { ...launch.options, env: spawnEnv, windowsHide: true }, label: `${found.launchPath} ${words}`, password: spec.password, passwordFrom: spec.passwordFrom, nativeWebSocketSafe, nativeServe };
3717
3772
  }
3718
3773
  const text = `${line || 'opencode'} ${words}`;
3719
- return { command: DEFAULT_SHELL, args: ['/d', '/s', '/c', text], options: { env: spawnEnv, windowsHide: true }, label: text, password: spec.password, passwordFrom: spec.passwordFrom, nativeWebSocketSafe };
3774
+ return { command: DEFAULT_SHELL, args: ['/d', '/s', '/c', text], options: { env: spawnEnv, windowsHide: true }, label: text, password: spec.password, passwordFrom: spec.passwordFrom, nativeWebSocketSafe, nativeServe };
3720
3775
  }
3721
3776
  const cmd = line || (found.path ? shellSingleQuote(found.path) : 'opencode');
3722
3777
  const unset = spec.passwordFrom === 'stdout' ? `${serveUnsetCommand()}; ` : '';
3723
3778
  // A packaged application's runtime and npm after the user's own (the login shell adds the user's profile on top).
3724
- return { command: DEFAULT_SHELL, args: ['-lc', `${unset}exec ${cmd} ${words}`], options: { env: withPackagedPathLast(spec.env) }, label: `${cmd} ${words}`, password: spec.password, passwordFrom: spec.passwordFrom, nativeWebSocketSafe };
3779
+ return { command: DEFAULT_SHELL, args: ['-lc', `${unset}exec ${cmd} ${words}`], options: { env: withPackagedPathLast(spec.env) }, label: `${cmd} ${words}`, password: spec.password, passwordFrom: spec.passwordFrom, nativeWebSocketSafe, nativeServe };
3725
3780
  }
3726
3781
 
3727
3782
  // Which OpenCode the binary SynaBun starts is: the resolver's `--version` of it
@@ -5626,10 +5681,10 @@ CRITICAL — When you need clarification during planning:
5626
5681
  return item;
5627
5682
  }
5628
5683
 
5629
- function loadCodexThreadHistoryFallback(threadId) {
5630
- if (!threadId) return [];
5684
+ function codexRolloutPathFor(threadId) {
5685
+ if (!threadId) return '';
5631
5686
  const dbPath = resolve(activeCodexHome, 'state_5.sqlite');
5632
- if (!existsSync(dbPath)) return [];
5687
+ if (!existsSync(dbPath)) return '';
5633
5688
 
5634
5689
  let db = null;
5635
5690
  let rolloutPath = '';
@@ -5642,20 +5697,27 @@ CRITICAL — When you need clarification during planning:
5642
5697
  } finally {
5643
5698
  try { db?.close(); } catch {}
5644
5699
  }
5645
- if (!rolloutPath || !existsSync(rolloutPath)) return [];
5700
+ return rolloutPath && existsSync(rolloutPath) ? rolloutPath : '';
5701
+ }
5646
5702
 
5647
- let raw = '';
5703
+ // The rollout as history items: read as a stream, parsed once while the file is unchanged
5704
+ // (lib/codex-rollout-reader.js), so the early preview and the fallback that follows share one read.
5705
+ async function loadCodexThreadHistoryFallback(threadId) {
5706
+ const rolloutPath = codexRolloutPathFor(threadId);
5707
+ if (!rolloutPath) return [];
5648
5708
  try {
5649
- raw = readFileSync(rolloutPath, 'utf-8');
5709
+ return await codexRolloutReader.items(rolloutPath, parseCodexRolloutLines);
5650
5710
  } catch (err) {
5651
5711
  console.warn('[codex-skin] Failed to read rollout history:', err.message);
5652
5712
  return [];
5653
5713
  }
5714
+ }
5654
5715
 
5716
+ async function parseCodexRolloutLines(lines) {
5655
5717
  const items = [];
5656
5718
  const calls = new Map();
5657
5719
  let seq = 0;
5658
- for (const line of raw.split('\n')) {
5720
+ for await (const line of lines) {
5659
5721
  if (!line.trim()) continue;
5660
5722
  let entry;
5661
5723
  try {
@@ -6178,7 +6240,29 @@ CRITICAL — When you need clarification during planning:
6178
6240
  }
6179
6241
 
6180
6242
 
6181
- async function bootstrapThread(threadId, cwd = PACKAGE_ROOT) {
6243
+ // Only for the thread the client shows (it asks with `preview: true`), only before the app-server is up, read
6244
+ // without blocking its start; dropped once the real history went out. The read is reused by the fallback.
6245
+ function sendEarlyCodexRolloutPreview(threadId, state) {
6246
+ if (!threadId || initialized) return null;
6247
+ const job = loadCodexThreadHistoryFallback(threadId);
6248
+ job.then((previewItems) => {
6249
+ if (state.historySent || !Array.isArray(previewItems) || !previewItems.length) return;
6250
+ const previewThreadId = String(threadId);
6251
+ sendToClient({
6252
+ type: 'history',
6253
+ preview: true,
6254
+ threadId: previewThreadId,
6255
+ thread: { id: previewThreadId, turns: [] },
6256
+ fallbackItems: previewItems,
6257
+ historyMode: 'read',
6258
+ });
6259
+ }).catch(() => {});
6260
+ return job;
6261
+ }
6262
+
6263
+ async function bootstrapThread(threadId, cwd = PACKAGE_ROOT, { preview = false } = {}) {
6264
+ const previewState = { historySent: false };
6265
+ if (preview) sendEarlyCodexRolloutPreview(threadId, previewState);
6182
6266
  await ensureInitialized();
6183
6267
  if (!threadId) {
6184
6268
  displayedThreadId = activeThreadId;
@@ -6193,11 +6277,13 @@ CRITICAL — When you need clarification during planning:
6193
6277
  const historyMode = activeThreadId && effectiveThreadId && activeThreadId === effectiveThreadId
6194
6278
  ? 'resume'
6195
6279
  : 'read';
6280
+ const fallbackItems = await loadCodexThreadHistoryFallback(effectiveThreadId);
6281
+ previewState.historySent = true;
6196
6282
  sendToClient({
6197
6283
  type: 'history',
6198
6284
  threadId: effectiveThreadId,
6199
6285
  thread: sanitizeCodexThreadForClient(thread),
6200
- fallbackItems: loadCodexThreadHistoryFallback(effectiveThreadId),
6286
+ fallbackItems,
6201
6287
  historyMode,
6202
6288
  });
6203
6289
  // `history` and `ready` must describe the same displayed thread. Returning
@@ -6206,6 +6292,7 @@ CRITICAL — When you need clarification during planning:
6206
6292
  sendToClient({ type: 'ready', threadId: effectiveThreadId });
6207
6293
  } catch (err) {
6208
6294
  console.warn('[codex-skin] Failed to resume thread:', threadId, err.message);
6295
+ previewState.historySent = true;
6209
6296
  sendToClient({ type: 'history', thread: null, resumeFailed: true, threadId });
6210
6297
  sendToClient({ type: 'error', message: `Could not restore saved Codex thread. ${err.message}` });
6211
6298
  sendToClient({ type: 'ready', threadId });
@@ -6318,7 +6405,7 @@ CRITICAL — When you need clarification during planning:
6318
6405
  const runtime = opts.runtimeOptions || await resolveCodexThreadRuntimeOptions(opts);
6319
6406
  displayedThreadId = String(threadId);
6320
6407
  const readThread = await readThreadHistory(threadId).catch(() => null);
6321
- const fallbackItems = loadCodexThreadHistoryFallback(threadId);
6408
+ const fallbackItems = await loadCodexThreadHistoryFallback(threadId);
6322
6409
  const previewThread = readThread || (fallbackItems.length ? { id: threadId, turns: [] } : null);
6323
6410
  if (previewThread) {
6324
6411
  sendToClient({
@@ -6783,7 +6870,7 @@ CRITICAL — When you need clarification during planning:
6783
6870
  }
6784
6871
  // Resolve the tab's account BEFORE first spawn so CODEX_HOME is correct.
6785
6872
  if (!child && msg.accountId) setActiveCodexAccount(msg.accountId);
6786
- await bootstrapThread(msg.threadId || null, msg.cwd || PACKAGE_ROOT);
6873
+ await bootstrapThread(msg.threadId || null, msg.cwd || PACKAGE_ROOT, { preview: msg.preview === true });
6787
6874
  return;
6788
6875
  }
6789
6876
  if (msg.type === 'permission_roots_list') {
@@ -8180,8 +8267,8 @@ function noteOpencodeServePort(port, delta) {
8180
8267
  if (next > 0) _opencodeServePortCounts.set(p, next); else _opencodeServePortCounts.delete(p);
8181
8268
  }
8182
8269
 
8183
- function trackOpencodeServe(proc, { kind, id = null, port, token, nativeWebSocketSafe = false }) {
8184
- nativeServeBoundary.track(proc, { port, safe: nativeWebSocketSafe });
8270
+ function trackOpencodeServe(proc, { kind, id = null, port, token, nativeWebSocketSafe = false, nativeServe = null }) {
8271
+ nativeServeBoundary.track(proc, { ...(nativeServe || {}), port, safe: nativeWebSocketSafe });
8185
8272
  const pid = Number(proc?.pid) || null;
8186
8273
  const exited = () => !proc || proc.exitCode != null || proc.signalCode != null;
8187
8274
  if (!exited()) {
@@ -8707,7 +8794,7 @@ async function ensureIsolatedServe(termId, { xdgRoot: requestedXdgRoot = null, c
8707
8794
  entry.proc = proc;
8708
8795
  entry.password = launch.password || null;
8709
8796
  // On record from its spawn: a crash of SynaBun from here on does not leave it running for good.
8710
- const tracked = trackOpencodeServe(proc, { kind: 'isolated', id: termId, port, token, nativeWebSocketSafe: launch.nativeWebSocketSafe });
8797
+ const tracked = trackOpencodeServe(proc, { kind: 'isolated', id: termId, port, token, nativeWebSocketSafe: launch.nativeWebSocketSafe, nativeServe: launch.nativeServe });
8711
8798
  // While it boots, its start sees an exit itself (another server on the port is a collision); afterwards
8712
8799
  // its exit ends the serve.
8713
8800
  let booting = true;
@@ -9023,6 +9110,30 @@ setInterval(() => {
9023
9110
 
9024
9111
  process.once('exit', stopAllIsolatedServes);
9025
9112
 
9113
+ // ── Antigravity side panel (/ws/antigravity) ──
9114
+ // One `agy acp` (Google's Antigravity ACP server) per panel socket, one ACP session per tab
9115
+ // (lib/antigravity/panel-bridge.js; docs/antigravity-sidepanel.md). Pasted attachments are written under the data
9116
+ // home for the agent to read; nothing reaches Antigravity's own folders.
9117
+ const antigravityPanel = createAntigravityPanelBridge({
9118
+ resolveLaunch: async () => antigravityLaunch(),
9119
+ childEnv: () => antigravityChildEnv(),
9120
+ attachmentsRoot: resolve(DATA_HOME, 'data', 'antigravity', 'attachments'),
9121
+ homesRoot: () => antigravityHomesRoot(DATA_HOME),
9122
+ isRunHomeActive: (homeDir) => (_nativeLoopRuntime?.list({ activeOnly: true }) || []).some((run) => run.provider === 'antigravity' && run.antigravityHome === homeDir && (run.status === 'starting' || run.status === 'running')),
9123
+ // Bypass keeps the browser policy: a test runner passes from inside a registered code project (lib/antigravity/bypass.js).
9124
+ policyRoots: () => { try { return browserPolicyRoots(loadHookProjects()); } catch { return []; } },
9125
+ clientInfo: { name: 'synabun', title: 'SynaBun', version: String(process.env.npm_package_version || '0') },
9126
+ // Greeting (lib/greeting/): a prompt typed in the panel is noted, and the greeting a tab opened with is told to
9127
+ // the model once, before the typed words (public/shared/agy/agy-greeting.js markers).
9128
+ onHumanPrompt: ({ cwd, words }) => { try { _greeting?.noteActivity({ cwd: cwd || PACKAGE_ROOT, surface: 'antigravity-panel', words }); } catch {} },
9129
+ greetingNote: (id, opts) => _greeting?.consumeNote(id, opts) || '',
9130
+ // `/synabun …` reads the bundled skill, as the Codex panel's does (lib/skill-prompt-bridge.js).
9131
+ expandPrompt: (text) => expandSynabunCodexSlashPrompt(text, join(SKILLS_SOURCE_DIR, 'synabun', 'SKILL.md')),
9132
+ // A turn that ended may have added to the conversation's transcript: Resume search and memory sessions follow.
9133
+ onTurnEnd: ({ sessionId }) => { try { refreshAntigravitySession(sessionId); } catch {} },
9134
+ log: (line) => console.log(line),
9135
+ });
9136
+
9026
9137
  function handleOpencodeV2Ws(ws) {
9027
9138
  _ocpV2WsClients.add(ws);
9028
9139
  ws._nativeLoopWindowId = null;
@@ -9553,7 +9664,7 @@ async function ensureOpencodeServer({ allowDuringHistoryClear = false } = {}) {
9553
9664
  });
9554
9665
  _ocpProc = proc;
9555
9666
  // On record from its spawn: a crash of SynaBun from here on does not leave it running for good.
9556
- tracked = trackOpencodeServe(proc, { kind: 'shared', id: 'shared', port: _ocpPort, token, nativeWebSocketSafe: launch.nativeWebSocketSafe });
9667
+ tracked = trackOpencodeServe(proc, { kind: 'shared', id: 'shared', port: _ocpPort, token, nativeWebSocketSafe: launch.nativeWebSocketSafe, nativeServe: launch.nativeServe });
9557
9668
  const readPassword = servePasswordReader();
9558
9669
  const outLog = createServeLogRedactor(line => console.log(`[opencode:stdout] ${line.trim()}`));
9559
9670
  const errLog = createServeLogRedactor(line => console.log(`[opencode:stderr] ${line.trim()}`));
@@ -11001,7 +11112,7 @@ const _codexSettingsProbes = createLaunchCache({
11001
11112
  });
11002
11113
  setCodexSettingsStore({ get: (key) => _codexSettingsProbes.peek(key), set: (key, value) => _codexSettingsProbes.set(key, value) });
11003
11114
 
11004
- async function getClaudeModelsForClient(force = false) {
11115
+ async function getClaudeModelsForClient(force = false, { nonBlocking = false } = {}) {
11005
11116
  try {
11006
11117
  const bin = getClaudeBin();
11007
11118
  // Sidepanel sessions and native loops run this same installation. The Agent
@@ -11010,8 +11121,9 @@ async function getClaudeModelsForClient(force = false) {
11010
11121
  // when the CLI changes).
11011
11122
  checkClaudeCliSkew(bin).catch(() => {});
11012
11123
  // Answered from the kept list at once (asked again in the background when it is old);
11013
- // only a first read, another binary or account, or ?refresh=1 waits for the CLI.
11014
- const result = await discoverClaudeModels(bin, { force, cwd: PACKAGE_ROOT, account: claudeModelAccountKey() });
11124
+ // only a first read, another binary or account, or ?refresh=1 waits for the CLI, unless
11125
+ // nonBlocking=true (e.g. cold /api/claude/config boot on Windows) warms it asynchronously.
11126
+ const result = await discoverClaudeModels(bin, { force, nonBlocking, cwd: PACKAGE_ROOT, account: claudeModelAccountKey() });
11015
11127
  if (result?.models?.length) return result;
11016
11128
  } catch (err) {
11017
11129
  console.warn('[claude-models] discovery failed:', err.message);
@@ -11039,7 +11151,11 @@ app.get('/api/claude/config', async (req, res) => {
11039
11151
  // Model ids come straight from the CLI and are already spawn-ready (they carry
11040
11152
  // the `[1m]` suffix where applicable), so the client uses them verbatim — no
11041
11153
  // "<id>:<contextWindow>" composite. See toCliModelName() for the legacy form.
11042
- const { source, models } = await getClaudeModelsForClient();
11154
+ // Default to non-blocking model discovery so cold panel opens on Windows never stall
11155
+ // behind a 3–7s `claude.exe` spawn; ui-claude-panel.js refreshes fallback models async.
11156
+ const { source, models } = await getClaudeModelsForClient(false, {
11157
+ nonBlocking: req.query?.waitModels !== '1',
11158
+ });
11043
11159
  res.json({ ok: true, config: cliCfg, projects, models, modelsSource: source, bootId: SERVER_BOOT_ID });
11044
11160
  } catch (err) {
11045
11161
  res.status(500).json({ error: err.message });
@@ -14372,9 +14488,15 @@ function isSidepanelClaimTokenRevoked(windowId, claimToken) {
14372
14488
 
14373
14489
  // Native provider run ledger. A sidepanel can attach after a page refresh or
14374
14490
  // after a schedule started while no Neural Interface window was open.
14491
+ function withSidepanelRunHome(run) {
14492
+ if (!run || run.provider !== 'antigravity' || run.antigravityHome || !run.runId) return run;
14493
+ const home = _assistantDispatcher?.get?.(run.runId)?.antigravityHome || null;
14494
+ return home ? { ...run, antigravityHome: home } : run;
14495
+ }
14496
+
14375
14497
  app.get('/api/sidepanel-runs', (req, res) => {
14376
14498
  const activeOnly = req.query.active === '1' || req.query.active === 'true';
14377
- res.json({ ok: true, runs: _nativeLoopRuntime?.list({ activeOnly }) || [] });
14499
+ res.json({ ok: true, runs: (_nativeLoopRuntime?.list({ activeOnly }) || []).map(withSidepanelRunHome) });
14378
14500
  });
14379
14501
 
14380
14502
  app.post('/api/sidepanel-runs/release-window', (req, res) => {
@@ -14386,7 +14508,7 @@ app.post('/api/sidepanel-runs/release-window', (req, res) => {
14386
14508
  app.get('/api/sidepanel-runs/:runId', (req, res) => {
14387
14509
  const run = _nativeLoopRuntime?.get(req.params.runId);
14388
14510
  if (!run) return res.status(404).json({ error: 'Native sidepanel run not found' });
14389
- res.json({ ok: true, run });
14511
+ res.json({ ok: true, run: withSidepanelRunHome(run) });
14390
14512
  });
14391
14513
 
14392
14514
  app.post('/api/sidepanel-runs/:runId/claim', (req, res) => {
@@ -14395,7 +14517,7 @@ app.post('/api/sidepanel-runs/:runId/claim', (req, res) => {
14395
14517
  }
14396
14518
  const result = _nativeLoopRuntime?.claim(req.params.runId, req.body?.windowId);
14397
14519
  if (!result?.ok) return res.status(result?.reason === 'not_found' ? 404 : 409).json(result || { ok: false });
14398
- res.json(result);
14520
+ res.json(result.run ? { ...result, run: withSidepanelRunHome(result.run) } : result);
14399
14521
  });
14400
14522
 
14401
14523
  app.post('/api/sidepanel-runs/:runId/release', (req, res) => {
@@ -14493,7 +14615,7 @@ app.post('/api/loop/launch', async (req, res) => {
14493
14615
  const acq = await acquireLoopBrowserAndTab({ requestedBrowserSessionId, terminalSessionId });
14494
14616
  if (acq.error) {
14495
14617
  loopLog(terminalSessionId, 'launch:browser', 'ABORT: browser acquisition failed — returning 500', { error: acq.error });
14496
- return res.status(500).json({ error: acq.error });
14618
+ return res.status(500).json({ error: acq.error, ...(acq.code ? { code: acq.code, offenders: acq.offenders } : {}) });
14497
14619
  }
14498
14620
  browserSessionId = acq.browserSessionId;
14499
14621
  browserTabId = acq.browserTabId;
@@ -14533,7 +14655,7 @@ app.post('/api/loop/launch', async (req, res) => {
14533
14655
  // .claude/ set awaitingNext and attachLoopDriver() drives /clear + re-prompt; if those
14534
14656
  // hooks are absent (e.g. a claude-code task run under a hookless harness) the loop STALLS.
14535
14657
  // 'exec': harness-agnostic — attachExecLoopDriver() owns iteration via sentinel echoes, so
14536
- // OpenCode/Codex/Gemini self-advance. Facebook seeding must stay profile:opencode for this.
14658
+ // OpenCode/Codex/Antigravity self-advance. Facebook seeding must stay profile:opencode for this.
14537
14659
  driverType: nativeRuntime ? 'native' : (cliProfile === 'claude-code' ? 'hook' : 'exec'),
14538
14660
  runtimeType: nativeRuntime ? 'native' : 'pty',
14539
14661
  surface: nativeRuntime ? 'sidepanel' : 'terminal',
@@ -14652,12 +14774,12 @@ app.post('/api/loop/launch', async (req, res) => {
14652
14774
  loopLog(terminalSessionId, 'launch:driver', 'attaching hook-driven loop driver (claude-code)');
14653
14775
  attachLoopDriver(terminalSessionId);
14654
14776
  } else {
14655
- // Codex/Gemini/other: spawn shell, server drives `<cli> exec` commands per iteration.
14777
+ // Codex/Antigravity/other: spawn shell, server drives `<cli> exec` commands per iteration.
14656
14778
  // The driver invokes a platform-native wrapper that captures the CLI
14657
14779
  // status and emits a sentinel. Immune to PS1/PROMPT overrides.
14658
14780
  loopLog(terminalSessionId, 'launch:spawn', 'creating shell PTY for exec-driven CLI', { profile: cliProfile, cwd: loopCwd });
14659
14781
  createTerminalSession('shell', 120, 30, loopCwd, { extraEnv: loopExtraEnv, sessionId: terminalSessionId });
14660
- loopLog(terminalSessionId, 'launch:driver', 'attaching exec loop driver (codex/gemini/other)', { profile: cliProfile });
14782
+ loopLog(terminalSessionId, 'launch:driver', 'attaching exec loop driver (codex/antigravity/other)', { profile: cliProfile });
14661
14783
  attachExecLoopDriver(terminalSessionId);
14662
14784
  }
14663
14785
 
@@ -14774,7 +14896,7 @@ app.post('/api/loop/stop', async (req, res) => {
14774
14896
  });
14775
14897
 
14776
14898
  // POST /api/loop/resolve-from-ancestors — process-tree fallback for MCP env loss.
14777
- // The MCP server walks its ancestor PID chain (codex/opencode/gemini → shell PTY)
14899
+ // The MCP server walks its ancestor PID chain (codex/opencode/antigravity → shell PTY)
14778
14900
  // and POSTs the chain here. We match any PID against terminalSessions[*].pty.pid
14779
14901
  // and return that loop's browserSessionId/browserTabId. Layer B of the
14780
14902
  // defense-in-depth browser tab isolation fix.
@@ -14819,7 +14941,7 @@ app.post('/api/loop/recover-browser', async (req, res) => {
14819
14941
  const terminalSessionId = req.body?.terminalSessionId || randomBytes(16).toString('hex');
14820
14942
  const acq = await acquireLoopBrowserAndTab({ terminalSessionId, logTag: 'recover:browser' });
14821
14943
  if (acq.error) {
14822
- return res.status(500).json({ error: acq.error });
14944
+ return res.status(500).json({ error: acq.error, ...(acq.code ? { code: acq.code, offenders: acq.offenders } : {}) });
14823
14945
  }
14824
14946
  // Persist the new pins onto the loop's state file when we can identify it,
14825
14947
  // so resolve-from-ancestors and loop/stop operate on the live session/tab.
@@ -15570,7 +15692,7 @@ async function launchScheduledLoop(schedule) {
15570
15692
  }
15571
15693
 
15572
15694
  // Native providers run headlessly in their durable provider conversation.
15573
- // Gemini and unsupported profiles retain the existing PTY drivers.
15695
+ // Unsupported profiles retain the existing PTY drivers.
15574
15696
  let runDescriptor = null;
15575
15697
  if (nativeRuntime) {
15576
15698
  const presentation = resolveScheduleRunPresentation(
@@ -16818,7 +16940,7 @@ function spawnAgentProcess(agent, prompt, extraEnv = {}) {
16818
16940
  const sessionId = agent._activeSessionId || agent.sessionId;
16819
16941
 
16820
16942
  const serverMap = JSON.parse(agent._mcpConfig).mcpServers;
16821
- const mcpValues = Object.values(serverMap).flatMap(entry => [...Object.values(entry.env || {}), ...Object.values(entry.headers || {}), ...(entry.args || []), ...(entry.url ? [entry.url] : [])]).filter(value => typeof value === 'string' && value);
16943
+ const mcpValues = mcpSecretValues(serverMap, { env: process.env });
16822
16944
  const mcpConfigPath = writePrivateMcpConfig(resolve(DATA_HOME, 'data', 'agents', agent.id, 'mcp.json'), serverMap);
16823
16945
  const args = [
16824
16946
  '--print',
@@ -17299,7 +17421,7 @@ app.post('/api/agents/launch', async (req, res) => {
17299
17421
  const acq = await acquireSharedBrowserTab({ ownerKey: `agent:${agentId}`, ownerType: 'interactive', logTag: 'agent:browser' });
17300
17422
  if (acq.error) {
17301
17423
  console.warn(`[agent] Browser tab acquire failed: ${acq.error}`);
17302
- return res.status(500).json({ error: `Could not start browser session for this automation: ${acq.error}` });
17424
+ return res.status(500).json({ error: `Could not start browser session for this automation: ${acq.error}`, ...(acq.code ? { code: acq.code, offenders: acq.offenders } : {}) });
17303
17425
  }
17304
17426
  browserSessionId = acq.browserSessionId;
17305
17427
  browserTabId = acq.browserTabId;
@@ -17499,7 +17621,7 @@ function isSynabunGitHubRemote(remoteText) {
17499
17621
  return /github\.com[:/]danilokhury\/Synabun(?:\.git)?(?:[\s)]|$)/i.test(String(remoteText || ''));
17500
17622
  }
17501
17623
 
17502
- function detectSynabunInstallSource() {
17624
+ async function detectSynabunInstallSource() {
17503
17625
  if (PACKAGED_RUNTIME) return { kind: 'packaged-app' };
17504
17626
  const normalizedRoot = PACKAGE_ROOT.replace(/\\/g, '/');
17505
17627
  if (/\/node_modules\/synabun$/i.test(normalizedRoot)) {
@@ -17509,13 +17631,13 @@ function detectSynabunInstallSource() {
17509
17631
  try {
17510
17632
  // Run as reads (lib/git-read.js): the app's own checkout is a folder like any other, and can be a registered
17511
17633
  // project. What git could not say is an empty answer.
17512
- const git = (args) => {
17513
- const result = spawnGitRead(args, { cwd: PACKAGE_ROOT, timeout: 2000 });
17634
+ const git = async (args) => {
17635
+ const result = await spawnGitRead(args, { cwd: PACKAGE_ROOT, timeout: 2000 });
17514
17636
  return result.status === 0 ? result.stdout.toString().trim() : '';
17515
17637
  };
17516
- if (git(['rev-parse', '--is-inside-work-tree']) === 'true') {
17517
- const remote = git(['remote', 'get-url', 'origin']);
17518
- const remotes = git(['remote', '-v']);
17638
+ if (await git(['rev-parse', '--is-inside-work-tree']) === 'true') {
17639
+ const remote = await git(['remote', 'get-url', 'origin']);
17640
+ const remotes = await git(['remote', '-v']);
17519
17641
  if (isSynabunGitHubRemote(`${remote}\n${remotes}`)) {
17520
17642
  const firstRemote = remote || remotes.split(/\s+/).find(part => isSynabunGitHubRemote(part)) || null;
17521
17643
  return { kind: 'github-clone', remote: firstRemote };
@@ -17600,7 +17722,7 @@ async function checkSynabunUpdate() {
17600
17722
  const installPlan = buildSynabunInstallPlan({
17601
17723
  ...found,
17602
17724
  current,
17603
- installSource: updateAvailable ? detectSynabunInstallSource() : null,
17725
+ installSource: updateAvailable ? await detectSynabunInstallSource() : null,
17604
17726
  repoUrl: SYNABUN_GITHUB_REPO_URL,
17605
17727
  });
17606
17728
 
@@ -17742,6 +17864,8 @@ function systemActivitySources() {
17742
17864
  const blockers = getOpenCodeHistoryBlockers();
17743
17865
  return blockers.activeTurns + blockers.startingTurns;
17744
17866
  },
17867
+ // A prompt running in an Antigravity panel tab (lib/antigravity/panel-bridge.js); dispatched runs are under assistant.
17868
+ 'antigravity-panel': () => antigravityPanel.turnsRunning(),
17745
17869
  'claude-cli': () => recentCliSessions(sessionRegistry),
17746
17870
  loop: () => {
17747
17871
  // A dispatched worker also runs in the native runtime: it is counted under assistant.
@@ -17770,7 +17894,9 @@ app.get('/api/system/activity', (req, res) => {
17770
17894
  const TOOL_DEFS = [
17771
17895
  { key: 'claude-code', cmd: 'claude', versionArg: '--version', npm: '@anthropic-ai/claude-code' },
17772
17896
  { key: 'codex', cmd: 'codex', versionArg: '--version', npm: '@openai/codex' },
17773
- { key: 'gemini', cmd: 'gemini', versionArg: '--version', npm: '@google/gemini-cli' },
17897
+ // Google's Antigravity CLI has no npm package: a Homebrew cask's newer version is what `brew outdated` says, and
17898
+ // the CLI updates itself (`agy update`).
17899
+ { key: 'antigravity', cmd: 'agy', versionArg: '--version' },
17774
17900
  // npm's opencode-ai is OpenCode 1: OpenCode 2's newer version is what Homebrew says (a brew install) or what OpenCode
17775
17901
  // itself announces in its own interface.
17776
17902
  { key: 'opencode', cmd: 'opencode', versionArg: '--version' },
@@ -17970,7 +18096,11 @@ async function checkToolVersions({ force = false } = {}) {
17970
18096
  || 'unknown';
17971
18097
  for (const [key, info] of Object.entries(results)) {
17972
18098
  const source = info.installed ? info.installSource : dominantSource;
17973
- info.installCommand = buildInstallCommand(key, source, { platform: process.platform });
18099
+ // The Antigravity CLI: the command of the install its button runs here (Homebrew's cask on a Mac that has
18100
+ // Homebrew, else Google's installer), so the banner never shows one command and runs another.
18101
+ info.installCommand = key === 'antigravity' && info.install?.available
18102
+ ? cliInstaller.manualCommand(key)
18103
+ : buildInstallCommand(key, source, { platform: process.platform });
17974
18104
  }
17975
18105
 
17976
18106
  // A check started before a newer refresh must never publish its stale answer.
@@ -18039,7 +18169,7 @@ app.use('/api/cli/path', createCliPathApi({
18039
18169
  pathManager: cliPathManager, resolver: cliResolver, isGuestRequest, port: () => PORT,
18040
18170
  diagnostics: async () => {
18041
18171
  const fresh = await freshTerminalPath();
18042
- const statuses = await cliResolver.resolveAll(['claude-code', 'codex', 'opencode']);
18172
+ const statuses = await cliResolver.resolveAll(['claude-code', 'codex', 'opencode', 'antigravity']);
18043
18173
  return { platform: process.platform, pathKey: Object.keys(fresh.env).find(key => key.toUpperCase() === 'PATH'),
18044
18174
  entries: fresh.entries, os: fresh.os, pathext: fresh.env.PATHEXT || fresh.env.PathExt || null,
18045
18175
  tools: Object.fromEntries(Object.entries(statuses).map(([tool, status]) => [tool, { path: status.path, launchPath: status.launchPath, ...cliPathManager.inspect(status) }])) };
@@ -18544,7 +18674,7 @@ const editorCompletionServe = createOpencodeCompletionEngine({
18544
18674
  const token = opencodeServeRecords.newToken();
18545
18675
  const launch = opencodeServeLaunch(port, { env: { ...env, [SERVE_TOKEN_ENV]: token }, passwordFrom: 'stdout' });
18546
18676
  const proc = spawn(launch.command, launch.args, { stdio: ['ignore', 'pipe', 'pipe'], ...launch.options, cwd, detached: false, shell: false, windowsHide: true });
18547
- editorCompletionServeTracks.set(proc, trackOpencodeServe(proc, { kind: 'completion', id: 'editor-complete', port, token, nativeWebSocketSafe: launch.nativeWebSocketSafe }));
18677
+ editorCompletionServeTracks.set(proc, trackOpencodeServe(proc, { kind: 'completion', id: 'editor-complete', port, token, nativeWebSocketSafe: launch.nativeWebSocketSafe, nativeServe: launch.nativeServe }));
18548
18678
  const outLog = createServeLogRedactor(line => console.log(`[editor-complete:out] ${line.trim().slice(0, 500)}`));
18549
18679
  const errLog = createServeLogRedactor(line => console.log(`[editor-complete:err] ${line.trim().slice(0, 500)}`));
18550
18680
  proc.stdout?.on('data', d => outLog.push(d));
@@ -18753,7 +18883,7 @@ app.get('/api/project-files', editorOwnerGuard, async (req, res) => {
18753
18883
  }
18754
18884
 
18755
18885
  // Git info for status badges
18756
- const git = getGitInfo(normalizedDir);
18886
+ const git = await getGitInfo(normalizedDir, { signal: gitRequestSignal(res) });
18757
18887
 
18758
18888
  if (recursive || (search && search.length > 2)) {
18759
18889
  // Recursive search mode
@@ -19171,8 +19301,9 @@ app.put('/api/morelogin/config', (req, res) => {
19171
19301
  app.post('/api/morelogin/test', async (req, res) => {
19172
19302
  try {
19173
19303
  const status = await moreLogin.getMoreLoginStatus(_moreLoginOpts());
19174
- if (!status.installed) return res.json({ ok: false, error: 'MoreLogin desktop app is not installed.' });
19304
+ if (!status.installed) return res.json({ ok: false, error: `MoreLogin desktop app was not found and nothing answers on port ${status.port}.` });
19175
19305
  if (!status.running) return res.json({ ok: false, error: `MoreLogin API not reachable on port ${status.port}. Open MoreLogin and enable its local API (Settings → API & MCP).` });
19306
+ if (status.apiError) return res.json({ ok: false, error: status.apiError });
19176
19307
  res.json({ ok: true, port: status.port, profileCount: status.profileCount });
19177
19308
  } catch (err) {
19178
19309
  res.json({ ok: false, error: err.message });
@@ -19268,6 +19399,25 @@ function _normalizeMcpEntry(input) {
19268
19399
  return { command, args, env, url, type };
19269
19400
  }
19270
19401
 
19402
+ // ── Antigravity's MCP configuration (~/.gemini/config/mcp_config.json) ──
19403
+ // The one file `agy mcp add` writes and both the Antigravity CLI and app read: { mcpServers: { name: { command, args,
19404
+ // env, disabled } | { serverUrl, headers, disabled } } }. Antigravity creates it empty; an empty file reads as {}.
19405
+ function antigravityConfigDir() { return join(getHomePath(), '.gemini', 'config'); }
19406
+ function antigravityMcpConfigPath() { return join(antigravityConfigDir(), 'mcp_config.json'); }
19407
+ /** The parsed file ({} when missing or empty); throws on a file that is not a JSON object, so nothing overwrites it. */
19408
+ function readAntigravityMcpConfig() {
19409
+ const path = antigravityMcpConfigPath();
19410
+ if (!existsSync(path)) return {};
19411
+ return parseAntigravityMcpConfig(readFileSync(path, 'utf8'));
19412
+ }
19413
+ function writeAntigravityMcpConfig(data) {
19414
+ const path = antigravityMcpConfigPath();
19415
+ mkdirSync(dirname(path), { recursive: true });
19416
+ const tmp = `${path}.synabun-${process.pid}.tmp`;
19417
+ writeFileSync(tmp, JSON.stringify(data, null, 2) + '\n', { encoding: 'utf8', mode: 0o600 });
19418
+ renameSync(tmp, path);
19419
+ }
19420
+
19271
19421
  function _writeMoreLoginMcpToAgents(entry) {
19272
19422
  const targets = [];
19273
19423
  const home = getHomePath();
@@ -19283,18 +19433,16 @@ function _writeMoreLoginMcpToAgents(entry) {
19283
19433
  targets.push('Claude Code');
19284
19434
  }
19285
19435
  } catch (e) { console.warn('[morelogin] Claude register failed:', e.message); }
19286
- try { // Gemini (~/.gemini/settings.json)
19287
- const p = join(home, '.gemini', 'settings.json');
19288
- if (existsSync(p)) {
19289
- const data = JSON.parse(readFileSync(p, 'utf8') || '{}');
19290
- if (!data.mcpServers) data.mcpServers = {};
19291
- data.mcpServers[MORELOGIN_MCP_KEY] = entry.url
19292
- ? { url: entry.url, trust: true }
19293
- : { command: entry.command, args: entry.args || [], ...(entry.env ? { env: entry.env } : {}), trust: true };
19294
- writeFileSync(p, JSON.stringify(data, null, 2) + '\n', 'utf8');
19295
- targets.push('Gemini');
19436
+ try { // Antigravity (~/.gemini/config/mcp_config.json, the CLI's and the app's): only where Antigravity has run
19437
+ if (existsSync(antigravityConfigDir())) {
19438
+ const data = readAntigravityMcpConfig();
19439
+ data.mcpServers = { ...(data.mcpServers || {}), [MORELOGIN_MCP_KEY]: buildAntigravityMcpEntry(entry.url
19440
+ ? { type: entry.type || 'http', url: entry.url }
19441
+ : { command: entry.command, args: entry.args || [], env: entry.env }) };
19442
+ writeAntigravityMcpConfig(data);
19443
+ targets.push('Antigravity');
19296
19444
  }
19297
- } catch (e) { console.warn('[morelogin] Gemini register failed:', e.message); }
19445
+ } catch (e) { console.warn('[morelogin] Antigravity register failed:', e.message); }
19298
19446
  try { // Codex (~/.codex/config.toml) — stdio only
19299
19447
  const p = join(home, '.codex', 'config.toml');
19300
19448
  if (existsSync(p) && entry.command) {
@@ -19332,13 +19480,11 @@ function _removeMoreLoginMcpFromAgents() {
19332
19480
  }
19333
19481
  } catch {}
19334
19482
  try {
19335
- const p = join(home, '.gemini', 'settings.json');
19336
- if (existsSync(p)) {
19337
- const data = JSON.parse(readFileSync(p, 'utf8') || '{}');
19483
+ if (existsSync(antigravityMcpConfigPath())) {
19484
+ const data = readAntigravityMcpConfig();
19338
19485
  if (data.mcpServers && data.mcpServers[MORELOGIN_MCP_KEY]) {
19339
- delete data.mcpServers[MORELOGIN_MCP_KEY];
19340
- writeFileSync(p, JSON.stringify(data, null, 2) + '\n', 'utf8');
19341
- targets.push('Gemini');
19486
+ writeAntigravityMcpConfig(removeAntigravityMcpServer(data, MORELOGIN_MCP_KEY));
19487
+ targets.push('Antigravity');
19342
19488
  }
19343
19489
  }
19344
19490
  } catch {}
@@ -21164,10 +21310,106 @@ app.get('/api/opencode/sessions', (req, res) => {
21164
21310
  }
21165
21311
  });
21166
21312
 
21167
- // GET /api/gemini/sessions — stub (Gemini CLI has no known session storage)
21168
- app.get('/api/gemini/sessions', (req, res) => {
21169
- const projects = loadHookProjects();
21170
- res.json({ projects: projects.map(p => ({ path: p.path, label: p.label || basename(p.path), total: 0, sessions: [] })) });
21313
+ // GET /api/antigravity/sessions — Antigravity's own conversations (ACP session/list on a short-lived `agy acp`),
21314
+ // grouped under the registered projects. A missing or signed-out CLI lists nothing and says why.
21315
+ let _antigravitySessionsCache = null; // { at, value }
21316
+ app.get('/api/antigravity/sessions', async (req, res) => {
21317
+ try {
21318
+ const projects = loadHookProjects();
21319
+ const limit = Math.min(parseInt(req.query.limit) || 30, 100);
21320
+ const offset = parseInt(req.query.offset) || 0;
21321
+ const search = (req.query.search || '').toString();
21322
+ const fresh = _antigravitySessionsCache && Date.now() - _antigravitySessionsCache.at < 15_000 && req.query.refresh !== '1';
21323
+ const listed = fresh ? _antigravitySessionsCache.value : await listAntigravitySessions({ launch: antigravityLaunch(), env: antigravityChildEnv() });
21324
+ if (!fresh && !listed.error) _antigravitySessionsCache = { at: Date.now(), value: listed };
21325
+ res.json({
21326
+ projects: groupSessionsByProject(listed.sessions, projects, { targetProject: req.query.project || null, search, offset, limit }),
21327
+ ...(listed.error ? { error: listed.error } : {}),
21328
+ ...(listed.authRequired ? { authRequired: true } : {}),
21329
+ ...(listed.cliMissing ? { cliMissing: true } : {}),
21330
+ });
21331
+ } catch (err) {
21332
+ res.status(500).json({ error: err.message });
21333
+ }
21334
+ });
21335
+
21336
+ // GET /api/antigravity/models — what an ACP session offers SynaBun (lib/antigravity/catalog.js: a short-lived `agy acp`
21337
+ // in a throwaway home; `agy models` only when ACP fails), kept for a minute.
21338
+ // { models: [{ id, label, efforts?, default? }], source: 'live' | 'stale' | 'unavailable', via, signedIn?, tier?, error? }
21339
+ let _antigravityModelsCache = null; // { at, value }
21340
+ app.get('/api/antigravity/models', async (req, res) => {
21341
+ try {
21342
+ const age = _antigravityModelsCache ? Date.now() - _antigravityModelsCache.at : Infinity;
21343
+ if (age < 60_000 && req.query.refresh !== '1') return res.json(_antigravityModelsCache.value);
21344
+ const value = await listAntigravityModels({ launch: antigravityLaunch(), env: antigravityChildEnv() });
21345
+ if (value.source === 'live') {
21346
+ // A different list than the one the Assistant's catalog last read: its rows are rebuilt (as for Claude's).
21347
+ const ids = (rows) => (rows || []).map((row) => `${row.id}${row.default ? '*' : ''}`).join('\n');
21348
+ const changed = _antigravityModelsCache && ids(_antigravityModelsCache.value.models) !== ids(value.models);
21349
+ _antigravityModelsCache = { at: Date.now(), value };
21350
+ if (changed) _assistantCatalog?.refresh?.().catch(() => {});
21351
+ }
21352
+ else if (_antigravityModelsCache?.value?.models?.length) return res.json({ ..._antigravityModelsCache.value, source: 'stale', error: value.error });
21353
+ res.json(value);
21354
+ } catch (err) {
21355
+ res.status(500).json({ models: [], source: 'unavailable', error: err.message });
21356
+ }
21357
+ });
21358
+
21359
+ // POST /api/antigravity/policy — the question the PreToolUse hook of a SynaBun Antigravity home asks before every tool
21360
+ // call of a brain or run SynaBun started (lib/antigravity/policy-hook.mjs → lib/antigravity/policy.js). The home's
21361
+ // token names who registered the checker; an unknown token is refused. Loopback socket only (requireLocal), and only
21362
+ // { token, payload }: the answer is { decision: 'deny' | 'ask' | 'allow', reason? }.
21363
+ app.post('/api/antigravity/policy', requireLoopbackSocket, express.json({ limit: '4mb' }), async (req, res) => {
21364
+ const body = req.body && typeof req.body === 'object' && !Array.isArray(req.body) ? req.body : {};
21365
+ try {
21366
+ res.json(await defaultAntigravityPolicy.decide(body.token, body.payload && typeof body.payload === 'object' ? body.payload : {}));
21367
+ } catch (err) {
21368
+ res.json({ decision: 'deny', reason: `SynaBun could not check this call (${err?.message || err}).` });
21369
+ }
21370
+ });
21371
+ /** Where the hook of a SynaBun Antigravity home reaches this server (loopback, the port this server listens on). */
21372
+ function antigravityPolicyUrl() { return `http://127.0.0.1:${PORT}/api/antigravity/policy`; }
21373
+ /** The home of an Antigravity brain or run (lib/antigravity/home.js), under the data home's runtime folder. */
21374
+ function antigravityHomeFor(kind, id) { return antigravityHomeDir(antigravityHomesRoot(DATA_HOME), kind, id); }
21375
+
21376
+ // Antigravity's conversations as SynaBun keeps them (lib/antigravity/transcripts.js, read only): the user's own go
21377
+ // into Resume's index (session_cache + session_fts, provider antigravity), every one, SynaBun's brain and run homes
21378
+ // included, into memory capture (lib/memory-session-capture.js). A pass at start, one after a panel turn ends (that
21379
+ // conversation only, a moment later so agy has written its last step), one every five minutes, and a fresh one
21380
+ // before Resume or Skills Studio searches them (at most every 15 s).
21381
+ function antigravitySessionStores({ userOnly = false } = {}) {
21382
+ return antigravityStores({ homesRoot: userOnly ? null : antigravityHomesRoot(DATA_HOME) });
21383
+ }
21384
+ let _antigravityCacheAt = 0;
21385
+ function refreshAntigravityCatalog({ force = false } = {}) {
21386
+ if (!force && Date.now() - _antigravityCacheAt < 15_000) return null;
21387
+ _antigravityCacheAt = Date.now();
21388
+ try { return rebuildAntigravityCache({ stores: antigravitySessionStores() }); }
21389
+ catch (err) { console.warn(' Session cache (antigravity) error:', err.message); return null; }
21390
+ }
21391
+ const _antigravityRefreshTimers = new Map();
21392
+ function refreshAntigravitySession(sessionId) {
21393
+ if (typeof sessionId !== 'string' || !sessionId) return;
21394
+ clearTimeout(_antigravityRefreshTimers.get(sessionId));
21395
+ _antigravityRefreshTimers.set(sessionId, setTimeout(() => {
21396
+ _antigravityRefreshTimers.delete(sessionId);
21397
+ try { rebuildAntigravityCache({ stores: antigravitySessionStores({ userOnly: true }), only: sessionId }); }
21398
+ catch (err) { console.warn(' Session cache (antigravity) error:', err.message); }
21399
+ }, 1500));
21400
+ _antigravityRefreshTimers.get(sessionId).unref?.();
21401
+ }
21402
+ /** SynaBun's own MCP entry for an Antigravity home: the registration the Setup card writes (stdio, this install). */
21403
+ function antigravitySynabunMcpEntry() {
21404
+ const { envPath, mcpCommand, mcpArgs } = getMcpPaths();
21405
+ return { command: mcpCommand, args: mcpArgs, env: { DOTENV_PATH: envPath, SYNABUN_DATA_HOME: DATA_HOME, MEMORY_DATA_DIR: resolve(DATA_HOME, 'mcp-data') } };
21406
+ }
21407
+
21408
+ // GET /api/antigravity/panel — what the side panel's socket offers on this server (lib/antigravity/panel-bridge.js).
21409
+ // Admin-only like the socket itself, so the page tells a refused socket (403 here too: a guest, the automation
21410
+ // browser) from a server that predates it (404: it needs a restart) instead of reconnecting forever.
21411
+ app.get('/api/antigravity/panel', (req, res) => {
21412
+ res.json({ ok: true, socket: ANTIGRAVITY_WS_PATH, capabilities: [...ANTIGRAVITY_PANEL_CAPABILITIES], live: antigravityPanel.liveCount() });
21171
21413
  });
21172
21414
 
21173
21415
  // ── Hybrid session content search (FTS5 + semantic re-rank for Claude) ──
@@ -21204,6 +21446,9 @@ app.get('/api/session-search', async (req, res) => {
21204
21446
  codexCatalogById = new Map(codexCatalog.map((session) => [session.sessionId, session]));
21205
21447
  }
21206
21448
 
21449
+ // Antigravity's index follows its transcripts (refreshAntigravityCatalog: at most every 15 s).
21450
+ if (provider === 'antigravity') refreshAntigravityCatalog();
21451
+
21207
21452
  // 1. FTS5 substring hits
21208
21453
  const ftsRows = searchSessionFts(q, { provider, project: projectFilter, limit: limit * 2 });
21209
21454
  const ftsMap = new Map();
@@ -21468,13 +21713,21 @@ app.post('/api/claude-code/sessions/:sessionId/fork', express.json(), claudeSess
21468
21713
  app.delete('/api/claude-code/sessions/:sessionId', express.json(), claudeSessionRoute((req) => {
21469
21714
  // Irreversible: the panel asks first, and says so here.
21470
21715
  if (req.body?.confirm !== true) { const e = new Error('Deleting a session needs confirm: true'); e.status = 400; throw e; }
21471
- return claudeSessionOps.remove(req.params.sessionId, req.body?.project);
21716
+ // The transcript cache keeps parsed rows by path: a deleted session's go with it (also when the delete fails half way).
21717
+ return Promise.resolve()
21718
+ .then(() => claudeSessionOps.remove(req.params.sessionId, req.body?.project))
21719
+ .finally(() => { try { claudeHistoryCache.forgetSession(req.params.sessionId); } catch {} });
21472
21720
  }));
21473
21721
  app.get('/api/claude-code/sessions/:sessionId/subagents', claudeSessionRoute(req => claudeSessionOps.subagents(req.params.sessionId, req.query.project)));
21474
21722
  app.get('/api/claude-code/sessions/:sessionId/subagents/:agentId/messages', claudeSessionRoute(req => claudeSessionOps.subagentMessages(req.params.sessionId, req.params.agentId, req.query.project)));
21475
21723
 
21476
21724
  // --- Session Messages (read JSONL for panel history) ---
21477
21725
 
21726
+ // The parsed transcripts of the last few sessions opened (lib/claude-history-cache.js).
21727
+ const claudeHistoryCache = createHistoryCache();
21728
+ // The Codex rollouts read last, parsed once for the early preview and the history fallback (lib/codex-rollout-reader.js).
21729
+ const codexRolloutReader = createRolloutReader();
21730
+
21478
21731
  app.get('/api/claude-code/sessions/:sessionId/messages', async (req, res) => {
21479
21732
  try {
21480
21733
  const { sessionId } = req.params;
@@ -21514,22 +21767,17 @@ app.get('/api/claude-code/sessions/:sessionId/messages', async (req, res) => {
21514
21767
  return res.status(missing.status).json(missing.body);
21515
21768
  }
21516
21769
 
21517
- // Stream line-by-line — session JSONLs reach tens of MB and this endpoint
21518
- // fires on every session open; a whole-file readFileSync stalled the loop.
21519
- // Split on "\n" only: readline also breaks at U+2028 / U+2029, which are legal
21520
- // inside a JSON string, and the halves of such a line were silently dropped.
21521
- // Read from the canonical path that was checked, opened without following a link.
21522
- const rl = jsonlLines(openConfinedStream(found.realFolder, found.realPath, { encoding: 'utf-8' }));
21770
+ // Session JSONLs reach tens of MB and one session open asks this route several times (the panel's
21771
+ // snapshot probe, the rebuild, "load earlier", a clipped result): the collector of a transcript is
21772
+ // kept, and a file that grew is parsed only from where the last read stopped (lib/claude-history-cache.js:
21773
+ // whole lines split on "\n" only, read from the canonical path that was checked, opened without following
21774
+ // a link).
21523
21775
  // Rows per transcript line: prompts (with their uuid), assistant text, thinking
21524
21776
  // and tool calls, tool results with their typed output (they live inside user
21525
21777
  // lines), compaction markers. The collector also follows the parentUuid chain,
21526
21778
  // so the branches a conversation rewind abandoned are left out: what is shown
21527
21779
  // is what the resumed model has. See lib/claude-history.js.
21528
- const collector = createHistoryCollector();
21529
- for await (const line of rl) {
21530
- if (!line.trim()) continue;
21531
- try { collector.add(JSON.parse(line)); } catch {}
21532
- }
21780
+ const collector = await claudeHistoryCache.collectorFor(found);
21533
21781
 
21534
21782
  // `?tool=<tool_use_id>`: just that call's result (a restored snapshot holds
21535
21783
  // only the clipped text of a long result and asks for the rest).
@@ -21747,7 +21995,7 @@ app.get('/api/claude-code/integrations', (req, res) => {
21747
21995
 
21748
21996
  // POST /api/claude-code/integrations — enable hook for a target
21749
21997
  // Optional `hook` param: 'SessionStart' | 'UserPromptSubmit' (omit for both)
21750
- app.post('/api/claude-code/integrations', (req, res) => {
21998
+ app.post('/api/claude-code/integrations', async (req, res) => {
21751
21999
  try {
21752
22000
  const { target, projectPath, label, hook } = req.body;
21753
22001
  // target: 'global' | 'project'
@@ -21817,7 +22065,7 @@ app.post('/api/claude-code/integrations', (req, res) => {
21817
22065
  let dubiousOwnership = false;
21818
22066
  let trustPath = null;
21819
22067
  try {
21820
- if (detectDubiousOwnership(normalized)) {
22068
+ if (await detectDubiousOwnership(normalized)) {
21821
22069
  dubiousOwnership = true;
21822
22070
  trustPath = normalizeGitPath(normalized);
21823
22071
  }
@@ -22252,7 +22500,7 @@ app.delete('/api/claude-code/mcp', (req, res) => {
22252
22500
  });
22253
22501
 
22254
22502
  // ═══════════════════════════════════════════
22255
- // Multi-Provider MCP Setup (Gemini, Codex)
22503
+ // Multi-Provider MCP Setup (Antigravity, Codex)
22256
22504
  // ═══════════════════════════════════════════
22257
22505
 
22258
22506
  // ── Minimal TOML helpers (for Codex config.toml) ──
@@ -22411,51 +22659,43 @@ function buildCodexMcpCliCommand() {
22411
22659
  return `codex mcp add SynaBun ${envArgs} -- node ${JSON.stringify(definition.args[0])}`;
22412
22660
  }
22413
22661
 
22414
- // ── Gemini MCP endpoints ──
22662
+ // ── Antigravity MCP endpoints ──
22663
+ // ~/.gemini/config/mcp_config.json (lib/mcp-client-config.js): the file the Antigravity CLI and app share. A file that
22664
+ // is not a JSON object is never overwritten: the request fails and says so.
22415
22665
 
22416
- app.get('/api/setup/gemini/mcp', (req, res) => {
22666
+ app.get('/api/setup/antigravity/mcp', (req, res) => {
22417
22667
  try {
22418
- const settingsPath = join(getHomePath(), '.gemini', 'settings.json');
22419
- if (!existsSync(settingsPath)) return res.json({ ok: true, connected: false });
22420
- const data = JSON.parse(readFileSync(settingsPath, 'utf-8'));
22421
- const connected = !!(data.mcpServers && data.mcpServers.SynaBun);
22422
- res.json({ ok: true, connected });
22668
+ const path = antigravityMcpConfigPath();
22669
+ if (!existsSync(path)) return res.json({ ok: true, connected: false, path });
22670
+ const data = readAntigravityMcpConfig();
22671
+ const entry = data.mcpServers?.SynaBun;
22672
+ res.json({ ok: true, connected: !!entry && entry.disabled !== true, disabled: entry?.disabled === true, path });
22423
22673
  } catch (err) {
22424
22674
  res.status(500).json({ error: err.message });
22425
22675
  }
22426
22676
  });
22427
22677
 
22428
- app.post('/api/setup/gemini/mcp', (req, res) => {
22678
+ app.post('/api/setup/antigravity/mcp', (req, res) => {
22429
22679
  try {
22430
- const dir = join(getHomePath(), '.gemini');
22431
- const settingsPath = join(dir, 'settings.json');
22432
22680
  const { envPath, mcpCommand, mcpArgs } = getMcpPaths();
22433
- mkdirSync(dir, { recursive: true });
22434
- let data = {};
22435
- try { data = JSON.parse(readFileSync(settingsPath, 'utf-8')); } catch {}
22436
- data = mergeGeminiMcpConfig(data, {
22681
+ const data = mergeAntigravityMcpConfig(readAntigravityMcpConfig(), {
22437
22682
  command: mcpCommand,
22438
22683
  args: mcpArgs,
22439
22684
  env: { DOTENV_PATH: envPath, SYNABUN_DATA_HOME: DATA_HOME, MEMORY_DATA_DIR: resolve(DATA_HOME, 'mcp-data') },
22440
22685
  });
22441
- writeFileSync(settingsPath, JSON.stringify(data, null, 2) + '\n', 'utf-8');
22442
- res.json({ ok: true, message: 'SynaBun MCP registered in Gemini CLI. Restart Gemini to connect.', rules: syncRulesWithMcp('gemini', 'install', req.body) });
22686
+ writeAntigravityMcpConfig(data);
22687
+ res.json({ ok: true, message: 'SynaBun MCP registered in Antigravity. New Antigravity sessions connect to it.', rules: syncRulesWithMcp('antigravity', 'install', req.body) });
22443
22688
  } catch (err) {
22444
22689
  res.status(500).json({ error: err.message });
22445
22690
  }
22446
22691
  });
22447
22692
 
22448
- app.delete('/api/setup/gemini/mcp', (req, res) => {
22693
+ app.delete('/api/setup/antigravity/mcp', (req, res) => {
22449
22694
  try {
22450
- const settingsPath = join(getHomePath(), '.gemini', 'settings.json');
22451
- if (!existsSync(settingsPath)) return res.json({ ok: true, rules: syncRulesWithMcp('gemini', 'remove') });
22452
- const data = JSON.parse(readFileSync(settingsPath, 'utf-8'));
22453
- if (data.mcpServers && data.mcpServers.SynaBun) {
22454
- delete data.mcpServers.SynaBun;
22455
- if (Object.keys(data.mcpServers).length === 0) delete data.mcpServers;
22456
- writeFileSync(settingsPath, JSON.stringify(data, null, 2) + '\n', 'utf-8');
22457
- }
22458
- res.json({ ok: true, message: 'SynaBun MCP removed from Gemini CLI.', rules: syncRulesWithMcp('gemini', 'remove') });
22695
+ if (!existsSync(antigravityMcpConfigPath())) return res.json({ ok: true, rules: syncRulesWithMcp('antigravity', 'remove') });
22696
+ const data = readAntigravityMcpConfig();
22697
+ if (data.mcpServers && data.mcpServers.SynaBun) writeAntigravityMcpConfig(removeAntigravityMcpServer(data));
22698
+ res.json({ ok: true, message: 'SynaBun MCP removed from Antigravity.', rules: syncRulesWithMcp('antigravity', 'remove') });
22459
22699
  } catch (err) {
22460
22700
  res.status(500).json({ error: err.message });
22461
22701
  }
@@ -22516,11 +22756,11 @@ app.get('/api/setup/status', (req, res) => {
22516
22756
  } catch {}
22517
22757
  claude.cliCommand = claudeMcpAddCommand(mcpPaths);
22518
22758
 
22519
- // Gemini
22520
- let gemini = { connected: false };
22759
+ // Antigravity (~/.gemini/config/mcp_config.json)
22760
+ let antigravity = { connected: false };
22521
22761
  try {
22522
- const gj = JSON.parse(readFileSync(join(home, '.gemini', 'settings.json'), 'utf-8'));
22523
- gemini.connected = !!(gj.mcpServers && gj.mcpServers.SynaBun);
22762
+ const entry = readAntigravityMcpConfig().mcpServers?.SynaBun;
22763
+ antigravity.connected = !!entry && entry.disabled !== true;
22524
22764
  } catch {}
22525
22765
 
22526
22766
  // Codex
@@ -22540,7 +22780,7 @@ app.get('/api/setup/status', (req, res) => {
22540
22780
  let rules = null;
22541
22781
  try { rules = rulesetInstaller.status(); } catch (err) { rules = { ok: false, error: err.message }; }
22542
22782
 
22543
- res.json({ ok: true, claude, gemini, codex, opencode, rules, paths: { mcpIndexPath, envPath, mcpCommand, mcpArgs } });
22783
+ res.json({ ok: true, claude, antigravity, codex, opencode, rules, paths: { mcpIndexPath, envPath, mcpCommand, mcpArgs } });
22544
22784
  } catch (err) {
22545
22785
  res.status(500).json({ error: err.message });
22546
22786
  }
@@ -22640,7 +22880,7 @@ skillsService = createSkillsService({
22640
22880
  // its skill hints); invite guests do not (broadcastSync keeps `skills:` to the owner).
22641
22881
  broadcast: (event) => broadcastSync({ type: 'skills:changed', event }),
22642
22882
  // Which assistants are installed here (the CLI resolver), for the default of "My assistants" (Settings → Skills).
22643
- installedRuntimes: () => ({ claude: cliFoundForWarmup('claude-code'), codex: cliFoundForWarmup('codex'), opencode: cliFoundForWarmup('opencode') }),
22883
+ installedRuntimes: () => ({ claude: cliFoundForWarmup('claude-code'), codex: cliFoundForWarmup('codex'), opencode: cliFoundForWarmup('opencode'), antigravity: cliFoundForWarmup('antigravity') }),
22644
22884
  runtimeChecks: skillsRuntimeChecks,
22645
22885
  operationsOptions: { notify: (item) => {
22646
22886
  const session = _assistantRuntime?.listSessions?.().find((s) => s.status !== 'ended');
@@ -22848,9 +23088,11 @@ function skillBrowserAvailability() {
22848
23088
  const unreadable = browserConfigUnreadable(cfg);
22849
23089
  if (unreadable) return { available: false, reason: `The browser settings cannot be read (${unreadable}): fix them in Settings → Browser.` };
22850
23090
  if (cfg.connectMode === 'morelogin') {
22851
- let installed = false;
22852
- try { installed = !!moreLogin.isMoreLoginInstalled().installed; } catch { installed = false; }
22853
- return installed ? { available: true, reason: null } : { available: false, reason: 'MoreLogin is the default browser (Settings → Browser) and it is not installed.' };
23091
+ // The exe in a known folder, or the local API answered lately (an install in a custom folder). This is read
23092
+ // synchronously, so a miss refreshes the answer in the background for the next call.
23093
+ const installed = moreLogin.isMoreLoginPresent();
23094
+ if (!installed) moreLogin.probeMoreLoginApi({ savedCfg: cfg, timeout: 1500 }).catch(() => {});
23095
+ return installed ? { available: true, reason: null } : { available: false, reason: 'MoreLogin is the default browser (Settings → Browser) and it was not found: its app is in no known folder and its local API does not answer.' };
22854
23096
  }
22855
23097
  if ([...browserSessions.values()].some((s) => s?.context)) return { available: true, reason: null };
22856
23098
  if (findBrowserExecutable(cfg.channel)) return { available: true, reason: null };
@@ -22941,6 +23183,19 @@ async function skillSourceSessions({ provider, project = null, q = '', limit = 4
22941
23183
  out.push({ provider, sessionId: row.id, title, project: proj.path, projectLabel: proj.label || basename(proj.path), modified: row.time_updated ? new Date(row.time_updated).toISOString() : null, messages: Number(row.message_count) || 0 });
22942
23184
  }
22943
23185
  }
23186
+ } else if (provider === 'antigravity') {
23187
+ // The user's own Antigravity conversations (the panel's and a terminal agy's), as Resume's index keeps them,
23188
+ // titled as Antigravity titles them (refreshAntigravityCatalog).
23189
+ refreshAntigravityCatalog();
23190
+ for (const entry of listSessionCache('antigravity', { limit: 100000 })) {
23191
+ if (entry.deleted || !entry.project_path) continue;
23192
+ const dir = resolve(entry.project_path);
23193
+ const proj = projects.find((p) => isInside(resolve(p.path), dir, { allowRoot: true }));
23194
+ if (!proj) continue;
23195
+ const title = String(entry.first_prompt || '');
23196
+ if (search && !title.toLowerCase().includes(search) && !String(entry.session_id).toLowerCase().includes(search)) continue;
23197
+ out.push({ provider, sessionId: entry.session_id, title, project: proj.path, projectLabel: proj.label || basename(proj.path), modified: entry.modified || null, messages: Number(entry.message_count) || 0 });
23198
+ }
22944
23199
  }
22945
23200
  return out.sort((a, b) => String(b.modified || '').localeCompare(String(a.modified || ''))).slice(0, Math.max(1, Math.min(100, Number(limit) || 40)));
22946
23201
  }
@@ -22948,7 +23203,8 @@ async function skillSourceSessions({ provider, project = null, q = '', limit = 4
22948
23203
  /**
22949
23204
  * One session transcript as messages, read the way each runtime keeps it: Claude Code's transcript inside a
22950
23205
  * registered project's folder (lib/path-confine.js findSessionTranscript), Codex's rollout inside its account's home,
22951
- * OpenCode's rows of its database. Bounded: the first messages and the last ones of a very long session.
23206
+ * OpenCode's rows of its database, Antigravity's transcript in the user's own ~/.gemini. Bounded: the first messages
23207
+ * and the last ones of a very long session.
22952
23208
  */
22953
23209
  async function skillSourceConversation({ provider, sessionId, project = null }) {
22954
23210
  const keep = (list, m) => { list.push(m); if (list.length > 2400) list.splice(300, list.length - 2100); };
@@ -22991,6 +23247,16 @@ async function skillSourceConversation({ provider, sessionId, project = null })
22991
23247
  const rows = db.prepare(`SELECT type, data FROM session_message WHERE session_id = ? AND type IN ('user', 'assistant') ORDER BY seq ASC LIMIT 5000`).all(sessionId);
22992
23248
  return { title: session.title || '', project, messages: opencodeRowsToMessages(rows) };
22993
23249
  }
23250
+ if (provider === 'antigravity') {
23251
+ // The transcript in the user's own Antigravity folder (~/.gemini/antigravity-cli/brain/<id>/…), read as
23252
+ // lib/antigravity/transcripts.js reads it: no link followed, bounded; never the model's reasoning.
23253
+ const found = findUserConversation(sessionId);
23254
+ if (!found) throw Object.assign(new Error('That Antigravity conversation was not found.'), { status: 404, code: 'source_not_found' });
23255
+ const parsed = parseAntigravityTranscript(readTranscriptText(found.transcript));
23256
+ const messages = [];
23257
+ for (const m of antigravityMessagesToMessages(parsed.messages)) keep(messages, m);
23258
+ return { title: found.title || parsed.firstPrompt || '', project: found.cwd || project, messages };
23259
+ }
22994
23260
  throw Object.assign(new Error('Unknown provider.'), { status: 400, code: 'source_failed' });
22995
23261
  }
22996
23262
 
@@ -23078,7 +23344,8 @@ function getOpenCodeCommandDir() {
23078
23344
  return xdgDefault;
23079
23345
  }
23080
23346
 
23081
- const SKILL_TARGETS = /** @type {const} */ (['claude', 'codex', 'opencode']);
23347
+ // Antigravity reads its user skills from ~/.gemini/config/skills (lib/skills/roots.js), installed like the others.
23348
+ const SKILL_TARGETS = /** @type {const} */ (['claude', 'codex', 'opencode', 'antigravity']);
23082
23349
 
23083
23350
  /** Where the old prompt / command bridge of a skill is (Codex, OpenCode), or null (Claude Code never had one). */
23084
23351
  function legacyBridgePath(target, skillName) {
@@ -23340,6 +23607,7 @@ function listAvailableSkills() {
23340
23607
  claude: isSkillInstalled('claude', dirName),
23341
23608
  codex: isSkillInstalled('codex', dirName),
23342
23609
  opencode: isSkillInstalled('opencode', dirName),
23610
+ antigravity: isSkillInstalled('antigravity', dirName),
23343
23611
  });
23344
23612
  // What each runtime has of it: a native skill folder ('native'; 'modified' when changed since SynaBun installed
23345
23613
  // it; 'other' when it is not SynaBun's), and an old bridge not moved yet. `installedTargets` stays the one flag
@@ -23724,6 +23992,11 @@ app.post('/api/sidepanel/kill-session', async (req, res) => {
23724
23992
  killed = true;
23725
23993
  }
23726
23994
  }
23995
+ } else if (provider === 'antigravity') {
23996
+ // An Antigravity session lives in the `agy acp` of the panel socket that opened it, which ends with that
23997
+ // socket: nothing outlives a closed panel, so there is nothing to kill here. Its conversation stays in
23998
+ // Antigravity's own history (Resume lists it).
23999
+ killed = false;
23727
24000
  } else if (provider === 'opencode') {
23728
24001
  // OpenCode sessions live on the OpenCode SDK server, not in an orphan
23729
24002
  // map — DELETE them via the proxy.
@@ -24942,7 +25215,7 @@ function externalToolsCatalog(cwd = null) {
24942
25215
  function applyOpenCodeExternalScope(xdgRoot, config, tools, cwd, { readProject = true } = {}) {
24943
25216
  const scoped = scopeOpenCodeServers(config, tools, externalInventory('opencode', cwd || PACKAGE_ROOT, { readProject }));
24944
25217
  Object.assign(config, scoped.config);
24945
- _externalMcpLogValues.set(xdgRoot, mcpDefinitionValues(scoped.config.mcp.servers));
25218
+ _externalMcpLogValues.set(xdgRoot, mcpSecretValues(scoped.config.mcp.servers));
24946
25219
  writeFileSync(resolve(xdgRoot, 'synabun-external-scope.json'), JSON.stringify(scoped.scope), { mode: 0o600 });
24947
25220
  return scoped.scope;
24948
25221
  }
@@ -25276,7 +25549,7 @@ app.get('/api/mcp/platforms', (req, res) => {
25276
25549
  claudeCode: existsSync(join(home, '.claude.json')) || existsSync(join(home, '.claude')),
25277
25550
  opencode: existsSync(getOpencodeConfigDir()),
25278
25551
  codex: existsSync(join(home, '.codex')),
25279
- gemini: existsSync(join(home, '.gemini')),
25552
+ antigravity: existsSync(antigravityConfigDir()) || existsSync(join(home, '.gemini', 'antigravity-cli')),
25280
25553
  });
25281
25554
  });
25282
25555
 
@@ -25393,7 +25666,7 @@ app.post('/api/mcp/install/github', async (req, res) => {
25393
25666
  });
25394
25667
 
25395
25668
  // GET /api/mcp/installed-all — unified view of every externally-installed MCP
25396
- // across Claude Code / OpenCode / Codex / Gemini + SynaBun's registry + Claude Code plugins.
25669
+ // across Claude Code / OpenCode / Codex / Antigravity + SynaBun's registry + Claude Code plugins.
25397
25670
  // Excludes SynaBun's built-in tool-group servers (they're baked in, not "installed").
25398
25671
  app.get('/api/mcp/installed-all', (req, res) => {
25399
25672
  try {
@@ -25440,17 +25713,17 @@ app.get('/api/mcp/installed-all', (req, res) => {
25440
25713
  }
25441
25714
  } catch {}
25442
25715
 
25443
- // Gemini
25716
+ // Antigravity (~/.gemini/config/mcp_config.json: a remote server is `serverUrl`)
25444
25717
  try {
25445
- const p = join(home, '.gemini', 'settings.json');
25446
- if (existsSync(p)) {
25447
- const data = JSON.parse(readFileSync(p, 'utf-8'));
25718
+ if (existsSync(antigravityMcpConfigPath())) {
25719
+ const data = readAntigravityMcpConfig();
25448
25720
  for (const [name, cfg] of Object.entries(data.mcpServers || {})) {
25449
- touch(name, 'gemini', {
25450
- transport: cfg.type || (cfg.url ? 'http' : 'stdio'),
25451
- command: cfg.command || null,
25452
- args: cfg.args || null,
25453
- url: cfg.url || null,
25721
+ const seen = describeAntigravityMcpEntry(cfg);
25722
+ touch(name, 'antigravity', {
25723
+ transport: seen.transport,
25724
+ command: seen.command || null,
25725
+ args: seen.args?.length ? seen.args : null,
25726
+ url: seen.url || null,
25454
25727
  });
25455
25728
  }
25456
25729
  }
@@ -25666,16 +25939,11 @@ async function internalMcpSync({ name, config, platforms }) {
25666
25939
  content = tomlUpsertSection(content, `mcp_servers.${name}`, kvPairs);
25667
25940
  writeFileSync(configPath, content, 'utf-8');
25668
25941
  results.codex = 'ok';
25669
- } else if (p === 'gemini') {
25670
- const dir = join(home, '.gemini');
25671
- const settingsPath = join(dir, 'settings.json');
25672
- mkdirSync(dir, { recursive: true });
25673
- let data = {};
25674
- try { data = JSON.parse(readFileSync(settingsPath, 'utf-8')); } catch {}
25675
- if (!data.mcpServers) data.mcpServers = {};
25676
- data.mcpServers[name] = buildGeminiMcpEntry(config);
25677
- writeFileSync(settingsPath, JSON.stringify(data, null, 2) + '\n', 'utf-8');
25678
- results.gemini = 'ok';
25942
+ } else if (p === 'antigravity') {
25943
+ const data = readAntigravityMcpConfig();
25944
+ data.mcpServers = { ...(data.mcpServers || {}), [name]: buildAntigravityMcpEntry(config) };
25945
+ writeAntigravityMcpConfig(data);
25946
+ results.antigravity = 'ok';
25679
25947
  }
25680
25948
  } catch (err) {
25681
25949
  results[p] = `error: ${err.message}`;
@@ -25733,17 +26001,12 @@ app.delete('/api/mcp/sync', (req, res) => {
25733
26001
  }
25734
26002
  results.codex = 'ok';
25735
26003
 
25736
- } else if (p === 'gemini') {
25737
- const settingsPath = join(home, '.gemini', 'settings.json');
25738
- if (existsSync(settingsPath)) {
25739
- const data = JSON.parse(readFileSync(settingsPath, 'utf-8'));
25740
- if (data.mcpServers?.[name]) {
25741
- delete data.mcpServers[name];
25742
- if (Object.keys(data.mcpServers).length === 0) delete data.mcpServers;
25743
- writeFileSync(settingsPath, JSON.stringify(data, null, 2) + '\n', 'utf-8');
25744
- }
26004
+ } else if (p === 'antigravity') {
26005
+ if (existsSync(antigravityMcpConfigPath())) {
26006
+ const data = readAntigravityMcpConfig();
26007
+ if (data.mcpServers?.[name]) writeAntigravityMcpConfig(removeAntigravityMcpServer(data, name));
25745
26008
  }
25746
- results.gemini = 'ok';
26009
+ results.antigravity = 'ok';
25747
26010
  }
25748
26011
  } catch (err) {
25749
26012
  results[p] = `error: ${err.message}`;
@@ -26160,9 +26423,11 @@ const CLI_CONFIG_PATH = resolve(DATA_HOME, 'data', 'cli-config.json');
26160
26423
  const CLI_DEFAULTS = {
26161
26424
  'claude-code': { command: 'claude' },
26162
26425
  'codex': { command: 'codex' },
26163
- 'gemini': { command: 'gemini' },
26426
+ 'antigravity': { command: 'agy' },
26164
26427
  'opencode': { command: 'opencode' },
26165
26428
  };
26429
+ // Profiles SynaBun no longer drives: a path saved for one (Gemini CLI, replaced by Antigravity) is never a profile.
26430
+ const RETIRED_CLI_PROFILES = new Set(['gemini']);
26166
26431
 
26167
26432
  // Parsed cli-config.json keyed by mtime — every terminal spawn resolves its
26168
26433
  // profile through here, so skip the read + parse when the file is unchanged.
@@ -26175,7 +26440,9 @@ function loadCliConfig() {
26175
26440
  if (!_cliConfigCache || _cliConfigCache.mtimeMs !== mtimeMs) {
26176
26441
  _cliConfigCache = { mtimeMs, raw: JSON.parse(readFileSync(CLI_CONFIG_PATH, 'utf-8')) };
26177
26442
  }
26178
- return { ...CLI_DEFAULTS, ...structuredClone(_cliConfigCache.raw) };
26443
+ const saved = structuredClone(_cliConfigCache.raw);
26444
+ for (const retired of RETIRED_CLI_PROFILES) delete saved?.[retired];
26445
+ return { ...CLI_DEFAULTS, ...saved };
26179
26446
  } catch { return { ...CLI_DEFAULTS }; }
26180
26447
  }
26181
26448
 
@@ -26196,7 +26463,7 @@ const SHELL_PROFILE = { shell: DEFAULT_SHELL, args: interactiveShellArgs(), env:
26196
26463
  /**
26197
26464
  * Build terminal profile config dynamically from cli-config.json.
26198
26465
  * Re-reads config each call so changes take effect without restart.
26199
- * @param {string} profileId - CLI profile ID (e.g. 'claude-code', 'codex', 'gemini', 'shell')
26466
+ * @param {string} profileId - CLI profile ID (e.g. 'claude-code', 'codex', 'antigravity', 'shell')
26200
26467
  * @param {Object} [opts] - Optional overrides
26201
26468
  * @param {string} [opts.model] - Model flag for CLIs that support it (e.g. claude --model sonnet)
26202
26469
  */
@@ -26219,6 +26486,8 @@ function getTerminalProfile(profileId, opts = {}) {
26219
26486
  cmd = `${cmd} resume ${terminalArgument(opts.resume, DEFAULT_SHELL)}`;
26220
26487
  } else if (opts.resume && profileId === 'opencode') {
26221
26488
  cmd = `${cmd} -s ${terminalArgument(opts.resume, DEFAULT_SHELL)}`;
26489
+ } else if (opts.resume && profileId === 'antigravity') {
26490
+ cmd = `${cmd} --conversation ${terminalArgument(opts.resume, DEFAULT_SHELL)}`;
26222
26491
  }
26223
26492
  // Append model flag for CLIs that support it
26224
26493
  else if (opts.model) {
@@ -26236,7 +26505,7 @@ function getTerminalProfile(profileId, opts = {}) {
26236
26505
  const modelMap = {
26237
26506
  'claude-code': (m) => `${cmd} --model ${m}`,
26238
26507
  'codex': (m) => `${cmd} --model ${m}`,
26239
- 'gemini': (m) => `${cmd} --model ${m}`,
26508
+ 'antigravity': (m) => `${cmd} --model ${m}`,
26240
26509
  // OpenCode's terminal app has no --model (it prints its help and exits): the model is picked in it.
26241
26510
  'opencode': () => cmd,
26242
26511
  };
@@ -26254,6 +26523,9 @@ function getTerminalProfile(profileId, opts = {}) {
26254
26523
  if (codexEffort) {
26255
26524
  cmd = `${cmd} -c model_reasoning_effort="${codexEffort}"`;
26256
26525
  }
26526
+ } else if (opts.effort && profileId === 'antigravity') {
26527
+ // agy --effort: low | medium | high | xhigh | max (the model's own default when the flag is left out).
26528
+ if (['low', 'medium', 'high', 'xhigh', 'max'].includes(opts.effort)) cmd = `${cmd} --effort ${opts.effort}`;
26257
26529
  }
26258
26530
  // Every Codex terminal: the Windows sandbox the Admin sandbox setting allows. A bare value
26259
26531
  // (Codex reads one that is not TOML as a string), so no shell can strip a quote from it.
@@ -26265,7 +26537,7 @@ function getTerminalProfile(profileId, opts = {}) {
26265
26537
  // The loop runs in a PTY with no human to answer prompts, so accessing
26266
26538
  // sibling projects or running Bash/Write would stall.
26267
26539
  // Scoped to opts.loopBypass so interactive terminal sessions still prompt.
26268
- if (opts.loopBypass && profileId === 'claude-code') {
26540
+ if (opts.loopBypass && (profileId === 'claude-code' || profileId === 'antigravity')) {
26269
26541
  cmd = `${cmd} --dangerously-skip-permissions`;
26270
26542
  }
26271
26543
 
@@ -26294,7 +26566,7 @@ function getTerminalProfile(profileId, opts = {}) {
26294
26566
  // provider sign-in terminals. The desktop app uses this same server/PTY host.
26295
26567
  async function freshTerminalPath(base = process.env) {
26296
26568
  const os = await osPath.refresh({ force: true });
26297
- const launchers = ['claude-code', 'codex', 'opencode', 'gemini'].map(tool => cliResolver.launcher(tool));
26569
+ const launchers = ['claude-code', 'codex', 'opencode', 'antigravity'].map(tool => cliResolver.launcher(tool));
26298
26570
  // A packaged application's runtime and npm's global bin (rememberPackagedPath above), after the user's own.
26299
26571
  const { last: synabunDirs, extra: extraDirs } = rememberedPackagedPath();
26300
26572
  return { ...composeTerminalPath({ env: base, os, launchers, synabunDirs, extraDirs }), os, launchers };
@@ -26861,7 +27133,7 @@ async function driveNextIteration(session, loopState, filename) {
26861
27133
  return true;
26862
27134
  }
26863
27135
 
26864
- // ── Exec-based Loop Driver (Codex, Gemini, etc.) ──
27136
+ // ── Exec-based Loop Driver (Codex, Antigravity, etc.) ──
26865
27137
  // Fully isolated from Claude's hook-based driver. Runs platform-native wrapper
26866
27138
  // files inside a shell PTY and detects completion via sentinel echo markers.
26867
27139
  // The wrapper captures the CLI status before printing SYNABUN_ITER_DONE_<N>, so
@@ -27191,6 +27463,31 @@ _nativeLoopRuntime = new NativeLoopRuntime({
27191
27463
  policyRoots: taskBrowserPolicyRoots(state),
27192
27464
  });
27193
27465
  },
27466
+ // Google Antigravity: one `agy acp` per run (lib/native-loop-providers.js createAntigravityNativeLoopAdapter), in
27467
+ // the run's own Antigravity home (lib/antigravity/home.js: exactly the run's MCP servers, the policy hook that
27468
+ // asks POST /api/antigravity/policy before every tool call, the user's rules and skills). Approvals go to the
27469
+ // Assistant's broker under `ask`; without an installed CLI the run fails at once with the install hint.
27470
+ antigravity: (state) => {
27471
+ const launch = antigravityLaunch();
27472
+ if (!launch) throw new Error('Antigravity runs need the Antigravity CLI (agy). Install it from the Antigravity side panel (one click), or run `brew install --cask antigravity-cli` (macOS) or `curl -fsSL https://antigravity.google/cli/install.sh | bash` (Windows: `irm https://antigravity.google/cli/install.ps1 | iex`), sign in by running `agy` once, then try again.');
27473
+ // A resumed run uses the home its first start wrote (its conversation is there).
27474
+ const homeDir = state.resumingWorker && typeof state.antigravityHome === 'string' && state.antigravityHome
27475
+ ? validateRunHome(state.antigravityHome, antigravityHomesRoot(DATA_HOME))
27476
+ : antigravityHomeFor('run', state.runId || state.terminalSessionId || `loop-${Date.now()}`);
27477
+ return createAntigravityNativeLoopAdapter({
27478
+ ...state,
27479
+ launch,
27480
+ // The PATH a new terminal has: Antigravity runs the user's own tools.
27481
+ extraEnv: { PATH: antigravityChildEnv().PATH, ...(state.extraEnv || {}) },
27482
+ serverPort: PORT,
27483
+ homeDir,
27484
+ synabunMcpEntry: antigravitySynabunMcpEntry(),
27485
+ policy: defaultAntigravityPolicy,
27486
+ policyUrl: antigravityPolicyUrl(),
27487
+ // Assistant task runs: where the browser policy lets its test runners run besides its own project.
27488
+ policyRoots: taskBrowserPolicyRoots(state),
27489
+ });
27490
+ },
27194
27491
  opencode: async (state) => {
27195
27492
  // An OpenCode SynaBun does not drive: the run fails with the sentence before its configuration folder, its
27196
27493
  // sandbox profile or its serve exist (a loop or a schedule was refused already; this is a worker's, a backstop).
@@ -27239,6 +27536,7 @@ _nativeLoopRuntime = new NativeLoopRuntime({
27239
27536
  state.model,
27240
27537
  {
27241
27538
  SYNABUN_DESKTOP_GRANT: typeof state.desktopGrantProvider === 'function' ? state.desktopGrantProvider() : null,
27539
+ SYNABUN_RUN_MODE: state.runMode === 'task' ? 'task' : null,
27242
27540
  },
27243
27541
  // Assistant task runs: the browser policy plugin (loops and schedules unchanged) with the registered code
27244
27542
  // projects its test runners may run in (the sandboxed serve cannot read the registry), and the run's confinement
@@ -27819,6 +28117,17 @@ _nativeLoopRuntime = new NativeLoopRuntime({
27819
28117
  ClaudeSession: claudeBridgeModule.ClaudeSession,
27820
28118
  sdkVersion: claudeBridgeModule.SDK_VERSION,
27821
28119
  handleCodexSkinWebSocket, ensureIsolatedServe, stopIsolatedServe, setupOpencodeSidepanelConfig,
28120
+ // An Antigravity brain (lib/assistant-brains/antigravity.js): `agy acp` in the brain's own home, its hook asking
28121
+ // POST /api/antigravity/policy, SynaBun's MCP entry the Setup card writes.
28122
+ antigravity: {
28123
+ launch: antigravityLaunch,
28124
+ // The PATH a new terminal has (agy runs the user's tools); never the owner's live data paths or a credential.
28125
+ childEnv: antigravityChildEnv,
28126
+ home: (sessionId) => antigravityHomeFor('brain', sessionId),
28127
+ synabunMcpEntry: antigravitySynabunMcpEntry,
28128
+ policy: defaultAntigravityPolicy,
28129
+ policyUrl: antigravityPolicyUrl,
28130
+ },
27822
28131
  broadcastSync,
27823
28132
  dataDir: LOOP_DATA_DIR,
27824
28133
  memory: assistantMemory,
@@ -28196,7 +28505,7 @@ function findLoopFileForTerminal(terminalSessionId) {
28196
28505
  }
28197
28506
 
28198
28507
  /**
28199
- * Exec-based loop driver for non-Claude CLIs (Codex, Gemini, etc.).
28508
+ * Exec-based loop driver for non-Claude CLIs (Codex, Antigravity, etc.).
28200
28509
  * State machine: booting → ready → running → ready → ...
28201
28510
  *
28202
28511
  * Completion detection: sentinel echo approach.
@@ -28972,7 +29281,7 @@ app.delete('/api/terminal/sessions/:id', (req, res) => {
28972
29281
  });
28973
29282
 
28974
29283
  app.get('/api/terminal/profiles', (req, res) => {
28975
- const profileIds = ['claude-code', 'codex', 'gemini', 'opencode', 'shell'];
29284
+ const profileIds = ['claude-code', 'codex', 'antigravity', 'opencode', 'shell'];
28976
29285
  const profiles = profileIds.map(id => {
28977
29286
  const p = getTerminalProfile(id);
28978
29287
  return { id, shell: p.shell, args: p.args };
@@ -29056,30 +29365,15 @@ function resolveGitPath() {
29056
29365
  }
29057
29366
 
29058
29367
  // Cross-platform git invocation. Uses resolved absolute path when possible (spawnable
29059
- // directly on all OSes with no shell/quoting pitfalls). Falls back to `shell: true` on
29060
- // Windows only when path resolution fails or when the resolved target is a .cmd/.bat shim.
29061
- function spawnGit(args, opts = {}) {
29062
- const baseOpts = { stdio: 'pipe', timeout: 5000, windowsHide: true, ...opts };
29063
- const gitPath = resolveGitPath();
29064
- if (gitPath) {
29065
- if (IS_WINDOWS && /\.(cmd|bat)$/i.test(gitPath)) {
29066
- const quoted = args.map(a => /[\s()%&|<>^"']/.test(a) ? `"${String(a).replace(/"/g, '\\"')}"` : a);
29067
- return spawnSync(gitPath, quoted, { ...baseOpts, shell: true });
29068
- }
29069
- return spawnSync(gitPath, args, baseOpts);
29070
- }
29071
- if (IS_WINDOWS) {
29072
- const quoted = args.map(a => /[\s()%&|<>^"']/.test(a) ? `"${String(a).replace(/"/g, '\\"')}"` : a);
29073
- return spawnSync('git', quoted, { ...baseOpts, shell: true });
29074
- }
29075
- return spawnSync('git', args, baseOpts);
29368
+ // directly without a shell). The runner handles a resolved Windows .cmd/.bat shim.
29369
+ async function spawnGit(args, opts = {}) {
29370
+ const binary = await resolveGitPathInBackground();
29371
+ return gitStatusRunner.runRaw(args, { ...opts, binary: binary || 'git', timeoutMs: opts.timeout || 5000 });
29076
29372
  }
29077
29373
 
29078
- // spawnGit for a command that only reads, in a folder a request named: nothing the repository's own configuration
29079
- // names is started for it (lib/git-read.js: no file system monitor program, no hook, no external diff, no fetch).
29080
- function spawnGitRead(args, opts = {}) {
29081
- const read = gitReadSync(opts.cwd, args, { env: opts.env });
29082
- return spawnGit(read.args, { ...opts, env: read.env });
29374
+ async function spawnGitRead(args, opts = {}) {
29375
+ const binary = await resolveGitPathInBackground();
29376
+ return gitStatusRunner.run(args, { ...opts, binary: binary || 'git', timeoutMs: opts.timeout || 5000 });
29083
29377
  }
29084
29378
 
29085
29379
  function logGitFailure(where, err) {
@@ -29118,22 +29412,20 @@ function resolveGitPathInBackground() {
29118
29412
 
29119
29413
  // Startup probe — always logs git status so Windows users can see whether git works at all. Asynchronous:
29120
29414
  // after the lookup above, one `git --version` (no folder, no repository's configuration) off the event loop.
29121
- setImmediate(() => {
29122
- resolveGitPathInBackground().then((gitPath) => new Promise((done) => {
29123
- const shell = IS_WINDOWS && (!gitPath || /\.(cmd|bat)$/i.test(gitPath));
29124
- execFile(gitPath || 'git', ['--version'], { encoding: 'utf8', timeout: 3000, windowsHide: true, shell }, (error, stdout, stderr) => {
29125
- if (!error) console.log(`[git] ${String(stdout || '').trim()} — OK`);
29126
- else console.warn(`[git] startup probe FAILED: ${String(stderr || '').trim() || error.message || 'unknown error'}. File explorer git UI will be hidden.`);
29127
- done();
29128
- });
29129
- })).catch(() => {});
29415
+ setImmediate(async () => {
29416
+ try {
29417
+ const gitPath = await resolveGitPathInBackground();
29418
+ const result = await gitStatusRunner.run(['--version'], { binary: gitPath || 'git', timeoutMs: 3000 });
29419
+ if (result.ok) console.log(`[git] ${String(result.stdout).trim()} — OK`);
29420
+ else console.warn(`[git] startup probe FAILED: ${String(result.stderr).trim() || result.code}. File explorer git UI will be hidden.`);
29421
+ } catch { /* availability is reported on the first read */ }
29130
29422
  });
29131
29423
 
29132
29424
  // Git "dubious ownership" detection — CVE-2022-24765 safety check refuses to operate on
29133
29425
  // repos owned by a different OS user/SID. Common on Windows external drives or repos
29134
29426
  // copied from another machine. Returns true only when git emits the specific error.
29135
- function detectDubiousOwnership(dir) {
29136
- const r = spawnGitRead(['rev-parse', '--is-inside-work-tree'], { cwd: dir, timeout: 3000 });
29427
+ async function detectDubiousOwnership(dir) {
29428
+ const r = await spawnGitRead(['rev-parse', '--is-inside-work-tree'], { cwd: dir, timeout: 3000 });
29137
29429
  if (r.status === 0) return false;
29138
29430
  const stderr = r.stderr?.toString?.() || '';
29139
29431
  return /dubious ownership|safe\.directory/i.test(stderr);
@@ -29146,91 +29438,27 @@ function normalizeGitPath(dir) {
29146
29438
  }
29147
29439
 
29148
29440
  /** Try to get git status for a directory. Returns { branch, statuses } or null. */
29149
- function getGitInfo(dir) {
29150
- // Check if inside a git work tree (spawnGit for consistent path resolution)
29151
- const inside = spawnGitRead(['rev-parse', '--is-inside-work-tree'], { cwd: dir, timeout: 3000 });
29152
- if (inside.status !== 0) {
29153
- logGitFailure('rev-parse --is-inside-work-tree', inside.error || { stderr: inside.stderr });
29154
- return null;
29441
+ async function getGitInfo(dir, options = {}) {
29442
+ const result = projectStatus(await gitStatusRunner.read(dir, options), dir);
29443
+ if (!result.isGit) return null;
29444
+ const statuses = new Map();
29445
+ for (const change of result.changes) {
29446
+ const child = change.path.split('/')[0];
29447
+ if (gitStatusPriority(change.status) > gitStatusPriority(statuses.get(child))) statuses.set(child, change.status);
29155
29448
  }
29449
+ return { branch: result.branch, statuses, scanState: result.timedOut ? 'timed-out' : result.ok ? 'ready' : 'failed' };
29450
+ }
29156
29451
 
29157
- // Get branch name — try --show-current first (git 2.22+), then fall back to
29158
- // --abbrev-ref HEAD (works on older git; detached HEAD returns the literal "HEAD"),
29159
- // then short SHA for detached HEAD. The chained fallback is essential for Windows users
29160
- // with older Git for Windows installs where --show-current exits non-zero.
29161
- let branch = '';
29162
- const showCurrent = spawnGitRead(['branch', '--show-current'], { cwd: dir, timeout: 3000 });
29163
- if (showCurrent.status === 0) {
29164
- branch = showCurrent.stdout.toString().trim();
29165
- } else {
29166
- logGitFailure('branch --show-current', showCurrent.error || { stderr: showCurrent.stderr });
29167
- }
29168
- if (!branch) {
29169
- const abbrev = spawnGitRead(['rev-parse', '--abbrev-ref', 'HEAD'], { cwd: dir, timeout: 3000 });
29170
- if (abbrev.status === 0) {
29171
- const b = abbrev.stdout.toString().trim();
29172
- if (b && b !== 'HEAD') branch = b;
29173
- }
29174
- }
29175
- if (!branch) {
29176
- const shortSha = spawnGitRead(['rev-parse', '--short', 'HEAD'], { cwd: dir, timeout: 3000 });
29177
- if (shortSha.status === 0) branch = shortSha.stdout.toString().trim();
29178
- }
29179
-
29180
- try {
29181
- // Get status for this directory (porcelain v1: XY <path>)
29182
- const statusResult = spawnGitRead(['status', '--porcelain', '-u', '.'], { cwd: dir, maxBuffer: 256 * 1024 * 1024 });
29183
- if (statusResult.status !== 0) {
29184
- logGitFailure('status --porcelain', statusResult.error || { stderr: statusResult.stderr });
29185
- return { branch, statuses: new Map() };
29186
- }
29187
- const raw = statusResult.stdout.toString();
29188
- const statuses = new Map(); // name → status code
29189
-
29190
- for (const line of raw.split('\n')) {
29191
- if (!line || line.length < 4) continue;
29192
- const xy = line.substring(0, 2); // index + worktree status
29193
- let filePath = line.substring(3);
29194
- // Handle renames: "R old -> new"
29195
- const arrowIdx = filePath.indexOf(' -> ');
29196
- if (arrowIdx !== -1) filePath = filePath.substring(arrowIdx + 4);
29197
-
29198
- // Get the immediate child name relative to this directory
29199
- const sep = filePath.indexOf('/');
29200
- const childName = sep === -1 ? filePath : filePath.substring(0, sep);
29201
-
29202
- // Map XY to a simplified status
29203
- let status;
29204
- const x = xy[0], y = xy[1];
29205
- if (xy === '??') status = 'untracked';
29206
- else if (xy === '!!') continue; // ignored
29207
- else if (x === 'U' || y === 'U' || xy === 'DD' || xy === 'AA') status = 'conflict';
29208
- else if (x === 'A') status = 'added';
29209
- else if (x === 'D' || y === 'D') status = 'deleted';
29210
- else if (x === 'R') status = 'renamed';
29211
- else if (x !== ' ' && y !== ' ') status = 'mixed'; // staged + unstaged changes
29212
- else if (x !== ' ') status = 'staged';
29213
- else if (y !== ' ') status = 'modified';
29214
- else continue;
29215
-
29216
- // For directories: escalate priority (conflict > modified > staged > added > untracked)
29217
- const existing = statuses.get(childName);
29218
- if (!existing || gitStatusPriority(status) > gitStatusPriority(existing)) {
29219
- statuses.set(childName, status);
29220
- }
29221
- }
29222
-
29223
- return { branch, statuses };
29224
- } catch (err) {
29225
- logGitFailure('getGitInfo status-parse', err);
29226
- return { branch, statuses: new Map() };
29227
- }
29452
+ function gitRequestSignal(res) {
29453
+ const controller = new AbortController();
29454
+ res.once('close', () => controller.abort());
29455
+ return controller.signal;
29228
29456
  }
29229
29457
 
29230
29458
  const GIT_STATUS_PRIORITY = { untracked: 1, added: 2, renamed: 2, staged: 3, modified: 4, mixed: 5, deleted: 5, conflict: 6 };
29231
29459
  function gitStatusPriority(s) { return GIT_STATUS_PRIORITY[s] || 0; }
29232
29460
 
29233
- app.get('/api/terminal/files', (req, res) => {
29461
+ app.get('/api/terminal/files', async (req, res) => {
29234
29462
  const dir = req.query.path;
29235
29463
  if (!dir || typeof dir !== 'string') return res.status(400).json({ error: 'path required' });
29236
29464
  const search = (req.query.search || '').trim().toLowerCase();
@@ -29287,7 +29515,7 @@ app.get('/api/terminal/files', (req, res) => {
29287
29515
  });
29288
29516
 
29289
29517
  // Try git — gracefully ignored if unavailable
29290
- const git = getGitInfo(dir);
29518
+ const git = await getGitInfo(dir, { signal: gitRequestSignal(res) });
29291
29519
  if (git) {
29292
29520
  for (const item of items) {
29293
29521
  const s = git.statuses.get(item.name);
@@ -29301,11 +29529,11 @@ app.get('/api/terminal/files', (req, res) => {
29301
29529
  }
29302
29530
  });
29303
29531
 
29304
- app.get('/api/terminal/branches', (req, res) => {
29532
+ app.get('/api/terminal/branches', async (req, res) => {
29305
29533
  const dir = req.query.path;
29306
29534
  if (!dir || typeof dir !== 'string') return res.status(400).json({ error: 'path required' });
29307
29535
 
29308
- const inside = spawnGitRead(['rev-parse', '--is-inside-work-tree'], { cwd: dir, timeout: 3000 });
29536
+ const inside = await spawnGitRead(['rev-parse', '--is-inside-work-tree'], { cwd: dir, timeout: 3000 });
29309
29537
  if (inside.status !== 0) {
29310
29538
  logGitFailure('branches: rev-parse', inside.error || { stderr: inside.stderr });
29311
29539
  return res.json({ branches: [], current: null });
@@ -29314,11 +29542,11 @@ app.get('/api/terminal/branches', (req, res) => {
29314
29542
  try {
29315
29543
  // Current branch — chained fallback (older git may not support --show-current)
29316
29544
  let current = '';
29317
- const showCurrent = spawnGitRead(['branch', '--show-current'], { cwd: dir, timeout: 3000 });
29545
+ const showCurrent = await spawnGitRead(['branch', '--show-current'], { cwd: dir, timeout: 3000 });
29318
29546
  if (showCurrent.status === 0) current = showCurrent.stdout.toString().trim();
29319
29547
  else logGitFailure('branches: show-current', showCurrent.error || { stderr: showCurrent.stderr });
29320
29548
  if (!current) {
29321
- const abbrev = spawnGitRead(['rev-parse', '--abbrev-ref', 'HEAD'], { cwd: dir, timeout: 3000 });
29549
+ const abbrev = await spawnGitRead(['rev-parse', '--abbrev-ref', 'HEAD'], { cwd: dir, timeout: 3000 });
29322
29550
  if (abbrev.status === 0) {
29323
29551
  const b = abbrev.stdout.toString().trim();
29324
29552
  if (b && b !== 'HEAD') current = b;
@@ -29327,7 +29555,7 @@ app.get('/api/terminal/branches', (req, res) => {
29327
29555
 
29328
29556
  // All local branches — plain `git branch` parsed manually; avoids --format=%(refname:short)
29329
29557
  // which can get mangled by cmd.exe variable expansion on Windows even with proper quoting.
29330
- const branchResult = spawnGitRead(['branch'], { cwd: dir });
29558
+ const branchResult = await spawnGitRead(['branch'], { cwd: dir });
29331
29559
  if (branchResult.status !== 0) logGitFailure('branch list', branchResult.error || { stderr: branchResult.stderr });
29332
29560
  const raw = branchResult.status === 0 ? branchResult.stdout.toString() : '';
29333
29561
  // Plain `git branch` output: each line is " branch" or "* branch" (current). Strip
@@ -29345,7 +29573,7 @@ app.get('/api/terminal/branches', (req, res) => {
29345
29573
  // Trust a workspace path with git — runs `git config --global --add safe.directory <path>`
29346
29574
  // so git stops refusing to operate on it (CVE-2022-24765 dubious-ownership protection).
29347
29575
  // Triggered by the add-project UI flows when the server detects the dubious-ownership error.
29348
- app.post('/api/terminal/trust-workspace', (req, res) => {
29576
+ app.post('/api/terminal/trust-workspace', async (req, res) => {
29349
29577
  const { path: dir } = req.body || {};
29350
29578
  if (!dir || typeof dir !== 'string') return res.status(400).json({ ok: false, error: 'path required' });
29351
29579
  if (/[\x00-\x1f]/.test(dir) || dir === '*' || dir === '/' || dir === '\\') {
@@ -29361,7 +29589,7 @@ app.post('/api/terminal/trust-workspace', (req, res) => {
29361
29589
  }
29362
29590
 
29363
29591
  const gitPath = normalizeGitPath(abs);
29364
- const r = spawnGit(['config', '--global', '--add', 'safe.directory', gitPath]);
29592
+ const r = await spawnGit(['config', '--global', '--add', 'safe.directory', gitPath]);
29365
29593
  if (r.status !== 0) {
29366
29594
  const stderr = r.stderr?.toString?.()?.trim() || r.error?.message || 'git config failed';
29367
29595
  logGitFailure('config --add safe.directory', r.error || { stderr: r.stderr });
@@ -29369,12 +29597,12 @@ app.post('/api/terminal/trust-workspace', (req, res) => {
29369
29597
  }
29370
29598
 
29371
29599
  // Confirm by re-probing
29372
- const stillDubious = detectDubiousOwnership(abs);
29600
+ const stillDubious = await detectDubiousOwnership(abs);
29373
29601
  console.log(`[git] trusted workspace: ${gitPath}${stillDubious ? ' (but still flagged — check permissions)' : ''}`);
29374
29602
  res.json({ ok: true, path: gitPath, stillDubious });
29375
29603
  });
29376
29604
 
29377
- app.post('/api/terminal/checkout', (req, res) => {
29605
+ app.post('/api/terminal/checkout', async (req, res) => {
29378
29606
  const { path: dir, branch } = req.body;
29379
29607
  if (!dir || !branch) return res.status(400).json({ error: 'path and branch required' });
29380
29608
 
@@ -29383,7 +29611,7 @@ app.post('/api/terminal/checkout', (req, res) => {
29383
29611
  return res.status(400).json({ error: 'Invalid branch name' });
29384
29612
  }
29385
29613
 
29386
- const inside = spawnGitRead(['rev-parse', '--is-inside-work-tree'], { cwd: dir, timeout: 3000 });
29614
+ const inside = await spawnGitRead(['rev-parse', '--is-inside-work-tree'], { cwd: dir, timeout: 3000 });
29387
29615
  if (inside.status !== 0) {
29388
29616
  logGitFailure('checkout: rev-parse', inside.error || { stderr: inside.stderr });
29389
29617
  return res.status(400).json({ error: 'Not a git repository' });
@@ -29393,7 +29621,8 @@ app.post('/api/terminal/checkout', (req, res) => {
29393
29621
  // No '--' separator — that would make git treat the branch name as a file pathspec.
29394
29622
  // (The switch itself is a write and runs plainly, with the repository's post-checkout hook; the two questions
29395
29623
  // around it are reads.)
29396
- const result = spawnGit(['checkout', branch], { cwd: dir, timeout: 10000 });
29624
+ const result = await spawnGit(['checkout', branch], { cwd: dir, timeout: 10000 });
29625
+ gitStatusRunner.invalidate(dir);
29397
29626
  if (result.status !== 0) {
29398
29627
  const stderr = result.stderr?.toString?.()?.trim() || result.error?.message || 'Checkout failed';
29399
29628
  return res.status(500).json({ error: stderr });
@@ -29401,71 +29630,21 @@ app.post('/api/terminal/checkout', (req, res) => {
29401
29630
 
29402
29631
  // Return confirmed branch name + git's own output
29403
29632
  let current = '';
29404
- const showCurrent = spawnGitRead(['branch', '--show-current'], { cwd: dir, timeout: 3000 });
29633
+ const showCurrent = await spawnGitRead(['branch', '--show-current'], { cwd: dir, timeout: 3000 });
29405
29634
  if (showCurrent.status === 0) current = showCurrent.stdout.toString().trim();
29406
29635
  else logGitFailure('checkout: show-current', showCurrent.error || { stderr: showCurrent.stderr });
29407
29636
  const output = result.stderr?.toString?.()?.trim() || `Switched to branch '${current}'`;
29408
29637
  res.json({ ok: true, branch: current, output });
29409
29638
  });
29410
29639
 
29411
- // Polled by the file explorer — run git via async execFile (never blocks the
29412
- // event loop) and memoize per path for 1.5s so rapid polls share one result.
29413
- const _gitStatusCache = new Map(); // dir -> { at, promise }
29414
- const GIT_STATUS_TTL_MS = 1500;
29415
-
29416
- async function computeGitStatus(dir) {
29417
- // Each of these only reads: run as lib/git-read.js runs a read, so nothing the folder's own git configuration
29418
- // names is started (the route takes any folder).
29419
- const read = async (args, options) => {
29420
- const safe = await gitRead(dir, args);
29421
- return execFileAsync('git', safe.args, { cwd: dir, env: safe.env, ...options });
29422
- };
29423
- try {
29424
- await read(['rev-parse', '--is-inside-work-tree'], { timeout: 3000 });
29425
- } catch {
29426
- return { ok: true, isGit: false, branch: null, changes: [] };
29427
- }
29428
-
29429
- const resolveBranch = async () => {
29430
- try {
29431
- const { stdout } = await read(['branch', '--show-current'], { timeout: 3000 });
29432
- let branch = stdout.trim();
29433
- if (!branch) {
29434
- const { stdout: head } = await read(['rev-parse', '--short', 'HEAD'], { timeout: 3000 });
29435
- branch = head.trim();
29436
- }
29437
- return branch;
29438
- } catch { return ''; }
29439
- };
29440
-
29441
- const [branch, statusOut] = await Promise.all([
29442
- resolveBranch(),
29443
- read(['status', '--porcelain'], { timeout: 5000, maxBuffer: 256 * 1024 * 1024 }),
29444
- ]);
29445
-
29446
- const changes = [];
29447
- for (const line of statusOut.stdout.split('\n')) {
29448
- if (!line || line.length < 4) continue;
29449
- const xy = line.substring(0, 2);
29450
- let filePath = line.substring(3);
29451
- const arrowIdx = filePath.indexOf(' -> ');
29452
- if (arrowIdx !== -1) filePath = filePath.substring(arrowIdx + 4);
29453
- const x = xy[0], y = xy[1];
29454
- let staged = x !== ' ' && x !== '?';
29455
- let status;
29456
- if (xy === '??') status = 'untracked';
29457
- else if (xy === '!!') continue;
29458
- else if (x === 'U' || y === 'U' || xy === 'DD' || xy === 'AA') status = 'conflict';
29459
- else if (x === 'A') status = 'added';
29460
- else if (x === 'D' || y === 'D') status = 'deleted';
29461
- else if (x === 'R') status = 'renamed';
29462
- else if (x !== ' ' && y !== ' ') status = 'mixed';
29463
- else if (x !== ' ') status = 'staged';
29464
- else status = 'modified';
29465
- changes.push({ path: filePath, status, staged });
29466
- }
29467
-
29468
- return { ok: true, isGit: true, branch, changes };
29640
+ // All callers share the canonical worktree's flight and last completed result.
29641
+ async function computeGitStatus(dir, options = {}) {
29642
+ const result = projectStatus(await gitStatusRunner.read(dir, options), dir);
29643
+ if (!result.ok || result.partial || result.stale) return { ok: false, isGit: !!result.isGit, branch: result.branch, changes: result.changes || [],
29644
+ partial: true, stale: !!result.stale, timedOut: !!result.timedOut, retryAt: result.retryAt,
29645
+ scanState: result.timedOut ? 'timed-out' : 'failed', error: 'Git status is incomplete. Refresh before switching branches or assuming the worktree is clean.' };
29646
+ if (!result.isGit) return { ok: true, isGit: false, branch: null, changes: [] };
29647
+ return { ok: true, isGit: true, branch: result.branch, changes: result.changes };
29469
29648
  }
29470
29649
 
29471
29650
  // The folder a git route is asked about: inside a registered folder, as the disk resolves it (the rule of
@@ -29479,11 +29658,9 @@ function registeredGitFolder(asked, res) {
29479
29658
  // be started, failed or took too long, as the command lines the /api/git routes used to run did. For reads only
29480
29659
  // (every caller is one): run as lib/git-read.js runs a read, so no program the repository's own configuration
29481
29660
  // names is started. `git add` and `git commit` are not run through here: a commit keeps the repository's hooks.
29482
- function gitOutput(dir, args, { timeout = 5000, maxBuffer } = {}) {
29483
- const read = gitReadSync(dir, args);
29484
- const result = spawnSync('git', read.args, { cwd: dir, env: read.env, stdio: 'pipe', timeout, windowsHide: true, ...(maxBuffer ? { maxBuffer } : {}) });
29485
- if (result.error) throw result.error;
29486
- if (result.status !== 0) throw new Error(result.stderr?.toString?.()?.trim?.() || `git ${args[0]} failed`);
29661
+ async function gitOutput(dir, args, { timeout = 5000, maxBuffer } = {}) {
29662
+ const result = await gitStatusRunner.run(args, { cwd: dir, timeoutMs: timeout, ...(maxBuffer ? { maxBuffer } : {}) });
29663
+ if (!result.ok) throw new Error(result.stderr?.toString?.()?.trim?.() || `git ${args[0]} failed`);
29487
29664
  return result.stdout.toString();
29488
29665
  }
29489
29666
 
@@ -29493,37 +29670,17 @@ function gitOutput(dir, args, { timeout = 5000, maxBuffer } = {}) {
29493
29670
  app.get('/api/git/status', editorOwnerGuard, async (req, res) => {
29494
29671
  const dir = req.query.path;
29495
29672
  if (!dir || typeof dir !== 'string') return res.status(400).json({ error: 'path required' });
29496
-
29497
- let entry = _gitStatusCache.get(dir);
29498
- if (!entry || Date.now() - entry.at >= GIT_STATUS_TTL_MS) {
29499
- const promise = computeGitStatus(dir);
29500
- entry = { at: Date.now(), promise };
29501
- _gitStatusCache.set(dir, entry);
29502
- // Failed computations shouldn't be served for the rest of the TTL.
29503
- promise.catch(() => {
29504
- if (_gitStatusCache.get(dir)?.promise === promise) _gitStatusCache.delete(dir);
29505
- });
29506
- // Keep the map bounded across many distinct paths.
29507
- if (_gitStatusCache.size > 100) {
29508
- const cutoff = Date.now() - 60_000;
29509
- for (const [k, v] of _gitStatusCache) if (v.at < cutoff) _gitStatusCache.delete(k);
29510
- }
29511
- }
29512
-
29513
- try {
29514
- res.json(await entry.promise);
29515
- } catch (err) {
29516
- res.status(500).json({ error: err.stderr?.toString?.()?.trim?.() || err.message });
29517
- }
29673
+ if (req.query.refresh === '1') gitStatusRunner.invalidate(dir, { refresh: true });
29674
+ res.json(await computeGitStatus(dir, { signal: gitRequestSignal(res) }));
29518
29675
  });
29519
29676
 
29520
- app.post('/api/git/commit', editorOwnerGuard, (req, res) => {
29677
+ app.post('/api/git/commit', editorOwnerGuard, async (req, res) => {
29521
29678
  const { path: dir, message, files } = req.body;
29522
29679
  if (!dir || typeof dir !== 'string' || !message) return res.status(400).json({ error: 'path and message required' });
29523
29680
  if (typeof message !== 'string' || message.trim().length === 0) return res.status(400).json({ error: 'Commit message cannot be empty' });
29524
29681
 
29525
29682
  try {
29526
- gitOutput(dir, ['rev-parse', '--is-inside-work-tree'], { timeout: 3000 });
29683
+ await gitOutput(dir, ['rev-parse', '--is-inside-work-tree'], { timeout: 3000 });
29527
29684
  } catch {
29528
29685
  return res.status(400).json({ error: 'Not a git repository' });
29529
29686
  }
@@ -29535,16 +29692,19 @@ app.post('/api/git/commit', editorOwnerGuard, (req, res) => {
29535
29692
  for (const f of files) {
29536
29693
  if (typeof f !== 'string' || f.includes('..')) return res.status(400).json({ error: 'Invalid file path' });
29537
29694
  }
29538
- const result = spawnSync('git', ['add', '--', ...files], { cwd: dir, env: withGitFallback(process.env), stdio: 'pipe', timeout: 10000 });
29695
+ const result = await gitStatusRunner.runRaw(['add', '--', ...files], { cwd: dir, timeoutMs: 10000 });
29696
+ gitStatusRunner.invalidate(dir);
29539
29697
  if (result.status !== 0) return res.status(500).json({ error: result.stderr?.toString?.()?.trim() || 'Stage failed' });
29540
29698
  } else {
29541
29699
  // Stage all changes
29542
- const result = spawnSync('git', ['add', '-A'], { cwd: dir, env: withGitFallback(process.env), stdio: 'pipe', timeout: 10000 });
29700
+ const result = await gitStatusRunner.runRaw(['add', '-A'], { cwd: dir, timeoutMs: 10000 });
29701
+ gitStatusRunner.invalidate(dir);
29543
29702
  if (result.status !== 0) return res.status(500).json({ error: result.stderr?.toString?.()?.trim() || 'Stage failed' });
29544
29703
  }
29545
29704
 
29546
29705
  // Commit
29547
- const result = spawnSync('git', ['commit', '-m', message.trim()], { cwd: dir, env: withGitFallback(process.env), stdio: 'pipe', timeout: 15000 });
29706
+ const result = await gitStatusRunner.runRaw(['commit', '-m', message.trim()], { cwd: dir, timeoutMs: 15000 });
29707
+ gitStatusRunner.invalidate(dir);
29548
29708
  if (result.status !== 0) {
29549
29709
  const stderr = result.stderr?.toString?.()?.trim() || '';
29550
29710
  const stdout = result.stdout?.toString?.()?.trim() || '';
@@ -29558,32 +29718,32 @@ app.post('/api/git/commit', editorOwnerGuard, (req, res) => {
29558
29718
  }
29559
29719
  });
29560
29720
 
29561
- app.get('/api/git/diff-summary', editorOwnerGuard, (req, res) => {
29721
+ app.get('/api/git/diff-summary', editorOwnerGuard, async (req, res) => {
29562
29722
  const asked = req.query.path;
29563
29723
  if (!asked || typeof asked !== 'string') return res.status(400).json({ error: 'path required' });
29564
29724
  const dir = registeredGitFolder(asked, res);
29565
29725
  if (!dir) return;
29566
29726
 
29567
29727
  try {
29568
- gitOutput(dir, ['rev-parse', '--is-inside-work-tree'], { timeout: 3000 });
29728
+ await gitOutput(dir, ['rev-parse', '--is-inside-work-tree'], { timeout: 3000 });
29569
29729
  } catch {
29570
29730
  return res.status(400).json({ error: 'Not a git repository' });
29571
29731
  }
29572
29732
 
29573
29733
  try {
29574
29734
  // Get diff stat for tracked files
29575
- const stat = gitOutput(dir, ['diff', '--stat', '--stat-width=120']).trim();
29735
+ const stat = (await gitOutput(dir, ['diff', '--stat', '--stat-width=120'])).trim();
29576
29736
 
29577
29737
  // Get diff stat for staged files
29578
- const stagedStat = gitOutput(dir, ['diff', '--cached', '--stat', '--stat-width=120']).trim();
29738
+ const stagedStat = (await gitOutput(dir, ['diff', '--cached', '--stat', '--stat-width=120'])).trim();
29579
29739
 
29580
29740
  // Get untracked files
29581
- const untracked = gitOutput(dir, ['ls-files', '--others', '--exclude-standard']).trim();
29741
+ const untracked = (await gitOutput(dir, ['ls-files', '--others', '--exclude-standard'])).trim();
29582
29742
 
29583
29743
  // Get short diff for context (limited to avoid huge payloads)
29584
- const diff = gitOutput(dir, ['diff', '-U1', '--no-color'], { timeout: 8000, maxBuffer: 64 * 1024 }).substring(0, 4000);
29744
+ const diff = (await gitOutput(dir, ['diff', '-U1', '--no-color'], { timeout: 8000, maxBuffer: 64 * 1024 })).substring(0, 4000);
29585
29745
 
29586
- const stagedDiff = gitOutput(dir, ['diff', '--cached', '-U1', '--no-color'], { timeout: 8000, maxBuffer: 64 * 1024 }).substring(0, 4000);
29746
+ const stagedDiff = (await gitOutput(dir, ['diff', '--cached', '-U1', '--no-color'], { timeout: 8000, maxBuffer: 64 * 1024 })).substring(0, 4000);
29587
29747
 
29588
29748
  // Build a commit message from the changes
29589
29749
  const changedFiles = [];
@@ -29651,7 +29811,7 @@ app.use(createGitDiffRouter({ fileRoots: allowedFileRoots, guard: editorOwnerGua
29651
29811
 
29652
29812
  // ── Git Log ──
29653
29813
 
29654
- app.get('/api/git/log', editorOwnerGuard, (req, res) => {
29814
+ app.get('/api/git/log', editorOwnerGuard, async (req, res) => {
29655
29815
  const asked = req.query.path;
29656
29816
  if (!asked || typeof asked !== 'string') return res.status(400).json({ error: 'path required' });
29657
29817
  const dir = registeredGitFolder(asked, res);
@@ -29659,13 +29819,13 @@ app.get('/api/git/log', editorOwnerGuard, (req, res) => {
29659
29819
  const count = Math.min(Math.max(parseInt(req.query.count) || 10, 1), 50);
29660
29820
 
29661
29821
  try {
29662
- gitOutput(dir, ['rev-parse', '--is-inside-work-tree'], { timeout: 3000 });
29822
+ await gitOutput(dir, ['rev-parse', '--is-inside-work-tree'], { timeout: 3000 });
29663
29823
  } catch {
29664
29824
  return res.status(400).json({ error: 'Not a git repository' });
29665
29825
  }
29666
29826
 
29667
29827
  try {
29668
- const raw = gitOutput(dir, ['log', '--oneline', '--no-decorate', '-n', String(count)]).trim();
29828
+ const raw = (await gitOutput(dir, ['log', '--oneline', '--no-decorate', '-n', String(count)])).trim();
29669
29829
  const commits = raw ? raw.split('\n').map(line => {
29670
29830
  const spaceIdx = line.indexOf(' ');
29671
29831
  return { hash: line.substring(0, spaceIdx), message: line.substring(spaceIdx + 1) };
@@ -29683,17 +29843,17 @@ app.post('/api/git/generate-message', editorOwnerGuard, async (req, res) => {
29683
29843
  if (!dir || typeof dir !== 'string') return res.status(400).json({ error: 'path required' });
29684
29844
 
29685
29845
  try {
29686
- gitOutput(dir, ['rev-parse', '--is-inside-work-tree'], { timeout: 3000 });
29846
+ await gitOutput(dir, ['rev-parse', '--is-inside-work-tree'], { timeout: 3000 });
29687
29847
  } catch {
29688
29848
  return res.status(400).json({ error: 'Not a git repository' });
29689
29849
  }
29690
29850
 
29691
29851
  // Gather diff data — separate try/catch per command so one failure doesn't skip the rest
29692
29852
  let diffText = '', stagedText = '', untrackedList = '', recentCommits = '';
29693
- try { diffText = gitOutput(dir, ['diff', '--no-color'], { timeout: 8000, maxBuffer: 2 * 1024 * 1024 }).substring(0, 4000); } catch {}
29694
- try { stagedText = gitOutput(dir, ['diff', '--cached', '--no-color'], { timeout: 8000, maxBuffer: 2 * 1024 * 1024 }).substring(0, 4000); } catch {}
29695
- try { untrackedList = gitOutput(dir, ['ls-files', '--others', '--exclude-standard']).trim(); } catch {}
29696
- try { recentCommits = gitOutput(dir, ['log', '--oneline', '--no-decorate', '-n', '5'], { timeout: 3000 }).trim(); } catch {}
29853
+ try { diffText = (await gitOutput(dir, ['diff', '--no-color'], { timeout: 8000, maxBuffer: 2 * 1024 * 1024 })).substring(0, 4000); } catch {}
29854
+ try { stagedText = (await gitOutput(dir, ['diff', '--cached', '--no-color'], { timeout: 8000, maxBuffer: 2 * 1024 * 1024 })).substring(0, 4000); } catch {}
29855
+ try { untrackedList = (await gitOutput(dir, ['ls-files', '--others', '--exclude-standard'])).trim(); } catch {}
29856
+ try { recentCommits = (await gitOutput(dir, ['log', '--oneline', '--no-decorate', '-n', '5'], { timeout: 3000 })).trim(); } catch {}
29697
29857
 
29698
29858
  const allDiff = (diffText + '\n' + stagedText).trim();
29699
29859
  if (!allDiff && !untrackedList) {
@@ -29744,8 +29904,8 @@ app.post('/api/git/generate-message', editorOwnerGuard, async (req, res) => {
29744
29904
  // Fallback: heuristic (same logic as /api/git/diff-summary)
29745
29905
  try {
29746
29906
  let stat = '', stagedStat = '';
29747
- try { stat = gitOutput(dir, ['diff', '--stat', '--stat-width=120'], { maxBuffer: 2 * 1024 * 1024 }).trim(); } catch {}
29748
- try { stagedStat = gitOutput(dir, ['diff', '--cached', '--stat', '--stat-width=120'], { maxBuffer: 2 * 1024 * 1024 }).trim(); } catch {}
29907
+ try { stat = (await gitOutput(dir, ['diff', '--stat', '--stat-width=120'], { maxBuffer: 2 * 1024 * 1024 })).trim(); } catch {}
29908
+ try { stagedStat = (await gitOutput(dir, ['diff', '--cached', '--stat', '--stat-width=120'], { maxBuffer: 2 * 1024 * 1024 })).trim(); } catch {}
29749
29909
  const changedFiles = [];
29750
29910
  for (const line of (stat + '\n' + stagedStat).split('\n').filter(l => l.includes('|'))) {
29751
29911
  const m = line.match(/^\s*(.+?)\s*\|/);
@@ -29823,7 +29983,7 @@ app.put('/api/cli/config', (req, res) => {
29823
29983
  app.post('/api/cli/detect/:profileId', async (req, res) => {
29824
29984
  try {
29825
29985
  const { profileId } = req.params;
29826
- if (!['claude-code', 'codex', 'gemini', 'opencode'].includes(profileId)) return res.status(400).json({ ok: false, error: 'Unknown profile' });
29986
+ if (!['claude-code', 'codex', 'antigravity', 'opencode'].includes(profileId)) return res.status(400).json({ ok: false, error: 'Unknown profile' });
29827
29987
  const status = await cliResolver.resolve(profileId, { force: true, configured: null });
29828
29988
  res.json({
29829
29989
  ok: true,
@@ -29873,7 +30033,7 @@ function stripAnsi(str) {
29873
30033
  // stdin: false — message appended as the last CLI arg (flag takes a value)
29874
30034
  const CLI_SIDECAR_CONFIG = {
29875
30035
  'claude-code': { args: ['-p', '--verbose'], stdin: true }, // -p = non-interactive mode; reads prompt from stdin
29876
- 'gemini': { args: ['-p'], stdin: false }, // -p <value>; PowerShell passes correctly on Windows
30036
+ 'antigravity': { args: ['-p'], stdin: false }, // agy -p <prompt>: one print-mode turn
29877
30037
  'codex': { args: ['-q'], stdin: false }, // -q <msg>; flag takes a value
29878
30038
  'shell': null,
29879
30039
  };
@@ -29888,7 +30048,7 @@ const CLI_LIVE_CONFIG = {
29888
30048
  maxIdleMs: 120000, // 2 min absolute max wait
29889
30049
  useBracketedPaste: true,
29890
30050
  },
29891
- 'gemini': {
30051
+ 'antigravity': {
29892
30052
  promptPatterns: [/>\s*$/m, /\u276F\s*$/m],
29893
30053
  idleMs: 3000,
29894
30054
  maxIdleMs: 120000,
@@ -30005,7 +30165,7 @@ function spawnSidecarProcess(linkId, sessionId, message) {
30005
30165
  const sidecarConfig = CLI_SIDECAR_CONFIG[profile];
30006
30166
  if (!sidecarConfig) throw new Error(`No sidecar config for profile: ${profile}`);
30007
30167
 
30008
- // Get the CLI command from config (e.g. 'claude', 'gemini', 'codex')
30168
+ // Get the CLI command from config (e.g. 'claude', 'agy', 'codex')
30009
30169
  const cliConfig = loadCliConfig();
30010
30170
  const command = (cliConfig[profile]?.command || '').trim();
30011
30171
  if (!command) throw new Error(`No CLI command configured for ${profile}`);
@@ -30025,7 +30185,7 @@ function spawnSidecarProcess(linkId, sessionId, message) {
30025
30185
  // stdin-based: args are fixed, message piped after spawn (e.g. Claude Code)
30026
30186
  } else if (IS_WIN) {
30027
30187
  // Windows: cmd.exe shell quoting mangles multi-word args (splits by spaces),
30028
- // causing CLIs like Gemini to misparse -p "hello world" as -p + positional "hello" "world".
30188
+ // causing CLIs that take a value after -p (agy) to misparse -p "hello world" as -p + positional "hello" "world".
30029
30189
  // Use PowerShell which correctly passes $msg as a single argument value.
30030
30190
  tmpFile = join(os.tmpdir(), `synabun-relay-${randomBytes(4).toString('hex')}.txt`);
30031
30191
  writeFileSync(tmpFile, message, 'utf-8');
@@ -30837,7 +30997,7 @@ async function acquireSharedBrowserTab({ requestedBrowserSessionId = null, owner
30837
30997
  profileMode: result.profileMode, profileSource: result.profileSource,
30838
30998
  });
30839
30999
  } catch (err) {
30840
- createError = err.message;
31000
+ createError = browserStartFailure(err);
30841
31001
  loopLog(ownerKey, logTag, 'strategy 2 FAILED: createBrowserSession threw', { err: err.message, stack: err.stack?.slice(0, 600) });
30842
31002
  }
30843
31003
  }
@@ -30847,8 +31007,9 @@ async function acquireSharedBrowserTab({ requestedBrowserSessionId = null, owner
30847
31007
  // run's log, lastRunResult and the schedule:failed toast.
30848
31008
  return {
30849
31009
  error: createError
30850
- ? `Could not start the shared browser session: ${createError}`
31010
+ ? `Could not start the shared browser session: ${createError.error}`
30851
31011
  : 'Could not start the shared browser session. Open Browser Settings and verify the selected profile can launch.',
31012
+ ...(createError?.code ? { code: createError.code, offenders: createError.offenders } : {}),
30852
31013
  };
30853
31014
  }
30854
31015
 
@@ -32013,34 +32174,72 @@ async function installMoneyGuardRoute(ctx) {
32013
32174
  // still aborted. Managed contexts also block WS through Playwright. MoreLogin
32014
32175
  // retains native WS only behind the verified server authentication boundary and
32015
32176
  // credentials confined to this server, never an environment, page or log.
32016
- function protectNativeServeContext(ctx) {
32017
- return nativeServeBoundary.protect(ctx, {
32018
- ports: liveOpencodeServePorts(),
32019
- unverifiedRecords: opencodeServeRecords.list().filter(record =>
32020
- !opencodeServeRecords.ownedHere(record) && opencodeServeRecords.verifySync(record) !== 'gone'),
32021
- });
32177
+ // Records of serves this server did not start that may still run: each one could answer a page's WebSocket.
32178
+ function unverifiedServeRecords() {
32179
+ return opencodeServeRecords.list().filter(record =>
32180
+ !opencodeServeRecords.ownedHere(record) && opencodeServeRecords.verifySync(record) !== 'gone')
32181
+ .map(record => ({ ...record, claimable: (() => { try { return opencodeServeRecords.claimable(record); } catch { return false; } })() }));
32182
+ }
32183
+ const NATIVE_SERVE_WAIT_MS = Number(process.env.SYNABUN_NATIVE_SERVE_WAIT_MS) || 20_000;
32184
+ const NATIVE_SERVE_SWEEP_MS = Number(process.env.SYNABUN_NATIVE_SERVE_SWEEP_MS) || 10_000;
32185
+ // Left of a request's deadline for the answer itself (the MCP client gives a session create 70 s).
32186
+ const NATIVE_SERVE_REPLY_MARGIN_MS = 5_000;
32187
+ // Before refusing, repair what SynaBun can itself, bounded: stop the serves an ended SynaBun left running (the records'
32188
+ // own sweep, which signals only a provably-SynaBun process whose owner has ended; at most NATIVE_SERVE_SWEEP_MS, in the
32189
+ // background while the rest is read) and wait for SynaBun's own verified serves still starting (at most
32190
+ // NATIVE_SERVE_WAIT_MS), both inside the request's deadline. The context is held by the boundary from before the
32191
+ // first look (nativeServeBoundary.attach), so no unverified serve starts meanwhile. Nothing is cached: every attempt
32192
+ // reads the live state again. Authentication is never relaxed: what is left is named.
32193
+ async function protectNativeServeContext(ctx, { deadline = Infinity, signal = null } = {}) {
32194
+ try {
32195
+ return await nativeServeBoundary.attach(ctx, {
32196
+ state: () => ({ ports: [...liveOpencodeServePorts()], unverifiedRecords: unverifiedServeRecords() }),
32197
+ sweep: () => opencodeServeRecords.sweep(),
32198
+ waitMs: NATIVE_SERVE_WAIT_MS, sweepMs: NATIVE_SERVE_SWEEP_MS,
32199
+ deadline: Number.isFinite(deadline) ? deadline - NATIVE_SERVE_REPLY_MARGIN_MS : Infinity, signal,
32200
+ });
32201
+ } catch (error) {
32202
+ if (error?.offenders) console.warn(`[browser] native serve boundary refused: ${JSON.stringify(error.offenders)}`);
32203
+ throw error;
32204
+ }
32205
+ }
32206
+
32207
+ // The answer a failed browser start gives its caller: the sentence, and for the native serve boundary its code and
32208
+ // the serves it names (narrowed: never a token or a whole record).
32209
+ function browserStartFailure(err) {
32210
+ const out = { error: err?.message || String(err) };
32211
+ if (err?.code === 'NATIVE_SERVE_UNVERIFIED') { out.code = err.code; out.offenders = narrowOffenders(err.offenders); }
32212
+ return out;
32022
32213
  }
32023
32214
 
32024
- async function installServePortGuard(ctx) {
32215
+ async function installServePortGuard(ctx, { deadline = Infinity, signal = null } = {}) {
32025
32216
  if (!ctx || ctx.__servePortGuardInstalled) return;
32026
32217
  // routeWebSocket installs page bindings and replaces WebSocket on navigation,
32027
32218
  // even with an empty matcher. Native contexts rely on the verified server's
32028
32219
  // authentication; never silently route or drop protection for unknown serves.
32029
32220
  const native = usesNativeBrowserNetworkGuard(ctx);
32030
- const release = native ? protectNativeServeContext(ctx) : null;
32031
32221
  const refused = (url) => { try { return !!browserServePortDenial(url?.href ?? url, liveOpencodeServePorts()); } catch { return false; } };
32222
+ const installRule = () => setBrowserNetworkRule(ctx, 'serve-port', {
32223
+ patterns: SERVE_PORT_PATTERNS,
32224
+ decide: request => refused(request.url) ? { abort: true } : null,
32225
+ });
32226
+ if (native) {
32227
+ // Held first (assertLaunch refuses an unverified start from here on), then the HTTP rule, then the bounded check.
32228
+ const release = nativeServeBoundary.reserve(ctx);
32229
+ try { await installRule(); } catch (error) {
32230
+ release();
32231
+ throw new Error(`Failed to install the OpenCode serve guard: ${error.message}`);
32232
+ }
32233
+ await protectNativeServeContext(ctx, { deadline, signal });
32234
+ ctx.__servePortGuardInstalled = true;
32235
+ return;
32236
+ }
32032
32237
  try {
32033
- await setBrowserNetworkRule(ctx, 'serve-port', {
32034
- patterns: SERVE_PORT_PATTERNS,
32035
- decide: request => refused(request.url) ? { abort: true } : null,
32036
- });
32037
- if (!native) {
32038
- if (typeof ctx.routeWebSocket !== 'function') throw new Error('WebSocket routing is unavailable');
32039
- await ctx.routeWebSocket(refused, ws => Promise.resolve(ws.close({ code: 1008, reason: 'SynaBun: not an address the browser opens' })).catch(() => {}));
32040
- }
32238
+ await installRule();
32239
+ if (typeof ctx.routeWebSocket !== 'function') throw new Error('WebSocket routing is unavailable');
32240
+ await ctx.routeWebSocket(refused, ws => Promise.resolve(ws.close({ code: 1008, reason: 'SynaBun: not an address the browser opens' })).catch(() => {}));
32041
32241
  ctx.__servePortGuardInstalled = true;
32042
32242
  } catch (error) {
32043
- release?.();
32044
32243
  throw new Error(`Failed to install the OpenCode serve guard: ${error.message}`);
32045
32244
  }
32046
32245
  }
@@ -32391,8 +32590,10 @@ async function createBrowserSession(options = {}) {
32391
32590
  || savedCfg.connectMode === 'morelogin';
32392
32591
  if (_wantMoreLogin) {
32393
32592
  try {
32394
- if (!moreLogin.isMoreLoginInstalled().installed) {
32395
- throw new Error('MoreLogin desktop app is not installed');
32593
+ // An install outside the known folders (a custom folder on Windows) still
32594
+ // counts when its local API answers.
32595
+ if (!moreLogin.isMoreLoginInstalled().installed && !(await moreLogin.probeMoreLoginApi({ savedCfg })).running) {
32596
+ throw new Error(`MoreLogin desktop app was not found and its local API does not answer on port ${moreLogin.getMoreLoginPort(savedCfg)}`);
32396
32597
  }
32397
32598
  // Installed but possibly not running (e.g. an autonomous schedule fires with
32398
32599
  // nobody at the machine to open it) — auto-launch and wait for its local API
@@ -32718,7 +32919,7 @@ async function createBrowserSession(options = {}) {
32718
32919
  const startServeDenial = browserServePortDenial(startUrl, liveOpencodeServePorts());
32719
32920
  if (startServeDenial) throw new Error(startServeDenial);
32720
32921
  await installMoneyGuardRoute(context);
32721
- await installServePortGuard(context);
32922
+ await installServePortGuard(context, { deadline: options.browserRequest?.deadline, signal: options.browserRequest?.signal });
32722
32923
  if (typeof _pageSession !== 'undefined') {
32723
32924
  const marked = await _pageSession.markWorkerBrowserContext(context, { port: PORT, hostNames: machineNames.names() });
32724
32925
  if (!marked) throw new Error('Cannot protect browser owner authority: local worker marker failed');
@@ -33730,30 +33931,41 @@ app.use('/api/browser', (req, res, next) => { res.setHeader('X-Synabun-Browser-D
33730
33931
  // action, a new tab, CDP). Closing it, or listing its tabs, stays allowed.
33731
33932
  app.use('/api/browser/sessions/:id', (req, res, next) => {
33732
33933
  const protectedContext = browserSessions.get(req.params.id)?.context;
33733
- const unsafe = protectedContext?.__browserNetworkGuardError;
33734
- if (unsafe && req.method !== 'DELETE' && !(req.method === 'GET' && req.path === '/tabs')) {
33735
- return res.status(503).json({ error: `Browser network protection unavailable: ${unsafe}`, networkGuard: true });
33736
- }
33737
- // Also cancel requests already waiting/probing when the protection disappears.
33738
- // Cancellation prevents their next page action; an action that already began
33739
- // retains the existing truthful "outcome uncertain" response semantics.
33740
- if (req.method !== 'DELETE' && protectedContext?.on) {
33741
- const cancel = reason => req.browserRequest?.cancelForProtection?.(reason);
33742
- protectedContext.on('synabun-network-guard-fault', cancel);
33743
- const release = () => protectedContext.removeListener('synabun-network-guard-fault', cancel);
33744
- res.once('finish', release);
33745
- res.once('close', release);
33746
- }
33747
- if (!req.get('X-Synabun-Terminal') || req.method === 'DELETE' || (req.method === 'GET' && req.path === '/tabs')) return next();
33748
- const session = browserSessions.get(req.params.id);
33749
- if (!session) return next();
33750
- const cfg = loadBrowserConfig();
33751
- if (cfg.connectMode !== 'morelogin' || sessionServesMoreLoginDefault(session, cfg)) return next();
33752
- const envId = moreLoginDefaultEnvId(cfg);
33753
- res.status(409).json({
33754
- error: `Session ${req.params.id} is not the default browser (${session._profileMode || 'unknown'} mode${session._mlEnvId ? `, MoreLogin env ${session._mlEnvId}` : ''}; the default is MoreLogin${envId ? ` env ${envId}` : ''}), so SynaBun's browser tools do not act on it. Leave sessionId out: browser_navigate opens the page in the default browser.`,
33755
- code: 'NOT_DEFAULT_BROWSER',
33756
- });
33934
+ const exempt = req.method === 'DELETE' || (req.method === 'GET' && req.path === '/tabs');
33935
+ const gate = () => {
33936
+ const unsafe = protectedContext?.__browserNetworkGuardError;
33937
+ if (unsafe && !exempt) {
33938
+ return res.status(503).json({ error: `Browser network protection unavailable: ${unsafe}`, networkGuard: true });
33939
+ }
33940
+ // Also cancel requests already waiting/probing when the protection disappears.
33941
+ // Cancellation prevents their next page action; an action that already began
33942
+ // retains the existing truthful "outcome uncertain" response semantics.
33943
+ if (req.method !== 'DELETE' && protectedContext?.on) {
33944
+ const cancel = reason => req.browserRequest?.cancelForProtection?.(reason);
33945
+ protectedContext.on('synabun-network-guard-fault', cancel);
33946
+ const release = () => protectedContext.removeListener('synabun-network-guard-fault', cancel);
33947
+ res.once('finish', release);
33948
+ res.once('close', release);
33949
+ }
33950
+ if (!req.get('X-Synabun-Terminal') || exempt) return next();
33951
+ const session = browserSessions.get(req.params.id);
33952
+ if (!session) return next();
33953
+ const cfg = loadBrowserConfig();
33954
+ if (cfg.connectMode !== 'morelogin' || sessionServesMoreLoginDefault(session, cfg)) return next();
33955
+ const envId = moreLoginDefaultEnvId(cfg);
33956
+ res.status(409).json({
33957
+ error: `Session ${req.params.id} is not the default browser (${session._profileMode || 'unknown'} mode${session._mlEnvId ? `, MoreLogin env ${session._mlEnvId}` : ''}; the default is MoreLogin${envId ? ` env ${envId}` : ''}), so SynaBun's browser tools do not act on it. Leave sessionId out: browser_navigate opens the page in the default browser.`,
33958
+ code: 'NOT_DEFAULT_BROWSER',
33959
+ });
33960
+ };
33961
+ // A faulted network guard re-arms on demand: a fresh protected attachment replaces
33962
+ // the broken one before this request runs, so a session never needs a server
33963
+ // restart to recover. Until a re-arm succeeds every action stays refused.
33964
+ if (protectedContext?.__browserNetworkGuardError && !exempt) {
33965
+ recoverBrowserNetworkGuard(protectedContext).then(gate).catch(next);
33966
+ return;
33967
+ }
33968
+ return gate();
33757
33969
  });
33758
33970
  // Read-only semantic context for the browser judgments (the judgments themselves run in the MCP layer).
33759
33971
  app.use('/api/browser', createBrowserAssistRouter({ browserSessions, getTargetPage, normalizeSelector }));
@@ -33822,6 +34034,8 @@ app.post('/api/browser/sessions', async (req, res) => {
33822
34034
  };
33823
34035
  const result = await createBrowserSession({
33824
34036
  url, width, height, screenWidth, screenHeight, deviceScaleFactor, timezone, _realHeaders,
34037
+ // Its deadline bounds the native serve check (installServePortGuard) inside the caller's timeout.
34038
+ browserRequest: req.browserRequest,
33825
34039
  // Optional per-session override to target a specific MoreLogin env (multi-identity)
33826
34040
  ...(connectMode === 'morelogin' ? { connectMode } : {}),
33827
34041
  ...(moreloginEnvId ? { moreloginEnvId: String(moreloginEnvId) } : {}),
@@ -33848,7 +34062,7 @@ app.post('/api/browser/sessions', async (req, res) => {
33848
34062
  });
33849
34063
  } catch (err) {
33850
34064
  console.error('Browser session create error:', err.message);
33851
- res.status(500).json({ error: err.message });
34065
+ res.status(500).json(browserStartFailure(err));
33852
34066
  }
33853
34067
  });
33854
34068
 
@@ -33873,7 +34087,7 @@ app.post('/api/browser/acquire', async (req, res) => {
33873
34087
  const clientId = req.body?.clientId;
33874
34088
  if (!clientId) return res.status(400).json({ error: 'clientId is required' });
33875
34089
  const acq = await acquireSharedBrowserTab({ ownerKey: clientId, ownerType: 'interactive', logTag: 'acquire:interactive' });
33876
- if (acq.error) return res.status(500).json({ error: acq.error });
34090
+ if (acq.error) return res.status(500).json({ error: acq.error, ...(acq.code ? { code: acq.code, offenders: acq.offenders } : {}) });
33877
34091
  res.json({ ok: true, sessionId: acq.browserSessionId, tabId: acq.browserTabId || null });
33878
34092
  } catch (err) {
33879
34093
  res.status(500).json({ error: err.message });
@@ -35560,7 +35774,7 @@ app.post('/api/browser/sessions/:id/upload', async (req, res) => {
35560
35774
  });
35561
35775
 
35562
35776
  // Browser config endpoint
35563
- app.get('/api/browser/config', (req, res) => {
35777
+ app.get('/api/browser/config', async (req, res) => {
35564
35778
  const config = loadBrowserConfig();
35565
35779
  const detected = findBrowserExecutable();
35566
35780
  // Check if Chrome is currently running (informs profile mirroring warnings)
@@ -35581,7 +35795,11 @@ app.get('/api/browser/config', (req, res) => {
35581
35795
  let attachSeeded = false;
35582
35796
  try { attachSeeded = existsSync(getAttachProfileRoot()); } catch {}
35583
35797
  let moreloginInstalled = false;
35584
- try { moreloginInstalled = moreLogin.isMoreLoginInstalled().installed; } catch {}
35798
+ // The exe in a known folder, or a local API that answers (a custom install folder): a short probe only when needed.
35799
+ try {
35800
+ moreloginInstalled = moreLogin.isMoreLoginPresent()
35801
+ || (await moreLogin.probeMoreLoginApi({ savedCfg: config, timeout: 1500 })).running;
35802
+ } catch {}
35585
35803
  const unreadable = browserConfigUnreadable(config);
35586
35804
  res.json({ config, detectedPath: detected, chromeRunning, attachSeeded, moreloginInstalled, ...(unreadable && { configError: `browser-config.json is unreadable (${unreadable}); saving these settings replaces it` }) });
35587
35805
  });
@@ -35626,12 +35844,20 @@ app.get('/api/browser/detect-profiles', (req, res) => {
35626
35844
  app.get('/api/browser/morelogin-envs', async (req, res) => {
35627
35845
  try {
35628
35846
  const cfg = loadBrowserConfig();
35629
- const installed = moreLogin.isMoreLoginInstalled().installed;
35630
35847
  const port = moreLogin.getMoreLoginPort(cfg);
35631
- if (!installed) return res.json({ ok: true, installed: false, running: false, port, envs: [] });
35848
+ // Probe first: the install-folder check knows only the default folders, and an
35849
+ // answering local API proves the app is there wherever it was installed.
35632
35850
  const probe = await moreLogin.probeMoreLoginApi({ savedCfg: cfg, port });
35851
+ const installed = probe.running || moreLogin.isMoreLoginInstalled().installed;
35852
+ if (!installed) return res.json({ ok: true, installed: false, running: false, port, envs: [], error: probe.error || null });
35633
35853
  if (!probe.running) return res.json({ ok: true, installed: true, running: false, port, envs: [], error: probe.error || null });
35854
+ // The API answered with an error (signature required, API off): say why, never an empty list.
35855
+ if (probe.apiError) return res.json({ ok: false, installed: true, running: true, port, envs: [], error: probe.apiError });
35634
35856
  const envs = await moreLogin.listProfiles({ savedCfg: cfg, port });
35857
+ // The API counted profiles but none could be read: a shape problem, never "no profiles yet".
35858
+ if (probe.count > 0 && !envs.length) {
35859
+ return res.json({ ok: false, installed: true, running: true, port, envs: [], error: `MoreLogin reports ${probe.count} profile(s) but SynaBun could not read them from its answer; a newer MoreLogin may have changed its API.` });
35860
+ }
35635
35861
  res.json({
35636
35862
  ok: true, installed: true, running: true, port, envs,
35637
35863
  // Observed website-policy blocks — MoreLogin's local API never exposes the
@@ -35882,10 +36108,11 @@ const httpServer = app.listen(PORT, async () => {
35882
36108
  // Activation is an explicit user choice via Settings → Browser / the MoreLogin tab.
35883
36109
  setTimeout(async () => {
35884
36110
  try {
35885
- if (!moreLogin.isMoreLoginInstalled().installed) return;
35886
- const cfg = loadBrowserConfig();
36111
+ const mlBootCfg = loadBrowserConfig();
36112
+ const cfg = mlBootCfg;
35887
36113
  const port = moreLogin.getMoreLoginPort(cfg);
35888
36114
  const probe = await moreLogin.probeMoreLoginApi({ savedCfg: cfg, port });
36115
+ if (!probe.running && !moreLogin.isMoreLoginInstalled().installed) return;
35889
36116
  const isDefault = cfg.connectMode === 'morelogin';
35890
36117
  console.log(` MoreLogin: detected${probe.running ? ` · API on :${port} (${probe.count} profile(s))` : ' · app not running'}${isDefault ? ' · default AI browser' : ''}`);
35891
36118
  } catch { /* best-effort */ }
@@ -35949,6 +36176,12 @@ const httpServer = app.listen(PORT, async () => {
35949
36176
  if (oc.updated > 0) console.log(` Session cache (opencode): indexed ${oc.updated}/${oc.sessions}`);
35950
36177
  } catch (err) { console.warn(' Session cache (opencode) error:', err.message); }
35951
36178
  },
36179
+ () => {
36180
+ const agy = refreshAntigravityCatalog({ force: true });
36181
+ if (agy?.updated > 0) console.log(` Session cache (antigravity): indexed ${agy.updated}/${agy.sessions}`);
36182
+ // Runs and brains end on their own: their conversations are picked up by this pass, every five minutes.
36183
+ setInterval(() => refreshAntigravityCatalog({ force: true }), 5 * 60 * 1000).unref?.();
36184
+ },
35952
36185
  ];
35953
36186
  const next = () => {
35954
36187
  const pass = passes.shift();
@@ -35988,6 +36221,18 @@ const httpServer = app.listen(PORT, async () => {
35988
36221
  console.warn(' Hook dedup warning:', err.message);
35989
36222
  }
35990
36223
 
36224
+ // Antigravity replaced Gemini CLI: what was saved under `gemini` (a CLI path, a keybind, loop templates and
36225
+ // schedules, the launch profile and resume tab in the UI state) is read as Antigravity's from this boot on
36226
+ // (lib/antigravity/migrate-gemini.js). Idempotent; a file it cannot read is left as it is. Never stops the boot.
36227
+ try {
36228
+ const migrated = migrateGeminiFiles({ dataHome: DATA_HOME, log: (line) => console.log(` ${line}`) });
36229
+ if (migrated.changed.includes('cli-config.json')) { _cliConfigCache = null; cliResolver.invalidate(); }
36230
+ const ui = loadUiState();
36231
+ if (migrateGeminiUiState(ui)) saveUiState(ui);
36232
+ } catch (err) {
36233
+ console.warn(' Antigravity migration warning:', err.message);
36234
+ }
36235
+
35991
36236
  // Keep the installed SynaBun rules current: an untouched copy of an older
35992
36237
  // ruleset is rewritten (unless the user switched that off), an edited copy is
35993
36238
  // left alone, a deleted one counts as an opt-out. Never installs for a tool
@@ -36003,6 +36248,16 @@ const httpServer = app.listen(PORT, async () => {
36003
36248
  console.warn(' Rules reconcile warning:', err.message);
36004
36249
  }
36005
36250
 
36251
+ // SynaBun's Antigravity homes (lib/antigravity/home.js): a run's home lives a week past its last use (a run resumes
36252
+ // within minutes of a restart), an Assistant brain's two months (its conversation can be picked up later).
36253
+ try {
36254
+ const root = antigravityHomesRoot(DATA_HOME);
36255
+ const removed = [...pruneAntigravityHomes(root, { kinds: ['run', 'models'], keep: antigravityPanel.activeHomes() }), ...pruneAntigravityHomes(root, { kinds: ['brain'], maxAgeMs: 60 * 24 * 60 * 60_000 })];
36256
+ if (removed.length) console.log(` Antigravity: removed ${removed.length} unused SynaBun home${removed.length === 1 ? '' : 's'}`);
36257
+ } catch (err) {
36258
+ console.warn(' Antigravity homes warning:', err.message);
36259
+ }
36260
+
36006
36261
  // Clean up orphaned loop state files from previous server runs.
36007
36262
  // After a restart, PTY sessions are gone but loop files remain with awaitingNext: true.
36008
36263
  // No terminal session exists to drive them, so delete them to prevent ghost loops.
@@ -36134,7 +36389,7 @@ httpServer.on('upgrade', function onUpgrade(req, socket, head) {
36134
36389
  if (guest) {
36135
36390
  if (url.pathname.startsWith('/ws/terminal/') && !invitePermissions.terminal) { socket.destroy(); return; }
36136
36391
  if (url.pathname.startsWith('/ws/browser/') && !invitePermissions.browser) { socket.destroy(); return; }
36137
- if (['/ws/claude-skin', '/ws/codex-skin', '/ws/opencode-v2'].includes(url.pathname) || url.pathname.startsWith('/ws/assistant/')) {
36392
+ if (['/ws/claude-skin', '/ws/codex-skin', '/ws/opencode-v2', '/ws/antigravity'].includes(url.pathname) || url.pathname.startsWith('/ws/assistant/')) {
36138
36393
  socket.destroy();
36139
36394
  return;
36140
36395
  }
@@ -36165,7 +36420,7 @@ httpServer.on('upgrade', function onUpgrade(req, socket, head) {
36165
36420
  return;
36166
36421
  }
36167
36422
 
36168
- if (url.pathname.startsWith('/ws/browser/') || url.pathname.startsWith('/ws/assistant/') || url.pathname === '/ws/whiteboard' || url.pathname === '/ws/cards' || url.pathname === '/ws/sync' || url.pathname === '/ws/claude-skin' || url.pathname === '/ws/codex-skin' || url.pathname === '/ws/opencode-v2' || url.pathname === '/ws/sessions') {
36423
+ if (url.pathname.startsWith('/ws/browser/') || url.pathname.startsWith('/ws/assistant/') || url.pathname === '/ws/whiteboard' || url.pathname === '/ws/cards' || url.pathname === '/ws/sync' || url.pathname === '/ws/claude-skin' || url.pathname === '/ws/codex-skin' || url.pathname === '/ws/opencode-v2' || url.pathname === '/ws/antigravity' || url.pathname === '/ws/sessions') {
36169
36424
  wss.handleUpgrade(req, socket, head, (ws) => {
36170
36425
  wss.emit('connection', ws, req);
36171
36426
  });
@@ -36232,6 +36487,12 @@ wss.on('connection', (ws, req) => {
36232
36487
  return;
36233
36488
  }
36234
36489
 
36490
+ // ── Route: Antigravity side panel (Google's agy over ACP) ──
36491
+ if (url.pathname === '/ws/antigravity') {
36492
+ antigravityPanel.handleSocket(ws);
36493
+ return;
36494
+ }
36495
+
36235
36496
  // ── Terminal sessions ── are handed off to the pty-host in the upgrade
36236
36497
  // handler (lib/pty-host); nothing else should reach this point.
36237
36498
  try { ws.close(1008, 'Unsupported WebSocket path'); } catch {}
@@ -36816,11 +37077,15 @@ process.on('exit', () => {
36816
37077
  try { stopOpencodeServer(); } catch {}
36817
37078
  try { editorCompletionServe.stop('exit'); } catch {}
36818
37079
  try { _claudeBridge?.shutdownAllBridges?.(); } catch {}
37080
+ try { antigravityPanel.closeAll(); } catch {}
36819
37081
  try { saveSessionSnapshot(); } catch {}
36820
37082
  try { flushUiStateSync(); } catch {}
36821
37083
  try { flushWhiteboardSync(); } catch {}
36822
37084
  try { flushCostDataSync(); } catch {}
36823
37085
  try { memoryMap.flushSync(); } catch {}
37086
+ // The Assistant's session records (a rename among them): this handler runs before the Assistant's own
37087
+ // exit handler, which the SIGKILL below never lets run.
37088
+ try { _assistantRuntime?.flushIndex?.(); } catch {}
36824
37089
  if (_preserveExitCode) return;
36825
37090
  try { process.kill(process.pid, 'SIGKILL'); } catch {}
36826
37091
  });