synabun 2.1.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (317) hide show
  1. package/CHANGELOG.md +121 -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 +501 -39
  71. package/neural-interface/i18n/pt-BR.json +501 -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-budget-planner.js +7 -3
  88. package/neural-interface/lib/assistant-budget-split.js +42 -15
  89. package/neural-interface/lib/assistant-catalog.js +76 -9
  90. package/neural-interface/lib/assistant-config.js +6 -3
  91. package/neural-interface/lib/assistant-dispatch.js +207 -49
  92. package/neural-interface/lib/assistant-envelope.js +153 -1
  93. package/neural-interface/lib/assistant-evidence.js +80 -0
  94. package/neural-interface/lib/assistant-hidden-models.js +7 -5
  95. package/neural-interface/lib/assistant-persona.js +16 -11
  96. package/neural-interface/lib/assistant-plan-permissions.js +15 -0
  97. package/neural-interface/lib/assistant-router.js +237 -35
  98. package/neural-interface/lib/assistant-runtime.js +158 -12
  99. package/neural-interface/lib/assistant-task-prompt.js +10 -8
  100. package/neural-interface/lib/assistant-tools.js +2 -2
  101. package/neural-interface/lib/assistant-usage.js +29 -0
  102. package/neural-interface/lib/backup-service.js +6 -1
  103. package/neural-interface/lib/browser-guard-cdp.js +5 -4
  104. package/neural-interface/lib/browser-network-guard.js +274 -82
  105. package/neural-interface/lib/browser-tool-policy.js +12 -7
  106. package/neural-interface/lib/claude-agent-bridge.js +7 -0
  107. package/neural-interface/lib/claude-history-cache.js +210 -0
  108. package/neural-interface/lib/claude-model-catalog.js +5 -1
  109. package/neural-interface/lib/cli-install-api.js +1 -1
  110. package/neural-interface/lib/cli-install.js +160 -31
  111. package/neural-interface/lib/cli-resolver.js +9 -2
  112. package/neural-interface/lib/cli-update-planner.js +25 -15
  113. package/neural-interface/lib/codex-rollout-reader.js +52 -0
  114. package/neural-interface/lib/codex-worker-app-server.js +25 -6
  115. package/neural-interface/lib/editor-git-api.js +3 -20
  116. package/neural-interface/lib/exec-loop-command.js +9 -2
  117. package/neural-interface/lib/file-explorer-api.js +74 -40
  118. package/neural-interface/lib/file-explorer-watch.js +9 -3
  119. package/neural-interface/lib/git-bash/install.js +52 -6
  120. package/neural-interface/lib/git-bash/manager.js +59 -17
  121. package/neural-interface/lib/git-bash/pins.js +12 -0
  122. package/neural-interface/lib/git-read.js +2 -2
  123. package/neural-interface/lib/git-status-runner.js +390 -0
  124. package/neural-interface/lib/git-status-watchdog.js +40 -0
  125. package/neural-interface/lib/greeting/budgets.js +1 -0
  126. package/neural-interface/lib/greeting/engine.js +2 -2
  127. package/neural-interface/lib/greeting/sources.js +10 -12
  128. package/neural-interface/lib/greeting/writer.js +1 -1
  129. package/neural-interface/lib/mcp-client-config.js +37 -6
  130. package/neural-interface/lib/mcp-external-servers.js +13 -4
  131. package/neural-interface/lib/mcp-installer.js +2 -1
  132. package/neural-interface/lib/memory-session-capture.js +17 -3
  133. package/neural-interface/lib/morelogin.js +147 -25
  134. package/neural-interface/lib/native-loop-providers.js +420 -21
  135. package/neural-interface/lib/native-loop-runtime.js +23 -2
  136. package/neural-interface/lib/opencode-native-boundary.js +11 -2
  137. package/neural-interface/lib/owner-authority.js +1 -1
  138. package/neural-interface/lib/path-confine.js +12 -9
  139. package/neural-interface/lib/peer-identity.js +32 -2
  140. package/neural-interface/lib/pty-host/core.js +28 -1
  141. package/neural-interface/lib/pty-host/lifetime.js +12 -0
  142. package/neural-interface/lib/pty-host/manager.js +12 -1
  143. package/neural-interface/lib/remote-policy.js +1 -1
  144. package/neural-interface/lib/request-gate.js +3 -0
  145. package/neural-interface/lib/rulesets/api.js +6 -5
  146. package/neural-interface/lib/rulesets/fs-safe.js +1 -1
  147. package/neural-interface/lib/rulesets/installer.js +89 -10
  148. package/neural-interface/lib/rulesets/render.js +1 -1
  149. package/neural-interface/lib/session-cache-builder.js +64 -1
  150. package/neural-interface/lib/skills/catalog.js +5 -1
  151. package/neural-interface/lib/skills/creation-brief.js +1 -1
  152. package/neural-interface/lib/skills/creation-sources.js +19 -1
  153. package/neural-interface/lib/skills/deploy.js +9 -4
  154. package/neural-interface/lib/skills/draft-confinement.js +3 -1
  155. package/neural-interface/lib/skills/eval-confinement.js +3 -1
  156. package/neural-interface/lib/skills/eval-trace.js +16 -0
  157. package/neural-interface/lib/skills/export.js +5 -2
  158. package/neural-interface/lib/skills/known-bridge-digests.json +58 -4
  159. package/neural-interface/lib/skills/ownership.js +1 -1
  160. package/neural-interface/lib/skills/roots.js +10 -3
  161. package/neural-interface/lib/skills/runtime-checks.js +6 -3
  162. package/neural-interface/lib/skills/service.js +15 -7
  163. package/neural-interface/lib/skills/telemetry.js +6 -0
  164. package/neural-interface/lib/synabun-version.js +1 -1
  165. package/neural-interface/lib/system-activity.js +1 -1
  166. package/neural-interface/lib/whatsapp/brain.js +7 -3
  167. package/neural-interface/lib/whatsapp/cards.js +22 -9
  168. package/neural-interface/lib/whatsapp/config.js +1 -1
  169. package/neural-interface/lib/worker-confinement.js +77 -1
  170. package/neural-interface/lib/worker-question.js +74 -0
  171. package/neural-interface/lib/worker-resume.js +13 -4
  172. package/neural-interface/lib/worker-sandbox.js +46 -16
  173. package/neural-interface/package-lock.json +18 -2
  174. package/neural-interface/package.json +2 -1
  175. package/neural-interface/public/onboarding.html +1 -1
  176. package/neural-interface/public/shared/agent-runtime-options.js +89 -11
  177. package/neural-interface/public/shared/agy/agy-bypass.js +47 -0
  178. package/neural-interface/public/shared/agy/agy-client.js +266 -0
  179. package/neural-interface/public/shared/agy/agy-commands.js +120 -0
  180. package/neural-interface/public/shared/agy/agy-copy.js +326 -0
  181. package/neural-interface/public/shared/agy/agy-diff.js +161 -0
  182. package/neural-interface/public/shared/agy/agy-events.js +469 -0
  183. package/neural-interface/public/shared/agy/agy-greeting.js +26 -0
  184. package/neural-interface/public/shared/agy/agy-options.js +232 -0
  185. package/neural-interface/public/shared/agy/agy-panel.js +2543 -0
  186. package/neural-interface/public/shared/agy/agy-render.js +392 -0
  187. package/neural-interface/public/shared/agy/agy-setup.js +149 -0
  188. package/neural-interface/public/shared/agy/agy-styles.js +604 -0
  189. package/neural-interface/public/shared/agy/agy-tabs-logic.js +181 -0
  190. package/neural-interface/public/shared/api.js +4 -4
  191. package/neural-interface/public/shared/assistant/asst-agents-dock.js +3 -4
  192. package/neural-interface/public/shared/assistant/asst-api.js +1 -1
  193. package/neural-interface/public/shared/assistant/asst-brain-picker.js +56 -9
  194. package/neural-interface/public/shared/assistant/asst-budget.js +5 -4
  195. package/neural-interface/public/shared/assistant/asst-control.js +48 -0
  196. package/neural-interface/public/shared/assistant/asst-hosts.js +16 -7
  197. package/neural-interface/public/shared/assistant/asst-limit-dock.js +6 -1
  198. package/neural-interface/public/shared/assistant/asst-limit-wait.js +38 -5
  199. package/neural-interface/public/shared/assistant/asst-panel.js +45 -8
  200. package/neural-interface/public/shared/assistant/asst-render.js +51 -12
  201. package/neural-interface/public/shared/assistant/asst-route.js +237 -57
  202. package/neural-interface/public/shared/assistant/asst-routes-editor.js +9 -8
  203. package/neural-interface/public/shared/assistant/asst-scenes.js +54 -1
  204. package/neural-interface/public/shared/assistant/asst-sidepanel.js +51 -7
  205. package/neural-interface/public/shared/assistant/asst-slash.js +8 -2
  206. package/neural-interface/public/shared/assistant/asst-state.js +76 -9
  207. package/neural-interface/public/shared/assistant/asst-styles.js +34 -0
  208. package/neural-interface/public/shared/assistant/asst-tool-kinds.js +34 -7
  209. package/neural-interface/public/shared/assistant/asst-usage.js +2 -1
  210. package/neural-interface/public/shared/cdx/cdx-output.js +26 -3
  211. package/neural-interface/public/shared/cdx/cdx-panel.js +14 -2
  212. package/neural-interface/public/shared/cdx/cdx-preview.js +12 -0
  213. package/neural-interface/public/shared/cdx/cdx-protocol.js +11 -10
  214. package/neural-interface/public/shared/cdx/cdx-render.js +10 -9
  215. package/neural-interface/public/shared/cdx/cdx-requests.js +11 -1
  216. package/neural-interface/public/shared/cdx/cdx-styles.js +4 -0
  217. package/neural-interface/public/shared/cdx/cdx-tabs.js +75 -5
  218. package/neural-interface/public/shared/cli-install-controller.js +218 -0
  219. package/neural-interface/public/shared/cli-install-view.js +7 -4
  220. package/neural-interface/public/shared/cli-status.js +9 -8
  221. package/neural-interface/public/shared/constants.js +3 -2
  222. package/neural-interface/public/shared/context-menu-model.js +1 -0
  223. package/neural-interface/public/shared/cp/cp-restore.js +162 -0
  224. package/neural-interface/public/shared/cp/cp-tool-cards.js +10 -3
  225. package/neural-interface/public/shared/desktop-bridge.js +17 -0
  226. package/neural-interface/public/shared/desktop-titlebar-view.js +12 -0
  227. package/neural-interface/public/shared/desktop-titlebar.js +13 -4
  228. package/neural-interface/public/shared/desktop-update-view.js +1 -0
  229. package/neural-interface/public/shared/explorer/fx-api.js +4 -4
  230. package/neural-interface/public/shared/explorer/fx-links.js +3 -3
  231. package/neural-interface/public/shared/explorer/fx-logic.js +1 -1
  232. package/neural-interface/public/shared/explorer/fx-main.js +48 -16
  233. package/neural-interface/public/shared/explorer/fx-tree.js +1 -0
  234. package/neural-interface/public/shared/greeting/greeting-card.js +2 -2
  235. package/neural-interface/public/shared/greeting/greeting-view.js +1 -0
  236. package/neural-interface/public/shared/html-shell.js +21 -4
  237. package/neural-interface/public/shared/ocp-v2/ocp-v2-panel.js +27 -7
  238. package/neural-interface/public/shared/ocp-v2/ocp-v2-rehydrate.js +13 -4
  239. package/neural-interface/public/shared/ocp-v2/ocp-v2-render.js +7 -1
  240. package/neural-interface/public/shared/ocp-v2/ocp-v2-styles.js +17 -0
  241. package/neural-interface/public/shared/ocp-v2/ocp-v2-ws.js +46 -3
  242. package/neural-interface/public/shared/provider-icons.js +4 -2
  243. package/neural-interface/public/shared/settings/settings-rules.js +2 -2
  244. package/neural-interface/public/shared/settings/settings-skill-folders-pane.js +1 -1
  245. package/neural-interface/public/shared/settings/settings-skill-folders.js +2 -2
  246. package/neural-interface/public/shared/showcase-privacy.js +6 -1
  247. package/neural-interface/public/shared/sidepanel-session-status.js +3 -3
  248. package/neural-interface/public/shared/skills/ss-creation.js +6 -3
  249. package/neural-interface/public/shared/skills/ss-dialog.js +5 -2
  250. package/neural-interface/public/shared/skills/ss-lint.js +9 -1
  251. package/neural-interface/public/shared/skills/ss-profiles.js +44 -6
  252. package/neural-interface/public/shared/skills/ss-review-view.js +2 -2
  253. package/neural-interface/public/shared/skills/studio/sx-actions.js +1 -1
  254. package/neural-interface/public/shared/skills/studio/sx-create-ai-view.js +1 -1
  255. package/neural-interface/public/shared/skills/studio/sx-create-ai.js +2 -1
  256. package/neural-interface/public/shared/skills/studio/sx-dom.js +2 -1
  257. package/neural-interface/public/shared/skills/studio/sx-fixes-view.js +359 -0
  258. package/neural-interface/public/shared/skills/studio/sx-fixes.js +327 -0
  259. package/neural-interface/public/shared/skills/studio/sx-inspector.js +1 -1
  260. package/neural-interface/public/shared/skills/studio/sx-install-flow.js +9 -6
  261. package/neural-interface/public/shared/skills/studio/sx-install.js +20 -3
  262. package/neural-interface/public/shared/skills/studio/sx-library-view.js +144 -35
  263. package/neural-interface/public/shared/skills/studio/sx-library.js +6 -2
  264. package/neural-interface/public/shared/skills/studio/sx-main.js +11 -2
  265. package/neural-interface/public/shared/skills/studio/sx-review-view.js +1 -1
  266. package/neural-interface/public/shared/skills/studio/sx-subject.js +7 -6
  267. package/neural-interface/public/shared/skills/studio/sx-workspace-view.js +6 -5
  268. package/neural-interface/public/shared/state.js +1 -1
  269. package/neural-interface/public/shared/streaming-markdown.js +58 -0
  270. package/neural-interface/public/shared/styles.css +354 -34
  271. package/neural-interface/public/shared/text-cache.js +29 -0
  272. package/neural-interface/public/shared/tool-list-picker.js +2 -2
  273. package/neural-interface/public/shared/tool-list-view.js +2 -2
  274. package/neural-interface/public/shared/topright-overflow-logic.js +54 -0
  275. package/neural-interface/public/shared/ui-antigravity-panel.js +19 -0
  276. package/neural-interface/public/shared/ui-assistant.js +2 -0
  277. package/neural-interface/public/shared/ui-automation-studio.js +20 -12
  278. package/neural-interface/public/shared/ui-claude-panel.js +299 -77
  279. package/neural-interface/public/shared/ui-cli-banner.js +23 -167
  280. package/neural-interface/public/shared/ui-file-explorer.js +48 -6
  281. package/neural-interface/public/shared/ui-file-links.js +2 -2
  282. package/neural-interface/public/shared/ui-games.js +2 -2
  283. package/neural-interface/public/shared/ui-judgments.js +1 -1
  284. package/neural-interface/public/shared/ui-keybinds.js +8 -2
  285. package/neural-interface/public/shared/ui-link.js +3 -3
  286. package/neural-interface/public/shared/ui-menubar.js +2 -2
  287. package/neural-interface/public/shared/ui-native-loop-router.js +4 -0
  288. package/neural-interface/public/shared/ui-navbar.js +45 -0
  289. package/neural-interface/public/shared/ui-notifications.js +1 -0
  290. package/neural-interface/public/shared/ui-resume.js +9 -7
  291. package/neural-interface/public/shared/ui-schedules-studio.js +41 -10
  292. package/neural-interface/public/shared/ui-scroll-follow.js +16 -5
  293. package/neural-interface/public/shared/ui-sessions.js +22 -12
  294. package/neural-interface/public/shared/ui-settings.js +90 -65
  295. package/neural-interface/public/shared/ui-sidepanel-session-entry.js +9 -2
  296. package/neural-interface/public/shared/ui-sidepanel-sessions.js +3 -3
  297. package/neural-interface/public/shared/ui-sidepanel-tray.js +48 -7
  298. package/neural-interface/public/shared/ui-sidepanel-windows.js +3 -3
  299. package/neural-interface/public/shared/ui-terminal.js +146 -10
  300. package/neural-interface/public/shared/ui-tool-updates.js +3 -3
  301. package/neural-interface/public/shared/ui-tooltip.js +71 -46
  302. package/neural-interface/public/shared/ui-topright-overflow.js +233 -0
  303. package/neural-interface/public/shared/ui-whiteboard.js +1 -1
  304. package/neural-interface/public/shared/ui-worker-end.js +1 -1
  305. package/neural-interface/public/shared/whatsapp/wa-view.js +2 -2
  306. package/neural-interface/public/shared/worker-questions.js +34 -0
  307. package/neural-interface/public/sidepanel-session.html +1 -1
  308. package/neural-interface/server.js +644 -426
  309. package/neural-interface/templates/rulesets/README.md +1 -1
  310. package/neural-interface/templates/rulesets/legacy-hashes.json +5 -5
  311. package/neural-interface/templates/rulesets/manifest.json +1 -1
  312. package/npm-shrinkwrap.json +18 -2
  313. package/package.json +2 -1
  314. package/skills/skill-forge/SKILL.md +4 -4
  315. package/skills/synabun/SKILL.md +1 -1
  316. package/skills/synabun/modules/schedule.md +3 -3
  317. /package/neural-interface/templates/rulesets/{gemini.md → antigravity.md} +0 -0
@@ -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,
@@ -203,7 +207,7 @@ import { createOpenCodeActivityRelay } from './lib/opencode-worker-activity.js';
203
207
  import { browserServePortDenial } from './lib/browser-tool-policy.js';
204
208
  import { createNativeServeBoundary, nativeServeCapability } 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,
@@ -2388,6 +2402,7 @@ function gracefulShutdown(reason = 'shutdown') {
2388
2402
  }
2389
2403
 
2390
2404
  async function runGracefulShutdown(reason) {
2405
+ gitStatusRunner.shutdown();
2391
2406
  console.log(`[server] gracefulShutdown(${reason}) — releasing children...`);
2392
2407
 
2393
2408
  // Native automation providers own SDK subprocesses/isolated serves that are
@@ -2399,6 +2414,8 @@ async function runGracefulShutdown(reason) {
2399
2414
  try { keepRuns = _assistantDispatcher?.suspendForShutdown?.(reason) || []; } catch (err) { console.warn(' assistant suspend err:', err?.message); }
2400
2415
  try { await _nativeLoopRuntime?.shutdown(reason, { keep: keepRuns }); } catch (err) { console.warn(' native loop stop err:', err?.message); }
2401
2416
  try { stopAllIsolatedServes(); } catch (err) { console.warn(' isolated opencode stop err:', err?.message); }
2417
+ // The Antigravity panel's `agy acp` processes (one per open panel socket).
2418
+ try { await antigravityPanel.closeAll(); } catch (err) { console.warn(' antigravity panel stop err:', err?.message); }
2402
2419
  // The code editor's completion serve, and today's autocomplete counters.
2403
2420
  try { await editorCompletion.stop(); } catch (err) { console.warn(' editor completion stop err:', err?.message); }
2404
2421
 
@@ -2818,7 +2835,17 @@ app.post('/api/system/run-update', async (req, res) => {
2818
2835
  appVersion: installPlan.current,
2819
2836
  additionalEntries: getBackupAdditionalEntries(),
2820
2837
  }));
2821
- applyBackupRetention({ folderPath: updateBackupDir });
2838
+ // Older pre-update snapshots are pruned only now, after this one is written and verified: a
2839
+ // failed createVerifiedBackup above threw and never reached here.
2840
+ try {
2841
+ const configuredKeep = loadAutoBackupConfig()?.retention?.preUpdateKeep ?? null;
2842
+ prunePreUpdateSnapshots({
2843
+ folderPath: updateBackupDir,
2844
+ keep: resolvePreUpdateKeep(configuredKeep),
2845
+ keepPath: snapshot.path,
2846
+ log: (message) => console.log(`[update-backup] ${message}`),
2847
+ });
2848
+ } catch (error) { console.error(`[update-backup] retention cleanup failed: ${error?.message || error}`); }
2822
2849
  const handoffPath = resolve(DATA_HOME, 'data', 'update-handoff.json');
2823
2850
  writeFileSync(handoffPath, JSON.stringify({
2824
2851
  version: 1,
@@ -3656,6 +3683,28 @@ function getClaudeSdkExecutable() {
3656
3683
  });
3657
3684
  }
3658
3685
 
3686
+ // Google Antigravity's CLI (`agy`): what the side panel (`agy acp`), the workers, the model list and the session list
3687
+ // start. The resolver's answer, as a program and the words before its own arguments: a binary as it is, a script
3688
+ // through node, a PowerShell script through PowerShell. A Windows command file (cmd.exe) cannot take words after the
3689
+ // fact, so it is reported as not runnable here (the terminal profile still runs it). null when none is installed.
3690
+ function antigravityLaunch() {
3691
+ const found = cliResolver.launcher('antigravity');
3692
+ _noteCliLookup('antigravity', found, 'Antigravity CLI');
3693
+ const spec = found.spec;
3694
+ if (!found.path || !spec || found.state === 'broken') return null;
3695
+ if (spec.kind === 'direct') return { command: spec.launchPath || found.path, args: [], version: found.version || null, path: found.path };
3696
+ if (spec.kind === 'node-script') return { command: process.execPath, args: [spec.launchPath], version: found.version || null, path: found.path };
3697
+ if (spec.kind === 'powershell') return { command: 'powershell.exe', args: ['-NoProfile', '-NonInteractive', '-File', spec.launchPath], version: found.version || null, path: found.path };
3698
+ return null;
3699
+ }
3700
+ // The environment an Antigravity process gets: the PATH a new terminal has (agy runs the user's tools), never the
3701
+ // owner's live data paths or a brain's credential.
3702
+ function antigravityChildEnv() {
3703
+ const env = withPathEnv({ ...process.env }, getAugmentedPath());
3704
+ for (const key of ['SQLITE_DB_PATH', 'MEMORY_DATA_DIR', 'SYNABUN_BRAIN_TOKEN', 'SYNABUN_ASSISTANT_SESSION', 'SYNABUN_ASSISTANT_THREADS']) delete env[key];
3705
+ return env;
3706
+ }
3707
+
3659
3708
  // Codex: what the SDK and the app-server start. On Windows the native codex.exe
3660
3709
  // an npm command file leads to (the SDK spawns without a shell); the file found
3661
3710
  // otherwise. SynaBun carries no Codex (lib/external-tools.js): without one,
@@ -5626,10 +5675,10 @@ CRITICAL — When you need clarification during planning:
5626
5675
  return item;
5627
5676
  }
5628
5677
 
5629
- function loadCodexThreadHistoryFallback(threadId) {
5630
- if (!threadId) return [];
5678
+ function codexRolloutPathFor(threadId) {
5679
+ if (!threadId) return '';
5631
5680
  const dbPath = resolve(activeCodexHome, 'state_5.sqlite');
5632
- if (!existsSync(dbPath)) return [];
5681
+ if (!existsSync(dbPath)) return '';
5633
5682
 
5634
5683
  let db = null;
5635
5684
  let rolloutPath = '';
@@ -5642,20 +5691,27 @@ CRITICAL — When you need clarification during planning:
5642
5691
  } finally {
5643
5692
  try { db?.close(); } catch {}
5644
5693
  }
5645
- if (!rolloutPath || !existsSync(rolloutPath)) return [];
5694
+ return rolloutPath && existsSync(rolloutPath) ? rolloutPath : '';
5695
+ }
5646
5696
 
5647
- let raw = '';
5697
+ // The rollout as history items: read as a stream, parsed once while the file is unchanged
5698
+ // (lib/codex-rollout-reader.js), so the early preview and the fallback that follows share one read.
5699
+ async function loadCodexThreadHistoryFallback(threadId) {
5700
+ const rolloutPath = codexRolloutPathFor(threadId);
5701
+ if (!rolloutPath) return [];
5648
5702
  try {
5649
- raw = readFileSync(rolloutPath, 'utf-8');
5703
+ return await codexRolloutReader.items(rolloutPath, parseCodexRolloutLines);
5650
5704
  } catch (err) {
5651
5705
  console.warn('[codex-skin] Failed to read rollout history:', err.message);
5652
5706
  return [];
5653
5707
  }
5708
+ }
5654
5709
 
5710
+ async function parseCodexRolloutLines(lines) {
5655
5711
  const items = [];
5656
5712
  const calls = new Map();
5657
5713
  let seq = 0;
5658
- for (const line of raw.split('\n')) {
5714
+ for await (const line of lines) {
5659
5715
  if (!line.trim()) continue;
5660
5716
  let entry;
5661
5717
  try {
@@ -6178,7 +6234,29 @@ CRITICAL — When you need clarification during planning:
6178
6234
  }
6179
6235
 
6180
6236
 
6181
- async function bootstrapThread(threadId, cwd = PACKAGE_ROOT) {
6237
+ // Only for the thread the client shows (it asks with `preview: true`), only before the app-server is up, read
6238
+ // without blocking its start; dropped once the real history went out. The read is reused by the fallback.
6239
+ function sendEarlyCodexRolloutPreview(threadId, state) {
6240
+ if (!threadId || initialized) return null;
6241
+ const job = loadCodexThreadHistoryFallback(threadId);
6242
+ job.then((previewItems) => {
6243
+ if (state.historySent || !Array.isArray(previewItems) || !previewItems.length) return;
6244
+ const previewThreadId = String(threadId);
6245
+ sendToClient({
6246
+ type: 'history',
6247
+ preview: true,
6248
+ threadId: previewThreadId,
6249
+ thread: { id: previewThreadId, turns: [] },
6250
+ fallbackItems: previewItems,
6251
+ historyMode: 'read',
6252
+ });
6253
+ }).catch(() => {});
6254
+ return job;
6255
+ }
6256
+
6257
+ async function bootstrapThread(threadId, cwd = PACKAGE_ROOT, { preview = false } = {}) {
6258
+ const previewState = { historySent: false };
6259
+ if (preview) sendEarlyCodexRolloutPreview(threadId, previewState);
6182
6260
  await ensureInitialized();
6183
6261
  if (!threadId) {
6184
6262
  displayedThreadId = activeThreadId;
@@ -6193,11 +6271,13 @@ CRITICAL — When you need clarification during planning:
6193
6271
  const historyMode = activeThreadId && effectiveThreadId && activeThreadId === effectiveThreadId
6194
6272
  ? 'resume'
6195
6273
  : 'read';
6274
+ const fallbackItems = await loadCodexThreadHistoryFallback(effectiveThreadId);
6275
+ previewState.historySent = true;
6196
6276
  sendToClient({
6197
6277
  type: 'history',
6198
6278
  threadId: effectiveThreadId,
6199
6279
  thread: sanitizeCodexThreadForClient(thread),
6200
- fallbackItems: loadCodexThreadHistoryFallback(effectiveThreadId),
6280
+ fallbackItems,
6201
6281
  historyMode,
6202
6282
  });
6203
6283
  // `history` and `ready` must describe the same displayed thread. Returning
@@ -6206,6 +6286,7 @@ CRITICAL — When you need clarification during planning:
6206
6286
  sendToClient({ type: 'ready', threadId: effectiveThreadId });
6207
6287
  } catch (err) {
6208
6288
  console.warn('[codex-skin] Failed to resume thread:', threadId, err.message);
6289
+ previewState.historySent = true;
6209
6290
  sendToClient({ type: 'history', thread: null, resumeFailed: true, threadId });
6210
6291
  sendToClient({ type: 'error', message: `Could not restore saved Codex thread. ${err.message}` });
6211
6292
  sendToClient({ type: 'ready', threadId });
@@ -6318,7 +6399,7 @@ CRITICAL — When you need clarification during planning:
6318
6399
  const runtime = opts.runtimeOptions || await resolveCodexThreadRuntimeOptions(opts);
6319
6400
  displayedThreadId = String(threadId);
6320
6401
  const readThread = await readThreadHistory(threadId).catch(() => null);
6321
- const fallbackItems = loadCodexThreadHistoryFallback(threadId);
6402
+ const fallbackItems = await loadCodexThreadHistoryFallback(threadId);
6322
6403
  const previewThread = readThread || (fallbackItems.length ? { id: threadId, turns: [] } : null);
6323
6404
  if (previewThread) {
6324
6405
  sendToClient({
@@ -6783,7 +6864,7 @@ CRITICAL — When you need clarification during planning:
6783
6864
  }
6784
6865
  // Resolve the tab's account BEFORE first spawn so CODEX_HOME is correct.
6785
6866
  if (!child && msg.accountId) setActiveCodexAccount(msg.accountId);
6786
- await bootstrapThread(msg.threadId || null, msg.cwd || PACKAGE_ROOT);
6867
+ await bootstrapThread(msg.threadId || null, msg.cwd || PACKAGE_ROOT, { preview: msg.preview === true });
6787
6868
  return;
6788
6869
  }
6789
6870
  if (msg.type === 'permission_roots_list') {
@@ -9023,6 +9104,30 @@ setInterval(() => {
9023
9104
 
9024
9105
  process.once('exit', stopAllIsolatedServes);
9025
9106
 
9107
+ // ── Antigravity side panel (/ws/antigravity) ──
9108
+ // One `agy acp` (Google's Antigravity ACP server) per panel socket, one ACP session per tab
9109
+ // (lib/antigravity/panel-bridge.js; docs/antigravity-sidepanel.md). Pasted attachments are written under the data
9110
+ // home for the agent to read; nothing reaches Antigravity's own folders.
9111
+ const antigravityPanel = createAntigravityPanelBridge({
9112
+ resolveLaunch: async () => antigravityLaunch(),
9113
+ childEnv: () => antigravityChildEnv(),
9114
+ attachmentsRoot: resolve(DATA_HOME, 'data', 'antigravity', 'attachments'),
9115
+ homesRoot: () => antigravityHomesRoot(DATA_HOME),
9116
+ isRunHomeActive: (homeDir) => (_nativeLoopRuntime?.list({ activeOnly: true }) || []).some((run) => run.provider === 'antigravity' && run.antigravityHome === homeDir && (run.status === 'starting' || run.status === 'running')),
9117
+ // Bypass keeps the browser policy: a test runner passes from inside a registered code project (lib/antigravity/bypass.js).
9118
+ policyRoots: () => { try { return browserPolicyRoots(loadHookProjects()); } catch { return []; } },
9119
+ clientInfo: { name: 'synabun', title: 'SynaBun', version: String(process.env.npm_package_version || '0') },
9120
+ // Greeting (lib/greeting/): a prompt typed in the panel is noted, and the greeting a tab opened with is told to
9121
+ // the model once, before the typed words (public/shared/agy/agy-greeting.js markers).
9122
+ onHumanPrompt: ({ cwd, words }) => { try { _greeting?.noteActivity({ cwd: cwd || PACKAGE_ROOT, surface: 'antigravity-panel', words }); } catch {} },
9123
+ greetingNote: (id, opts) => _greeting?.consumeNote(id, opts) || '',
9124
+ // `/synabun …` reads the bundled skill, as the Codex panel's does (lib/skill-prompt-bridge.js).
9125
+ expandPrompt: (text) => expandSynabunCodexSlashPrompt(text, join(SKILLS_SOURCE_DIR, 'synabun', 'SKILL.md')),
9126
+ // A turn that ended may have added to the conversation's transcript: Resume search and memory sessions follow.
9127
+ onTurnEnd: ({ sessionId }) => { try { refreshAntigravitySession(sessionId); } catch {} },
9128
+ log: (line) => console.log(line),
9129
+ });
9130
+
9026
9131
  function handleOpencodeV2Ws(ws) {
9027
9132
  _ocpV2WsClients.add(ws);
9028
9133
  ws._nativeLoopWindowId = null;
@@ -11001,7 +11106,7 @@ const _codexSettingsProbes = createLaunchCache({
11001
11106
  });
11002
11107
  setCodexSettingsStore({ get: (key) => _codexSettingsProbes.peek(key), set: (key, value) => _codexSettingsProbes.set(key, value) });
11003
11108
 
11004
- async function getClaudeModelsForClient(force = false) {
11109
+ async function getClaudeModelsForClient(force = false, { nonBlocking = false } = {}) {
11005
11110
  try {
11006
11111
  const bin = getClaudeBin();
11007
11112
  // Sidepanel sessions and native loops run this same installation. The Agent
@@ -11010,8 +11115,9 @@ async function getClaudeModelsForClient(force = false) {
11010
11115
  // when the CLI changes).
11011
11116
  checkClaudeCliSkew(bin).catch(() => {});
11012
11117
  // 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() });
11118
+ // only a first read, another binary or account, or ?refresh=1 waits for the CLI, unless
11119
+ // nonBlocking=true (e.g. cold /api/claude/config boot on Windows) warms it asynchronously.
11120
+ const result = await discoverClaudeModels(bin, { force, nonBlocking, cwd: PACKAGE_ROOT, account: claudeModelAccountKey() });
11015
11121
  if (result?.models?.length) return result;
11016
11122
  } catch (err) {
11017
11123
  console.warn('[claude-models] discovery failed:', err.message);
@@ -11039,7 +11145,11 @@ app.get('/api/claude/config', async (req, res) => {
11039
11145
  // Model ids come straight from the CLI and are already spawn-ready (they carry
11040
11146
  // the `[1m]` suffix where applicable), so the client uses them verbatim — no
11041
11147
  // "<id>:<contextWindow>" composite. See toCliModelName() for the legacy form.
11042
- const { source, models } = await getClaudeModelsForClient();
11148
+ // Default to non-blocking model discovery so cold panel opens on Windows never stall
11149
+ // behind a 3–7s `claude.exe` spawn; ui-claude-panel.js refreshes fallback models async.
11150
+ const { source, models } = await getClaudeModelsForClient(false, {
11151
+ nonBlocking: req.query?.waitModels !== '1',
11152
+ });
11043
11153
  res.json({ ok: true, config: cliCfg, projects, models, modelsSource: source, bootId: SERVER_BOOT_ID });
11044
11154
  } catch (err) {
11045
11155
  res.status(500).json({ error: err.message });
@@ -14372,9 +14482,15 @@ function isSidepanelClaimTokenRevoked(windowId, claimToken) {
14372
14482
 
14373
14483
  // Native provider run ledger. A sidepanel can attach after a page refresh or
14374
14484
  // after a schedule started while no Neural Interface window was open.
14485
+ function withSidepanelRunHome(run) {
14486
+ if (!run || run.provider !== 'antigravity' || run.antigravityHome || !run.runId) return run;
14487
+ const home = _assistantDispatcher?.get?.(run.runId)?.antigravityHome || null;
14488
+ return home ? { ...run, antigravityHome: home } : run;
14489
+ }
14490
+
14375
14491
  app.get('/api/sidepanel-runs', (req, res) => {
14376
14492
  const activeOnly = req.query.active === '1' || req.query.active === 'true';
14377
- res.json({ ok: true, runs: _nativeLoopRuntime?.list({ activeOnly }) || [] });
14493
+ res.json({ ok: true, runs: (_nativeLoopRuntime?.list({ activeOnly }) || []).map(withSidepanelRunHome) });
14378
14494
  });
14379
14495
 
14380
14496
  app.post('/api/sidepanel-runs/release-window', (req, res) => {
@@ -14386,7 +14502,7 @@ app.post('/api/sidepanel-runs/release-window', (req, res) => {
14386
14502
  app.get('/api/sidepanel-runs/:runId', (req, res) => {
14387
14503
  const run = _nativeLoopRuntime?.get(req.params.runId);
14388
14504
  if (!run) return res.status(404).json({ error: 'Native sidepanel run not found' });
14389
- res.json({ ok: true, run });
14505
+ res.json({ ok: true, run: withSidepanelRunHome(run) });
14390
14506
  });
14391
14507
 
14392
14508
  app.post('/api/sidepanel-runs/:runId/claim', (req, res) => {
@@ -14395,7 +14511,7 @@ app.post('/api/sidepanel-runs/:runId/claim', (req, res) => {
14395
14511
  }
14396
14512
  const result = _nativeLoopRuntime?.claim(req.params.runId, req.body?.windowId);
14397
14513
  if (!result?.ok) return res.status(result?.reason === 'not_found' ? 404 : 409).json(result || { ok: false });
14398
- res.json(result);
14514
+ res.json(result.run ? { ...result, run: withSidepanelRunHome(result.run) } : result);
14399
14515
  });
14400
14516
 
14401
14517
  app.post('/api/sidepanel-runs/:runId/release', (req, res) => {
@@ -14533,7 +14649,7 @@ app.post('/api/loop/launch', async (req, res) => {
14533
14649
  // .claude/ set awaitingNext and attachLoopDriver() drives /clear + re-prompt; if those
14534
14650
  // hooks are absent (e.g. a claude-code task run under a hookless harness) the loop STALLS.
14535
14651
  // 'exec': harness-agnostic — attachExecLoopDriver() owns iteration via sentinel echoes, so
14536
- // OpenCode/Codex/Gemini self-advance. Facebook seeding must stay profile:opencode for this.
14652
+ // OpenCode/Codex/Antigravity self-advance. Facebook seeding must stay profile:opencode for this.
14537
14653
  driverType: nativeRuntime ? 'native' : (cliProfile === 'claude-code' ? 'hook' : 'exec'),
14538
14654
  runtimeType: nativeRuntime ? 'native' : 'pty',
14539
14655
  surface: nativeRuntime ? 'sidepanel' : 'terminal',
@@ -14652,12 +14768,12 @@ app.post('/api/loop/launch', async (req, res) => {
14652
14768
  loopLog(terminalSessionId, 'launch:driver', 'attaching hook-driven loop driver (claude-code)');
14653
14769
  attachLoopDriver(terminalSessionId);
14654
14770
  } else {
14655
- // Codex/Gemini/other: spawn shell, server drives `<cli> exec` commands per iteration.
14771
+ // Codex/Antigravity/other: spawn shell, server drives `<cli> exec` commands per iteration.
14656
14772
  // The driver invokes a platform-native wrapper that captures the CLI
14657
14773
  // status and emits a sentinel. Immune to PS1/PROMPT overrides.
14658
14774
  loopLog(terminalSessionId, 'launch:spawn', 'creating shell PTY for exec-driven CLI', { profile: cliProfile, cwd: loopCwd });
14659
14775
  createTerminalSession('shell', 120, 30, loopCwd, { extraEnv: loopExtraEnv, sessionId: terminalSessionId });
14660
- loopLog(terminalSessionId, 'launch:driver', 'attaching exec loop driver (codex/gemini/other)', { profile: cliProfile });
14776
+ loopLog(terminalSessionId, 'launch:driver', 'attaching exec loop driver (codex/antigravity/other)', { profile: cliProfile });
14661
14777
  attachExecLoopDriver(terminalSessionId);
14662
14778
  }
14663
14779
 
@@ -14774,7 +14890,7 @@ app.post('/api/loop/stop', async (req, res) => {
14774
14890
  });
14775
14891
 
14776
14892
  // 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)
14893
+ // The MCP server walks its ancestor PID chain (codex/opencode/antigravity → shell PTY)
14778
14894
  // and POSTs the chain here. We match any PID against terminalSessions[*].pty.pid
14779
14895
  // and return that loop's browserSessionId/browserTabId. Layer B of the
14780
14896
  // defense-in-depth browser tab isolation fix.
@@ -15570,7 +15686,7 @@ async function launchScheduledLoop(schedule) {
15570
15686
  }
15571
15687
 
15572
15688
  // Native providers run headlessly in their durable provider conversation.
15573
- // Gemini and unsupported profiles retain the existing PTY drivers.
15689
+ // Unsupported profiles retain the existing PTY drivers.
15574
15690
  let runDescriptor = null;
15575
15691
  if (nativeRuntime) {
15576
15692
  const presentation = resolveScheduleRunPresentation(
@@ -17499,7 +17615,7 @@ function isSynabunGitHubRemote(remoteText) {
17499
17615
  return /github\.com[:/]danilokhury\/Synabun(?:\.git)?(?:[\s)]|$)/i.test(String(remoteText || ''));
17500
17616
  }
17501
17617
 
17502
- function detectSynabunInstallSource() {
17618
+ async function detectSynabunInstallSource() {
17503
17619
  if (PACKAGED_RUNTIME) return { kind: 'packaged-app' };
17504
17620
  const normalizedRoot = PACKAGE_ROOT.replace(/\\/g, '/');
17505
17621
  if (/\/node_modules\/synabun$/i.test(normalizedRoot)) {
@@ -17509,13 +17625,13 @@ function detectSynabunInstallSource() {
17509
17625
  try {
17510
17626
  // Run as reads (lib/git-read.js): the app's own checkout is a folder like any other, and can be a registered
17511
17627
  // project. What git could not say is an empty answer.
17512
- const git = (args) => {
17513
- const result = spawnGitRead(args, { cwd: PACKAGE_ROOT, timeout: 2000 });
17628
+ const git = async (args) => {
17629
+ const result = await spawnGitRead(args, { cwd: PACKAGE_ROOT, timeout: 2000 });
17514
17630
  return result.status === 0 ? result.stdout.toString().trim() : '';
17515
17631
  };
17516
- if (git(['rev-parse', '--is-inside-work-tree']) === 'true') {
17517
- const remote = git(['remote', 'get-url', 'origin']);
17518
- const remotes = git(['remote', '-v']);
17632
+ if (await git(['rev-parse', '--is-inside-work-tree']) === 'true') {
17633
+ const remote = await git(['remote', 'get-url', 'origin']);
17634
+ const remotes = await git(['remote', '-v']);
17519
17635
  if (isSynabunGitHubRemote(`${remote}\n${remotes}`)) {
17520
17636
  const firstRemote = remote || remotes.split(/\s+/).find(part => isSynabunGitHubRemote(part)) || null;
17521
17637
  return { kind: 'github-clone', remote: firstRemote };
@@ -17600,7 +17716,7 @@ async function checkSynabunUpdate() {
17600
17716
  const installPlan = buildSynabunInstallPlan({
17601
17717
  ...found,
17602
17718
  current,
17603
- installSource: updateAvailable ? detectSynabunInstallSource() : null,
17719
+ installSource: updateAvailable ? await detectSynabunInstallSource() : null,
17604
17720
  repoUrl: SYNABUN_GITHUB_REPO_URL,
17605
17721
  });
17606
17722
 
@@ -17742,6 +17858,8 @@ function systemActivitySources() {
17742
17858
  const blockers = getOpenCodeHistoryBlockers();
17743
17859
  return blockers.activeTurns + blockers.startingTurns;
17744
17860
  },
17861
+ // A prompt running in an Antigravity panel tab (lib/antigravity/panel-bridge.js); dispatched runs are under assistant.
17862
+ 'antigravity-panel': () => antigravityPanel.turnsRunning(),
17745
17863
  'claude-cli': () => recentCliSessions(sessionRegistry),
17746
17864
  loop: () => {
17747
17865
  // A dispatched worker also runs in the native runtime: it is counted under assistant.
@@ -17770,7 +17888,9 @@ app.get('/api/system/activity', (req, res) => {
17770
17888
  const TOOL_DEFS = [
17771
17889
  { key: 'claude-code', cmd: 'claude', versionArg: '--version', npm: '@anthropic-ai/claude-code' },
17772
17890
  { key: 'codex', cmd: 'codex', versionArg: '--version', npm: '@openai/codex' },
17773
- { key: 'gemini', cmd: 'gemini', versionArg: '--version', npm: '@google/gemini-cli' },
17891
+ // Google's Antigravity CLI has no npm package: a Homebrew cask's newer version is what `brew outdated` says, and
17892
+ // the CLI updates itself (`agy update`).
17893
+ { key: 'antigravity', cmd: 'agy', versionArg: '--version' },
17774
17894
  // npm's opencode-ai is OpenCode 1: OpenCode 2's newer version is what Homebrew says (a brew install) or what OpenCode
17775
17895
  // itself announces in its own interface.
17776
17896
  { key: 'opencode', cmd: 'opencode', versionArg: '--version' },
@@ -17970,7 +18090,11 @@ async function checkToolVersions({ force = false } = {}) {
17970
18090
  || 'unknown';
17971
18091
  for (const [key, info] of Object.entries(results)) {
17972
18092
  const source = info.installed ? info.installSource : dominantSource;
17973
- info.installCommand = buildInstallCommand(key, source, { platform: process.platform });
18093
+ // The Antigravity CLI: the command of the install its button runs here (Homebrew's cask on a Mac that has
18094
+ // Homebrew, else Google's installer), so the banner never shows one command and runs another.
18095
+ info.installCommand = key === 'antigravity' && info.install?.available
18096
+ ? cliInstaller.manualCommand(key)
18097
+ : buildInstallCommand(key, source, { platform: process.platform });
17974
18098
  }
17975
18099
 
17976
18100
  // A check started before a newer refresh must never publish its stale answer.
@@ -18039,7 +18163,7 @@ app.use('/api/cli/path', createCliPathApi({
18039
18163
  pathManager: cliPathManager, resolver: cliResolver, isGuestRequest, port: () => PORT,
18040
18164
  diagnostics: async () => {
18041
18165
  const fresh = await freshTerminalPath();
18042
- const statuses = await cliResolver.resolveAll(['claude-code', 'codex', 'opencode']);
18166
+ const statuses = await cliResolver.resolveAll(['claude-code', 'codex', 'opencode', 'antigravity']);
18043
18167
  return { platform: process.platform, pathKey: Object.keys(fresh.env).find(key => key.toUpperCase() === 'PATH'),
18044
18168
  entries: fresh.entries, os: fresh.os, pathext: fresh.env.PATHEXT || fresh.env.PathExt || null,
18045
18169
  tools: Object.fromEntries(Object.entries(statuses).map(([tool, status]) => [tool, { path: status.path, launchPath: status.launchPath, ...cliPathManager.inspect(status) }])) };
@@ -18753,7 +18877,7 @@ app.get('/api/project-files', editorOwnerGuard, async (req, res) => {
18753
18877
  }
18754
18878
 
18755
18879
  // Git info for status badges
18756
- const git = getGitInfo(normalizedDir);
18880
+ const git = await getGitInfo(normalizedDir, { signal: gitRequestSignal(res) });
18757
18881
 
18758
18882
  if (recursive || (search && search.length > 2)) {
18759
18883
  // Recursive search mode
@@ -19171,8 +19295,9 @@ app.put('/api/morelogin/config', (req, res) => {
19171
19295
  app.post('/api/morelogin/test', async (req, res) => {
19172
19296
  try {
19173
19297
  const status = await moreLogin.getMoreLoginStatus(_moreLoginOpts());
19174
- if (!status.installed) return res.json({ ok: false, error: 'MoreLogin desktop app is not installed.' });
19298
+ if (!status.installed) return res.json({ ok: false, error: `MoreLogin desktop app was not found and nothing answers on port ${status.port}.` });
19175
19299
  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).` });
19300
+ if (status.apiError) return res.json({ ok: false, error: status.apiError });
19176
19301
  res.json({ ok: true, port: status.port, profileCount: status.profileCount });
19177
19302
  } catch (err) {
19178
19303
  res.json({ ok: false, error: err.message });
@@ -19268,6 +19393,25 @@ function _normalizeMcpEntry(input) {
19268
19393
  return { command, args, env, url, type };
19269
19394
  }
19270
19395
 
19396
+ // ── Antigravity's MCP configuration (~/.gemini/config/mcp_config.json) ──
19397
+ // The one file `agy mcp add` writes and both the Antigravity CLI and app read: { mcpServers: { name: { command, args,
19398
+ // env, disabled } | { serverUrl, headers, disabled } } }. Antigravity creates it empty; an empty file reads as {}.
19399
+ function antigravityConfigDir() { return join(getHomePath(), '.gemini', 'config'); }
19400
+ function antigravityMcpConfigPath() { return join(antigravityConfigDir(), 'mcp_config.json'); }
19401
+ /** The parsed file ({} when missing or empty); throws on a file that is not a JSON object, so nothing overwrites it. */
19402
+ function readAntigravityMcpConfig() {
19403
+ const path = antigravityMcpConfigPath();
19404
+ if (!existsSync(path)) return {};
19405
+ return parseAntigravityMcpConfig(readFileSync(path, 'utf8'));
19406
+ }
19407
+ function writeAntigravityMcpConfig(data) {
19408
+ const path = antigravityMcpConfigPath();
19409
+ mkdirSync(dirname(path), { recursive: true });
19410
+ const tmp = `${path}.synabun-${process.pid}.tmp`;
19411
+ writeFileSync(tmp, JSON.stringify(data, null, 2) + '\n', { encoding: 'utf8', mode: 0o600 });
19412
+ renameSync(tmp, path);
19413
+ }
19414
+
19271
19415
  function _writeMoreLoginMcpToAgents(entry) {
19272
19416
  const targets = [];
19273
19417
  const home = getHomePath();
@@ -19283,18 +19427,16 @@ function _writeMoreLoginMcpToAgents(entry) {
19283
19427
  targets.push('Claude Code');
19284
19428
  }
19285
19429
  } 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');
19430
+ try { // Antigravity (~/.gemini/config/mcp_config.json, the CLI's and the app's): only where Antigravity has run
19431
+ if (existsSync(antigravityConfigDir())) {
19432
+ const data = readAntigravityMcpConfig();
19433
+ data.mcpServers = { ...(data.mcpServers || {}), [MORELOGIN_MCP_KEY]: buildAntigravityMcpEntry(entry.url
19434
+ ? { type: entry.type || 'http', url: entry.url }
19435
+ : { command: entry.command, args: entry.args || [], env: entry.env }) };
19436
+ writeAntigravityMcpConfig(data);
19437
+ targets.push('Antigravity');
19296
19438
  }
19297
- } catch (e) { console.warn('[morelogin] Gemini register failed:', e.message); }
19439
+ } catch (e) { console.warn('[morelogin] Antigravity register failed:', e.message); }
19298
19440
  try { // Codex (~/.codex/config.toml) — stdio only
19299
19441
  const p = join(home, '.codex', 'config.toml');
19300
19442
  if (existsSync(p) && entry.command) {
@@ -19332,13 +19474,11 @@ function _removeMoreLoginMcpFromAgents() {
19332
19474
  }
19333
19475
  } catch {}
19334
19476
  try {
19335
- const p = join(home, '.gemini', 'settings.json');
19336
- if (existsSync(p)) {
19337
- const data = JSON.parse(readFileSync(p, 'utf8') || '{}');
19477
+ if (existsSync(antigravityMcpConfigPath())) {
19478
+ const data = readAntigravityMcpConfig();
19338
19479
  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');
19480
+ writeAntigravityMcpConfig(removeAntigravityMcpServer(data, MORELOGIN_MCP_KEY));
19481
+ targets.push('Antigravity');
19342
19482
  }
19343
19483
  }
19344
19484
  } catch {}
@@ -21164,10 +21304,106 @@ app.get('/api/opencode/sessions', (req, res) => {
21164
21304
  }
21165
21305
  });
21166
21306
 
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: [] })) });
21307
+ // GET /api/antigravity/sessions — Antigravity's own conversations (ACP session/list on a short-lived `agy acp`),
21308
+ // grouped under the registered projects. A missing or signed-out CLI lists nothing and says why.
21309
+ let _antigravitySessionsCache = null; // { at, value }
21310
+ app.get('/api/antigravity/sessions', async (req, res) => {
21311
+ try {
21312
+ const projects = loadHookProjects();
21313
+ const limit = Math.min(parseInt(req.query.limit) || 30, 100);
21314
+ const offset = parseInt(req.query.offset) || 0;
21315
+ const search = (req.query.search || '').toString();
21316
+ const fresh = _antigravitySessionsCache && Date.now() - _antigravitySessionsCache.at < 15_000 && req.query.refresh !== '1';
21317
+ const listed = fresh ? _antigravitySessionsCache.value : await listAntigravitySessions({ launch: antigravityLaunch(), env: antigravityChildEnv() });
21318
+ if (!fresh && !listed.error) _antigravitySessionsCache = { at: Date.now(), value: listed };
21319
+ res.json({
21320
+ projects: groupSessionsByProject(listed.sessions, projects, { targetProject: req.query.project || null, search, offset, limit }),
21321
+ ...(listed.error ? { error: listed.error } : {}),
21322
+ ...(listed.authRequired ? { authRequired: true } : {}),
21323
+ ...(listed.cliMissing ? { cliMissing: true } : {}),
21324
+ });
21325
+ } catch (err) {
21326
+ res.status(500).json({ error: err.message });
21327
+ }
21328
+ });
21329
+
21330
+ // GET /api/antigravity/models — what an ACP session offers SynaBun (lib/antigravity/catalog.js: a short-lived `agy acp`
21331
+ // in a throwaway home; `agy models` only when ACP fails), kept for a minute.
21332
+ // { models: [{ id, label, efforts?, default? }], source: 'live' | 'stale' | 'unavailable', via, signedIn?, tier?, error? }
21333
+ let _antigravityModelsCache = null; // { at, value }
21334
+ app.get('/api/antigravity/models', async (req, res) => {
21335
+ try {
21336
+ const age = _antigravityModelsCache ? Date.now() - _antigravityModelsCache.at : Infinity;
21337
+ if (age < 60_000 && req.query.refresh !== '1') return res.json(_antigravityModelsCache.value);
21338
+ const value = await listAntigravityModels({ launch: antigravityLaunch(), env: antigravityChildEnv() });
21339
+ if (value.source === 'live') {
21340
+ // A different list than the one the Assistant's catalog last read: its rows are rebuilt (as for Claude's).
21341
+ const ids = (rows) => (rows || []).map((row) => `${row.id}${row.default ? '*' : ''}`).join('\n');
21342
+ const changed = _antigravityModelsCache && ids(_antigravityModelsCache.value.models) !== ids(value.models);
21343
+ _antigravityModelsCache = { at: Date.now(), value };
21344
+ if (changed) _assistantCatalog?.refresh?.().catch(() => {});
21345
+ }
21346
+ else if (_antigravityModelsCache?.value?.models?.length) return res.json({ ..._antigravityModelsCache.value, source: 'stale', error: value.error });
21347
+ res.json(value);
21348
+ } catch (err) {
21349
+ res.status(500).json({ models: [], source: 'unavailable', error: err.message });
21350
+ }
21351
+ });
21352
+
21353
+ // POST /api/antigravity/policy — the question the PreToolUse hook of a SynaBun Antigravity home asks before every tool
21354
+ // call of a brain or run SynaBun started (lib/antigravity/policy-hook.mjs → lib/antigravity/policy.js). The home's
21355
+ // token names who registered the checker; an unknown token is refused. Loopback socket only (requireLocal), and only
21356
+ // { token, payload }: the answer is { decision: 'deny' | 'ask' | 'allow', reason? }.
21357
+ app.post('/api/antigravity/policy', requireLoopbackSocket, express.json({ limit: '4mb' }), async (req, res) => {
21358
+ const body = req.body && typeof req.body === 'object' && !Array.isArray(req.body) ? req.body : {};
21359
+ try {
21360
+ res.json(await defaultAntigravityPolicy.decide(body.token, body.payload && typeof body.payload === 'object' ? body.payload : {}));
21361
+ } catch (err) {
21362
+ res.json({ decision: 'deny', reason: `SynaBun could not check this call (${err?.message || err}).` });
21363
+ }
21364
+ });
21365
+ /** Where the hook of a SynaBun Antigravity home reaches this server (loopback, the port this server listens on). */
21366
+ function antigravityPolicyUrl() { return `http://127.0.0.1:${PORT}/api/antigravity/policy`; }
21367
+ /** The home of an Antigravity brain or run (lib/antigravity/home.js), under the data home's runtime folder. */
21368
+ function antigravityHomeFor(kind, id) { return antigravityHomeDir(antigravityHomesRoot(DATA_HOME), kind, id); }
21369
+
21370
+ // Antigravity's conversations as SynaBun keeps them (lib/antigravity/transcripts.js, read only): the user's own go
21371
+ // into Resume's index (session_cache + session_fts, provider antigravity), every one, SynaBun's brain and run homes
21372
+ // included, into memory capture (lib/memory-session-capture.js). A pass at start, one after a panel turn ends (that
21373
+ // conversation only, a moment later so agy has written its last step), one every five minutes, and a fresh one
21374
+ // before Resume or Skills Studio searches them (at most every 15 s).
21375
+ function antigravitySessionStores({ userOnly = false } = {}) {
21376
+ return antigravityStores({ homesRoot: userOnly ? null : antigravityHomesRoot(DATA_HOME) });
21377
+ }
21378
+ let _antigravityCacheAt = 0;
21379
+ function refreshAntigravityCatalog({ force = false } = {}) {
21380
+ if (!force && Date.now() - _antigravityCacheAt < 15_000) return null;
21381
+ _antigravityCacheAt = Date.now();
21382
+ try { return rebuildAntigravityCache({ stores: antigravitySessionStores() }); }
21383
+ catch (err) { console.warn(' Session cache (antigravity) error:', err.message); return null; }
21384
+ }
21385
+ const _antigravityRefreshTimers = new Map();
21386
+ function refreshAntigravitySession(sessionId) {
21387
+ if (typeof sessionId !== 'string' || !sessionId) return;
21388
+ clearTimeout(_antigravityRefreshTimers.get(sessionId));
21389
+ _antigravityRefreshTimers.set(sessionId, setTimeout(() => {
21390
+ _antigravityRefreshTimers.delete(sessionId);
21391
+ try { rebuildAntigravityCache({ stores: antigravitySessionStores({ userOnly: true }), only: sessionId }); }
21392
+ catch (err) { console.warn(' Session cache (antigravity) error:', err.message); }
21393
+ }, 1500));
21394
+ _antigravityRefreshTimers.get(sessionId).unref?.();
21395
+ }
21396
+ /** SynaBun's own MCP entry for an Antigravity home: the registration the Setup card writes (stdio, this install). */
21397
+ function antigravitySynabunMcpEntry() {
21398
+ const { envPath, mcpCommand, mcpArgs } = getMcpPaths();
21399
+ return { command: mcpCommand, args: mcpArgs, env: { DOTENV_PATH: envPath, SYNABUN_DATA_HOME: DATA_HOME, MEMORY_DATA_DIR: resolve(DATA_HOME, 'mcp-data') } };
21400
+ }
21401
+
21402
+ // GET /api/antigravity/panel — what the side panel's socket offers on this server (lib/antigravity/panel-bridge.js).
21403
+ // Admin-only like the socket itself, so the page tells a refused socket (403 here too: a guest, the automation
21404
+ // browser) from a server that predates it (404: it needs a restart) instead of reconnecting forever.
21405
+ app.get('/api/antigravity/panel', (req, res) => {
21406
+ res.json({ ok: true, socket: ANTIGRAVITY_WS_PATH, capabilities: [...ANTIGRAVITY_PANEL_CAPABILITIES], live: antigravityPanel.liveCount() });
21171
21407
  });
21172
21408
 
21173
21409
  // ── Hybrid session content search (FTS5 + semantic re-rank for Claude) ──
@@ -21204,6 +21440,9 @@ app.get('/api/session-search', async (req, res) => {
21204
21440
  codexCatalogById = new Map(codexCatalog.map((session) => [session.sessionId, session]));
21205
21441
  }
21206
21442
 
21443
+ // Antigravity's index follows its transcripts (refreshAntigravityCatalog: at most every 15 s).
21444
+ if (provider === 'antigravity') refreshAntigravityCatalog();
21445
+
21207
21446
  // 1. FTS5 substring hits
21208
21447
  const ftsRows = searchSessionFts(q, { provider, project: projectFilter, limit: limit * 2 });
21209
21448
  const ftsMap = new Map();
@@ -21468,13 +21707,21 @@ app.post('/api/claude-code/sessions/:sessionId/fork', express.json(), claudeSess
21468
21707
  app.delete('/api/claude-code/sessions/:sessionId', express.json(), claudeSessionRoute((req) => {
21469
21708
  // Irreversible: the panel asks first, and says so here.
21470
21709
  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);
21710
+ // The transcript cache keeps parsed rows by path: a deleted session's go with it (also when the delete fails half way).
21711
+ return Promise.resolve()
21712
+ .then(() => claudeSessionOps.remove(req.params.sessionId, req.body?.project))
21713
+ .finally(() => { try { claudeHistoryCache.forgetSession(req.params.sessionId); } catch {} });
21472
21714
  }));
21473
21715
  app.get('/api/claude-code/sessions/:sessionId/subagents', claudeSessionRoute(req => claudeSessionOps.subagents(req.params.sessionId, req.query.project)));
21474
21716
  app.get('/api/claude-code/sessions/:sessionId/subagents/:agentId/messages', claudeSessionRoute(req => claudeSessionOps.subagentMessages(req.params.sessionId, req.params.agentId, req.query.project)));
21475
21717
 
21476
21718
  // --- Session Messages (read JSONL for panel history) ---
21477
21719
 
21720
+ // The parsed transcripts of the last few sessions opened (lib/claude-history-cache.js).
21721
+ const claudeHistoryCache = createHistoryCache();
21722
+ // The Codex rollouts read last, parsed once for the early preview and the history fallback (lib/codex-rollout-reader.js).
21723
+ const codexRolloutReader = createRolloutReader();
21724
+
21478
21725
  app.get('/api/claude-code/sessions/:sessionId/messages', async (req, res) => {
21479
21726
  try {
21480
21727
  const { sessionId } = req.params;
@@ -21514,22 +21761,17 @@ app.get('/api/claude-code/sessions/:sessionId/messages', async (req, res) => {
21514
21761
  return res.status(missing.status).json(missing.body);
21515
21762
  }
21516
21763
 
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' }));
21764
+ // Session JSONLs reach tens of MB and one session open asks this route several times (the panel's
21765
+ // snapshot probe, the rebuild, "load earlier", a clipped result): the collector of a transcript is
21766
+ // kept, and a file that grew is parsed only from where the last read stopped (lib/claude-history-cache.js:
21767
+ // whole lines split on "\n" only, read from the canonical path that was checked, opened without following
21768
+ // a link).
21523
21769
  // Rows per transcript line: prompts (with their uuid), assistant text, thinking
21524
21770
  // and tool calls, tool results with their typed output (they live inside user
21525
21771
  // lines), compaction markers. The collector also follows the parentUuid chain,
21526
21772
  // so the branches a conversation rewind abandoned are left out: what is shown
21527
21773
  // 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
- }
21774
+ const collector = await claudeHistoryCache.collectorFor(found);
21533
21775
 
21534
21776
  // `?tool=<tool_use_id>`: just that call's result (a restored snapshot holds
21535
21777
  // only the clipped text of a long result and asks for the rest).
@@ -21747,7 +21989,7 @@ app.get('/api/claude-code/integrations', (req, res) => {
21747
21989
 
21748
21990
  // POST /api/claude-code/integrations — enable hook for a target
21749
21991
  // Optional `hook` param: 'SessionStart' | 'UserPromptSubmit' (omit for both)
21750
- app.post('/api/claude-code/integrations', (req, res) => {
21992
+ app.post('/api/claude-code/integrations', async (req, res) => {
21751
21993
  try {
21752
21994
  const { target, projectPath, label, hook } = req.body;
21753
21995
  // target: 'global' | 'project'
@@ -21817,7 +22059,7 @@ app.post('/api/claude-code/integrations', (req, res) => {
21817
22059
  let dubiousOwnership = false;
21818
22060
  let trustPath = null;
21819
22061
  try {
21820
- if (detectDubiousOwnership(normalized)) {
22062
+ if (await detectDubiousOwnership(normalized)) {
21821
22063
  dubiousOwnership = true;
21822
22064
  trustPath = normalizeGitPath(normalized);
21823
22065
  }
@@ -22252,7 +22494,7 @@ app.delete('/api/claude-code/mcp', (req, res) => {
22252
22494
  });
22253
22495
 
22254
22496
  // ═══════════════════════════════════════════
22255
- // Multi-Provider MCP Setup (Gemini, Codex)
22497
+ // Multi-Provider MCP Setup (Antigravity, Codex)
22256
22498
  // ═══════════════════════════════════════════
22257
22499
 
22258
22500
  // ── Minimal TOML helpers (for Codex config.toml) ──
@@ -22411,51 +22653,43 @@ function buildCodexMcpCliCommand() {
22411
22653
  return `codex mcp add SynaBun ${envArgs} -- node ${JSON.stringify(definition.args[0])}`;
22412
22654
  }
22413
22655
 
22414
- // ── Gemini MCP endpoints ──
22656
+ // ── Antigravity MCP endpoints ──
22657
+ // ~/.gemini/config/mcp_config.json (lib/mcp-client-config.js): the file the Antigravity CLI and app share. A file that
22658
+ // is not a JSON object is never overwritten: the request fails and says so.
22415
22659
 
22416
- app.get('/api/setup/gemini/mcp', (req, res) => {
22660
+ app.get('/api/setup/antigravity/mcp', (req, res) => {
22417
22661
  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 });
22662
+ const path = antigravityMcpConfigPath();
22663
+ if (!existsSync(path)) return res.json({ ok: true, connected: false, path });
22664
+ const data = readAntigravityMcpConfig();
22665
+ const entry = data.mcpServers?.SynaBun;
22666
+ res.json({ ok: true, connected: !!entry && entry.disabled !== true, disabled: entry?.disabled === true, path });
22423
22667
  } catch (err) {
22424
22668
  res.status(500).json({ error: err.message });
22425
22669
  }
22426
22670
  });
22427
22671
 
22428
- app.post('/api/setup/gemini/mcp', (req, res) => {
22672
+ app.post('/api/setup/antigravity/mcp', (req, res) => {
22429
22673
  try {
22430
- const dir = join(getHomePath(), '.gemini');
22431
- const settingsPath = join(dir, 'settings.json');
22432
22674
  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, {
22675
+ const data = mergeAntigravityMcpConfig(readAntigravityMcpConfig(), {
22437
22676
  command: mcpCommand,
22438
22677
  args: mcpArgs,
22439
22678
  env: { DOTENV_PATH: envPath, SYNABUN_DATA_HOME: DATA_HOME, MEMORY_DATA_DIR: resolve(DATA_HOME, 'mcp-data') },
22440
22679
  });
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) });
22680
+ writeAntigravityMcpConfig(data);
22681
+ res.json({ ok: true, message: 'SynaBun MCP registered in Antigravity. New Antigravity sessions connect to it.', rules: syncRulesWithMcp('antigravity', 'install', req.body) });
22443
22682
  } catch (err) {
22444
22683
  res.status(500).json({ error: err.message });
22445
22684
  }
22446
22685
  });
22447
22686
 
22448
- app.delete('/api/setup/gemini/mcp', (req, res) => {
22687
+ app.delete('/api/setup/antigravity/mcp', (req, res) => {
22449
22688
  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') });
22689
+ if (!existsSync(antigravityMcpConfigPath())) return res.json({ ok: true, rules: syncRulesWithMcp('antigravity', 'remove') });
22690
+ const data = readAntigravityMcpConfig();
22691
+ if (data.mcpServers && data.mcpServers.SynaBun) writeAntigravityMcpConfig(removeAntigravityMcpServer(data));
22692
+ res.json({ ok: true, message: 'SynaBun MCP removed from Antigravity.', rules: syncRulesWithMcp('antigravity', 'remove') });
22459
22693
  } catch (err) {
22460
22694
  res.status(500).json({ error: err.message });
22461
22695
  }
@@ -22516,11 +22750,11 @@ app.get('/api/setup/status', (req, res) => {
22516
22750
  } catch {}
22517
22751
  claude.cliCommand = claudeMcpAddCommand(mcpPaths);
22518
22752
 
22519
- // Gemini
22520
- let gemini = { connected: false };
22753
+ // Antigravity (~/.gemini/config/mcp_config.json)
22754
+ let antigravity = { connected: false };
22521
22755
  try {
22522
- const gj = JSON.parse(readFileSync(join(home, '.gemini', 'settings.json'), 'utf-8'));
22523
- gemini.connected = !!(gj.mcpServers && gj.mcpServers.SynaBun);
22756
+ const entry = readAntigravityMcpConfig().mcpServers?.SynaBun;
22757
+ antigravity.connected = !!entry && entry.disabled !== true;
22524
22758
  } catch {}
22525
22759
 
22526
22760
  // Codex
@@ -22540,7 +22774,7 @@ app.get('/api/setup/status', (req, res) => {
22540
22774
  let rules = null;
22541
22775
  try { rules = rulesetInstaller.status(); } catch (err) { rules = { ok: false, error: err.message }; }
22542
22776
 
22543
- res.json({ ok: true, claude, gemini, codex, opencode, rules, paths: { mcpIndexPath, envPath, mcpCommand, mcpArgs } });
22777
+ res.json({ ok: true, claude, antigravity, codex, opencode, rules, paths: { mcpIndexPath, envPath, mcpCommand, mcpArgs } });
22544
22778
  } catch (err) {
22545
22779
  res.status(500).json({ error: err.message });
22546
22780
  }
@@ -22640,7 +22874,7 @@ skillsService = createSkillsService({
22640
22874
  // its skill hints); invite guests do not (broadcastSync keeps `skills:` to the owner).
22641
22875
  broadcast: (event) => broadcastSync({ type: 'skills:changed', event }),
22642
22876
  // 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') }),
22877
+ installedRuntimes: () => ({ claude: cliFoundForWarmup('claude-code'), codex: cliFoundForWarmup('codex'), opencode: cliFoundForWarmup('opencode'), antigravity: cliFoundForWarmup('antigravity') }),
22644
22878
  runtimeChecks: skillsRuntimeChecks,
22645
22879
  operationsOptions: { notify: (item) => {
22646
22880
  const session = _assistantRuntime?.listSessions?.().find((s) => s.status !== 'ended');
@@ -22848,9 +23082,11 @@ function skillBrowserAvailability() {
22848
23082
  const unreadable = browserConfigUnreadable(cfg);
22849
23083
  if (unreadable) return { available: false, reason: `The browser settings cannot be read (${unreadable}): fix them in Settings → Browser.` };
22850
23084
  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.' };
23085
+ // The exe in a known folder, or the local API answered lately (an install in a custom folder). This is read
23086
+ // synchronously, so a miss refreshes the answer in the background for the next call.
23087
+ const installed = moreLogin.isMoreLoginPresent();
23088
+ if (!installed) moreLogin.probeMoreLoginApi({ savedCfg: cfg, timeout: 1500 }).catch(() => {});
23089
+ 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
23090
  }
22855
23091
  if ([...browserSessions.values()].some((s) => s?.context)) return { available: true, reason: null };
22856
23092
  if (findBrowserExecutable(cfg.channel)) return { available: true, reason: null };
@@ -22941,6 +23177,19 @@ async function skillSourceSessions({ provider, project = null, q = '', limit = 4
22941
23177
  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
23178
  }
22943
23179
  }
23180
+ } else if (provider === 'antigravity') {
23181
+ // The user's own Antigravity conversations (the panel's and a terminal agy's), as Resume's index keeps them,
23182
+ // titled as Antigravity titles them (refreshAntigravityCatalog).
23183
+ refreshAntigravityCatalog();
23184
+ for (const entry of listSessionCache('antigravity', { limit: 100000 })) {
23185
+ if (entry.deleted || !entry.project_path) continue;
23186
+ const dir = resolve(entry.project_path);
23187
+ const proj = projects.find((p) => isInside(resolve(p.path), dir, { allowRoot: true }));
23188
+ if (!proj) continue;
23189
+ const title = String(entry.first_prompt || '');
23190
+ if (search && !title.toLowerCase().includes(search) && !String(entry.session_id).toLowerCase().includes(search)) continue;
23191
+ 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 });
23192
+ }
22944
23193
  }
22945
23194
  return out.sort((a, b) => String(b.modified || '').localeCompare(String(a.modified || ''))).slice(0, Math.max(1, Math.min(100, Number(limit) || 40)));
22946
23195
  }
@@ -22948,7 +23197,8 @@ async function skillSourceSessions({ provider, project = null, q = '', limit = 4
22948
23197
  /**
22949
23198
  * One session transcript as messages, read the way each runtime keeps it: Claude Code's transcript inside a
22950
23199
  * 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.
23200
+ * OpenCode's rows of its database, Antigravity's transcript in the user's own ~/.gemini. Bounded: the first messages
23201
+ * and the last ones of a very long session.
22952
23202
  */
22953
23203
  async function skillSourceConversation({ provider, sessionId, project = null }) {
22954
23204
  const keep = (list, m) => { list.push(m); if (list.length > 2400) list.splice(300, list.length - 2100); };
@@ -22991,6 +23241,16 @@ async function skillSourceConversation({ provider, sessionId, project = null })
22991
23241
  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
23242
  return { title: session.title || '', project, messages: opencodeRowsToMessages(rows) };
22993
23243
  }
23244
+ if (provider === 'antigravity') {
23245
+ // The transcript in the user's own Antigravity folder (~/.gemini/antigravity-cli/brain/<id>/…), read as
23246
+ // lib/antigravity/transcripts.js reads it: no link followed, bounded; never the model's reasoning.
23247
+ const found = findUserConversation(sessionId);
23248
+ if (!found) throw Object.assign(new Error('That Antigravity conversation was not found.'), { status: 404, code: 'source_not_found' });
23249
+ const parsed = parseAntigravityTranscript(readTranscriptText(found.transcript));
23250
+ const messages = [];
23251
+ for (const m of antigravityMessagesToMessages(parsed.messages)) keep(messages, m);
23252
+ return { title: found.title || parsed.firstPrompt || '', project: found.cwd || project, messages };
23253
+ }
22994
23254
  throw Object.assign(new Error('Unknown provider.'), { status: 400, code: 'source_failed' });
22995
23255
  }
22996
23256
 
@@ -23078,7 +23338,8 @@ function getOpenCodeCommandDir() {
23078
23338
  return xdgDefault;
23079
23339
  }
23080
23340
 
23081
- const SKILL_TARGETS = /** @type {const} */ (['claude', 'codex', 'opencode']);
23341
+ // Antigravity reads its user skills from ~/.gemini/config/skills (lib/skills/roots.js), installed like the others.
23342
+ const SKILL_TARGETS = /** @type {const} */ (['claude', 'codex', 'opencode', 'antigravity']);
23082
23343
 
23083
23344
  /** Where the old prompt / command bridge of a skill is (Codex, OpenCode), or null (Claude Code never had one). */
23084
23345
  function legacyBridgePath(target, skillName) {
@@ -23340,6 +23601,7 @@ function listAvailableSkills() {
23340
23601
  claude: isSkillInstalled('claude', dirName),
23341
23602
  codex: isSkillInstalled('codex', dirName),
23342
23603
  opencode: isSkillInstalled('opencode', dirName),
23604
+ antigravity: isSkillInstalled('antigravity', dirName),
23343
23605
  });
23344
23606
  // What each runtime has of it: a native skill folder ('native'; 'modified' when changed since SynaBun installed
23345
23607
  // it; 'other' when it is not SynaBun's), and an old bridge not moved yet. `installedTargets` stays the one flag
@@ -23724,6 +23986,11 @@ app.post('/api/sidepanel/kill-session', async (req, res) => {
23724
23986
  killed = true;
23725
23987
  }
23726
23988
  }
23989
+ } else if (provider === 'antigravity') {
23990
+ // An Antigravity session lives in the `agy acp` of the panel socket that opened it, which ends with that
23991
+ // socket: nothing outlives a closed panel, so there is nothing to kill here. Its conversation stays in
23992
+ // Antigravity's own history (Resume lists it).
23993
+ killed = false;
23727
23994
  } else if (provider === 'opencode') {
23728
23995
  // OpenCode sessions live on the OpenCode SDK server, not in an orphan
23729
23996
  // map — DELETE them via the proxy.
@@ -25276,7 +25543,7 @@ app.get('/api/mcp/platforms', (req, res) => {
25276
25543
  claudeCode: existsSync(join(home, '.claude.json')) || existsSync(join(home, '.claude')),
25277
25544
  opencode: existsSync(getOpencodeConfigDir()),
25278
25545
  codex: existsSync(join(home, '.codex')),
25279
- gemini: existsSync(join(home, '.gemini')),
25546
+ antigravity: existsSync(antigravityConfigDir()) || existsSync(join(home, '.gemini', 'antigravity-cli')),
25280
25547
  });
25281
25548
  });
25282
25549
 
@@ -25393,7 +25660,7 @@ app.post('/api/mcp/install/github', async (req, res) => {
25393
25660
  });
25394
25661
 
25395
25662
  // 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.
25663
+ // across Claude Code / OpenCode / Codex / Antigravity + SynaBun's registry + Claude Code plugins.
25397
25664
  // Excludes SynaBun's built-in tool-group servers (they're baked in, not "installed").
25398
25665
  app.get('/api/mcp/installed-all', (req, res) => {
25399
25666
  try {
@@ -25440,17 +25707,17 @@ app.get('/api/mcp/installed-all', (req, res) => {
25440
25707
  }
25441
25708
  } catch {}
25442
25709
 
25443
- // Gemini
25710
+ // Antigravity (~/.gemini/config/mcp_config.json: a remote server is `serverUrl`)
25444
25711
  try {
25445
- const p = join(home, '.gemini', 'settings.json');
25446
- if (existsSync(p)) {
25447
- const data = JSON.parse(readFileSync(p, 'utf-8'));
25712
+ if (existsSync(antigravityMcpConfigPath())) {
25713
+ const data = readAntigravityMcpConfig();
25448
25714
  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,
25715
+ const seen = describeAntigravityMcpEntry(cfg);
25716
+ touch(name, 'antigravity', {
25717
+ transport: seen.transport,
25718
+ command: seen.command || null,
25719
+ args: seen.args?.length ? seen.args : null,
25720
+ url: seen.url || null,
25454
25721
  });
25455
25722
  }
25456
25723
  }
@@ -25666,16 +25933,11 @@ async function internalMcpSync({ name, config, platforms }) {
25666
25933
  content = tomlUpsertSection(content, `mcp_servers.${name}`, kvPairs);
25667
25934
  writeFileSync(configPath, content, 'utf-8');
25668
25935
  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';
25936
+ } else if (p === 'antigravity') {
25937
+ const data = readAntigravityMcpConfig();
25938
+ data.mcpServers = { ...(data.mcpServers || {}), [name]: buildAntigravityMcpEntry(config) };
25939
+ writeAntigravityMcpConfig(data);
25940
+ results.antigravity = 'ok';
25679
25941
  }
25680
25942
  } catch (err) {
25681
25943
  results[p] = `error: ${err.message}`;
@@ -25733,17 +25995,12 @@ app.delete('/api/mcp/sync', (req, res) => {
25733
25995
  }
25734
25996
  results.codex = 'ok';
25735
25997
 
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
- }
25998
+ } else if (p === 'antigravity') {
25999
+ if (existsSync(antigravityMcpConfigPath())) {
26000
+ const data = readAntigravityMcpConfig();
26001
+ if (data.mcpServers?.[name]) writeAntigravityMcpConfig(removeAntigravityMcpServer(data, name));
25745
26002
  }
25746
- results.gemini = 'ok';
26003
+ results.antigravity = 'ok';
25747
26004
  }
25748
26005
  } catch (err) {
25749
26006
  results[p] = `error: ${err.message}`;
@@ -26160,9 +26417,11 @@ const CLI_CONFIG_PATH = resolve(DATA_HOME, 'data', 'cli-config.json');
26160
26417
  const CLI_DEFAULTS = {
26161
26418
  'claude-code': { command: 'claude' },
26162
26419
  'codex': { command: 'codex' },
26163
- 'gemini': { command: 'gemini' },
26420
+ 'antigravity': { command: 'agy' },
26164
26421
  'opencode': { command: 'opencode' },
26165
26422
  };
26423
+ // Profiles SynaBun no longer drives: a path saved for one (Gemini CLI, replaced by Antigravity) is never a profile.
26424
+ const RETIRED_CLI_PROFILES = new Set(['gemini']);
26166
26425
 
26167
26426
  // Parsed cli-config.json keyed by mtime — every terminal spawn resolves its
26168
26427
  // profile through here, so skip the read + parse when the file is unchanged.
@@ -26175,7 +26434,9 @@ function loadCliConfig() {
26175
26434
  if (!_cliConfigCache || _cliConfigCache.mtimeMs !== mtimeMs) {
26176
26435
  _cliConfigCache = { mtimeMs, raw: JSON.parse(readFileSync(CLI_CONFIG_PATH, 'utf-8')) };
26177
26436
  }
26178
- return { ...CLI_DEFAULTS, ...structuredClone(_cliConfigCache.raw) };
26437
+ const saved = structuredClone(_cliConfigCache.raw);
26438
+ for (const retired of RETIRED_CLI_PROFILES) delete saved?.[retired];
26439
+ return { ...CLI_DEFAULTS, ...saved };
26179
26440
  } catch { return { ...CLI_DEFAULTS }; }
26180
26441
  }
26181
26442
 
@@ -26196,7 +26457,7 @@ const SHELL_PROFILE = { shell: DEFAULT_SHELL, args: interactiveShellArgs(), env:
26196
26457
  /**
26197
26458
  * Build terminal profile config dynamically from cli-config.json.
26198
26459
  * 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')
26460
+ * @param {string} profileId - CLI profile ID (e.g. 'claude-code', 'codex', 'antigravity', 'shell')
26200
26461
  * @param {Object} [opts] - Optional overrides
26201
26462
  * @param {string} [opts.model] - Model flag for CLIs that support it (e.g. claude --model sonnet)
26202
26463
  */
@@ -26219,6 +26480,8 @@ function getTerminalProfile(profileId, opts = {}) {
26219
26480
  cmd = `${cmd} resume ${terminalArgument(opts.resume, DEFAULT_SHELL)}`;
26220
26481
  } else if (opts.resume && profileId === 'opencode') {
26221
26482
  cmd = `${cmd} -s ${terminalArgument(opts.resume, DEFAULT_SHELL)}`;
26483
+ } else if (opts.resume && profileId === 'antigravity') {
26484
+ cmd = `${cmd} --conversation ${terminalArgument(opts.resume, DEFAULT_SHELL)}`;
26222
26485
  }
26223
26486
  // Append model flag for CLIs that support it
26224
26487
  else if (opts.model) {
@@ -26236,7 +26499,7 @@ function getTerminalProfile(profileId, opts = {}) {
26236
26499
  const modelMap = {
26237
26500
  'claude-code': (m) => `${cmd} --model ${m}`,
26238
26501
  'codex': (m) => `${cmd} --model ${m}`,
26239
- 'gemini': (m) => `${cmd} --model ${m}`,
26502
+ 'antigravity': (m) => `${cmd} --model ${m}`,
26240
26503
  // OpenCode's terminal app has no --model (it prints its help and exits): the model is picked in it.
26241
26504
  'opencode': () => cmd,
26242
26505
  };
@@ -26254,6 +26517,9 @@ function getTerminalProfile(profileId, opts = {}) {
26254
26517
  if (codexEffort) {
26255
26518
  cmd = `${cmd} -c model_reasoning_effort="${codexEffort}"`;
26256
26519
  }
26520
+ } else if (opts.effort && profileId === 'antigravity') {
26521
+ // agy --effort: low | medium | high | xhigh | max (the model's own default when the flag is left out).
26522
+ if (['low', 'medium', 'high', 'xhigh', 'max'].includes(opts.effort)) cmd = `${cmd} --effort ${opts.effort}`;
26257
26523
  }
26258
26524
  // Every Codex terminal: the Windows sandbox the Admin sandbox setting allows. A bare value
26259
26525
  // (Codex reads one that is not TOML as a string), so no shell can strip a quote from it.
@@ -26265,7 +26531,7 @@ function getTerminalProfile(profileId, opts = {}) {
26265
26531
  // The loop runs in a PTY with no human to answer prompts, so accessing
26266
26532
  // sibling projects or running Bash/Write would stall.
26267
26533
  // Scoped to opts.loopBypass so interactive terminal sessions still prompt.
26268
- if (opts.loopBypass && profileId === 'claude-code') {
26534
+ if (opts.loopBypass && (profileId === 'claude-code' || profileId === 'antigravity')) {
26269
26535
  cmd = `${cmd} --dangerously-skip-permissions`;
26270
26536
  }
26271
26537
 
@@ -26294,7 +26560,7 @@ function getTerminalProfile(profileId, opts = {}) {
26294
26560
  // provider sign-in terminals. The desktop app uses this same server/PTY host.
26295
26561
  async function freshTerminalPath(base = process.env) {
26296
26562
  const os = await osPath.refresh({ force: true });
26297
- const launchers = ['claude-code', 'codex', 'opencode', 'gemini'].map(tool => cliResolver.launcher(tool));
26563
+ const launchers = ['claude-code', 'codex', 'opencode', 'antigravity'].map(tool => cliResolver.launcher(tool));
26298
26564
  // A packaged application's runtime and npm's global bin (rememberPackagedPath above), after the user's own.
26299
26565
  const { last: synabunDirs, extra: extraDirs } = rememberedPackagedPath();
26300
26566
  return { ...composeTerminalPath({ env: base, os, launchers, synabunDirs, extraDirs }), os, launchers };
@@ -26861,7 +27127,7 @@ async function driveNextIteration(session, loopState, filename) {
26861
27127
  return true;
26862
27128
  }
26863
27129
 
26864
- // ── Exec-based Loop Driver (Codex, Gemini, etc.) ──
27130
+ // ── Exec-based Loop Driver (Codex, Antigravity, etc.) ──
26865
27131
  // Fully isolated from Claude's hook-based driver. Runs platform-native wrapper
26866
27132
  // files inside a shell PTY and detects completion via sentinel echo markers.
26867
27133
  // The wrapper captures the CLI status before printing SYNABUN_ITER_DONE_<N>, so
@@ -27191,6 +27457,31 @@ _nativeLoopRuntime = new NativeLoopRuntime({
27191
27457
  policyRoots: taskBrowserPolicyRoots(state),
27192
27458
  });
27193
27459
  },
27460
+ // Google Antigravity: one `agy acp` per run (lib/native-loop-providers.js createAntigravityNativeLoopAdapter), in
27461
+ // the run's own Antigravity home (lib/antigravity/home.js: exactly the run's MCP servers, the policy hook that
27462
+ // asks POST /api/antigravity/policy before every tool call, the user's rules and skills). Approvals go to the
27463
+ // Assistant's broker under `ask`; without an installed CLI the run fails at once with the install hint.
27464
+ antigravity: (state) => {
27465
+ const launch = antigravityLaunch();
27466
+ 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.');
27467
+ // A resumed run uses the home its first start wrote (its conversation is there).
27468
+ const homeDir = state.resumingWorker && typeof state.antigravityHome === 'string' && state.antigravityHome
27469
+ ? validateRunHome(state.antigravityHome, antigravityHomesRoot(DATA_HOME))
27470
+ : antigravityHomeFor('run', state.runId || state.terminalSessionId || `loop-${Date.now()}`);
27471
+ return createAntigravityNativeLoopAdapter({
27472
+ ...state,
27473
+ launch,
27474
+ // The PATH a new terminal has: Antigravity runs the user's own tools.
27475
+ extraEnv: { PATH: antigravityChildEnv().PATH, ...(state.extraEnv || {}) },
27476
+ serverPort: PORT,
27477
+ homeDir,
27478
+ synabunMcpEntry: antigravitySynabunMcpEntry(),
27479
+ policy: defaultAntigravityPolicy,
27480
+ policyUrl: antigravityPolicyUrl(),
27481
+ // Assistant task runs: where the browser policy lets its test runners run besides its own project.
27482
+ policyRoots: taskBrowserPolicyRoots(state),
27483
+ });
27484
+ },
27194
27485
  opencode: async (state) => {
27195
27486
  // An OpenCode SynaBun does not drive: the run fails with the sentence before its configuration folder, its
27196
27487
  // sandbox profile or its serve exist (a loop or a schedule was refused already; this is a worker's, a backstop).
@@ -27239,6 +27530,7 @@ _nativeLoopRuntime = new NativeLoopRuntime({
27239
27530
  state.model,
27240
27531
  {
27241
27532
  SYNABUN_DESKTOP_GRANT: typeof state.desktopGrantProvider === 'function' ? state.desktopGrantProvider() : null,
27533
+ SYNABUN_RUN_MODE: state.runMode === 'task' ? 'task' : null,
27242
27534
  },
27243
27535
  // Assistant task runs: the browser policy plugin (loops and schedules unchanged) with the registered code
27244
27536
  // projects its test runners may run in (the sandboxed serve cannot read the registry), and the run's confinement
@@ -27819,6 +28111,17 @@ _nativeLoopRuntime = new NativeLoopRuntime({
27819
28111
  ClaudeSession: claudeBridgeModule.ClaudeSession,
27820
28112
  sdkVersion: claudeBridgeModule.SDK_VERSION,
27821
28113
  handleCodexSkinWebSocket, ensureIsolatedServe, stopIsolatedServe, setupOpencodeSidepanelConfig,
28114
+ // An Antigravity brain (lib/assistant-brains/antigravity.js): `agy acp` in the brain's own home, its hook asking
28115
+ // POST /api/antigravity/policy, SynaBun's MCP entry the Setup card writes.
28116
+ antigravity: {
28117
+ launch: antigravityLaunch,
28118
+ // The PATH a new terminal has (agy runs the user's tools); never the owner's live data paths or a credential.
28119
+ childEnv: antigravityChildEnv,
28120
+ home: (sessionId) => antigravityHomeFor('brain', sessionId),
28121
+ synabunMcpEntry: antigravitySynabunMcpEntry,
28122
+ policy: defaultAntigravityPolicy,
28123
+ policyUrl: antigravityPolicyUrl,
28124
+ },
27822
28125
  broadcastSync,
27823
28126
  dataDir: LOOP_DATA_DIR,
27824
28127
  memory: assistantMemory,
@@ -28196,7 +28499,7 @@ function findLoopFileForTerminal(terminalSessionId) {
28196
28499
  }
28197
28500
 
28198
28501
  /**
28199
- * Exec-based loop driver for non-Claude CLIs (Codex, Gemini, etc.).
28502
+ * Exec-based loop driver for non-Claude CLIs (Codex, Antigravity, etc.).
28200
28503
  * State machine: booting → ready → running → ready → ...
28201
28504
  *
28202
28505
  * Completion detection: sentinel echo approach.
@@ -28972,7 +29275,7 @@ app.delete('/api/terminal/sessions/:id', (req, res) => {
28972
29275
  });
28973
29276
 
28974
29277
  app.get('/api/terminal/profiles', (req, res) => {
28975
- const profileIds = ['claude-code', 'codex', 'gemini', 'opencode', 'shell'];
29278
+ const profileIds = ['claude-code', 'codex', 'antigravity', 'opencode', 'shell'];
28976
29279
  const profiles = profileIds.map(id => {
28977
29280
  const p = getTerminalProfile(id);
28978
29281
  return { id, shell: p.shell, args: p.args };
@@ -29056,30 +29359,15 @@ function resolveGitPath() {
29056
29359
  }
29057
29360
 
29058
29361
  // 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);
29362
+ // directly without a shell). The runner handles a resolved Windows .cmd/.bat shim.
29363
+ async function spawnGit(args, opts = {}) {
29364
+ const binary = await resolveGitPathInBackground();
29365
+ return gitStatusRunner.runRaw(args, { ...opts, binary: binary || 'git', timeoutMs: opts.timeout || 5000 });
29076
29366
  }
29077
29367
 
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 });
29368
+ async function spawnGitRead(args, opts = {}) {
29369
+ const binary = await resolveGitPathInBackground();
29370
+ return gitStatusRunner.run(args, { ...opts, binary: binary || 'git', timeoutMs: opts.timeout || 5000 });
29083
29371
  }
29084
29372
 
29085
29373
  function logGitFailure(where, err) {
@@ -29118,22 +29406,20 @@ function resolveGitPathInBackground() {
29118
29406
 
29119
29407
  // Startup probe — always logs git status so Windows users can see whether git works at all. Asynchronous:
29120
29408
  // 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(() => {});
29409
+ setImmediate(async () => {
29410
+ try {
29411
+ const gitPath = await resolveGitPathInBackground();
29412
+ const result = await gitStatusRunner.run(['--version'], { binary: gitPath || 'git', timeoutMs: 3000 });
29413
+ if (result.ok) console.log(`[git] ${String(result.stdout).trim()} — OK`);
29414
+ else console.warn(`[git] startup probe FAILED: ${String(result.stderr).trim() || result.code}. File explorer git UI will be hidden.`);
29415
+ } catch { /* availability is reported on the first read */ }
29130
29416
  });
29131
29417
 
29132
29418
  // Git "dubious ownership" detection — CVE-2022-24765 safety check refuses to operate on
29133
29419
  // repos owned by a different OS user/SID. Common on Windows external drives or repos
29134
29420
  // 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 });
29421
+ async function detectDubiousOwnership(dir) {
29422
+ const r = await spawnGitRead(['rev-parse', '--is-inside-work-tree'], { cwd: dir, timeout: 3000 });
29137
29423
  if (r.status === 0) return false;
29138
29424
  const stderr = r.stderr?.toString?.() || '';
29139
29425
  return /dubious ownership|safe\.directory/i.test(stderr);
@@ -29146,91 +29432,27 @@ function normalizeGitPath(dir) {
29146
29432
  }
29147
29433
 
29148
29434
  /** 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;
29155
- }
29156
-
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();
29435
+ async function getGitInfo(dir, options = {}) {
29436
+ const result = projectStatus(await gitStatusRunner.read(dir, options), dir);
29437
+ if (!result.isGit) return null;
29438
+ const statuses = new Map();
29439
+ for (const change of result.changes) {
29440
+ const child = change.path.split('/')[0];
29441
+ if (gitStatusPriority(change.status) > gitStatusPriority(statuses.get(child))) statuses.set(child, change.status);
29178
29442
  }
29443
+ return { branch: result.branch, statuses, scanState: result.timedOut ? 'timed-out' : result.ok ? 'ready' : 'failed' };
29444
+ }
29179
29445
 
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
- }
29446
+ function gitRequestSignal(res) {
29447
+ const controller = new AbortController();
29448
+ res.once('close', () => controller.abort());
29449
+ return controller.signal;
29228
29450
  }
29229
29451
 
29230
29452
  const GIT_STATUS_PRIORITY = { untracked: 1, added: 2, renamed: 2, staged: 3, modified: 4, mixed: 5, deleted: 5, conflict: 6 };
29231
29453
  function gitStatusPriority(s) { return GIT_STATUS_PRIORITY[s] || 0; }
29232
29454
 
29233
- app.get('/api/terminal/files', (req, res) => {
29455
+ app.get('/api/terminal/files', async (req, res) => {
29234
29456
  const dir = req.query.path;
29235
29457
  if (!dir || typeof dir !== 'string') return res.status(400).json({ error: 'path required' });
29236
29458
  const search = (req.query.search || '').trim().toLowerCase();
@@ -29287,7 +29509,7 @@ app.get('/api/terminal/files', (req, res) => {
29287
29509
  });
29288
29510
 
29289
29511
  // Try git — gracefully ignored if unavailable
29290
- const git = getGitInfo(dir);
29512
+ const git = await getGitInfo(dir, { signal: gitRequestSignal(res) });
29291
29513
  if (git) {
29292
29514
  for (const item of items) {
29293
29515
  const s = git.statuses.get(item.name);
@@ -29301,11 +29523,11 @@ app.get('/api/terminal/files', (req, res) => {
29301
29523
  }
29302
29524
  });
29303
29525
 
29304
- app.get('/api/terminal/branches', (req, res) => {
29526
+ app.get('/api/terminal/branches', async (req, res) => {
29305
29527
  const dir = req.query.path;
29306
29528
  if (!dir || typeof dir !== 'string') return res.status(400).json({ error: 'path required' });
29307
29529
 
29308
- const inside = spawnGitRead(['rev-parse', '--is-inside-work-tree'], { cwd: dir, timeout: 3000 });
29530
+ const inside = await spawnGitRead(['rev-parse', '--is-inside-work-tree'], { cwd: dir, timeout: 3000 });
29309
29531
  if (inside.status !== 0) {
29310
29532
  logGitFailure('branches: rev-parse', inside.error || { stderr: inside.stderr });
29311
29533
  return res.json({ branches: [], current: null });
@@ -29314,11 +29536,11 @@ app.get('/api/terminal/branches', (req, res) => {
29314
29536
  try {
29315
29537
  // Current branch — chained fallback (older git may not support --show-current)
29316
29538
  let current = '';
29317
- const showCurrent = spawnGitRead(['branch', '--show-current'], { cwd: dir, timeout: 3000 });
29539
+ const showCurrent = await spawnGitRead(['branch', '--show-current'], { cwd: dir, timeout: 3000 });
29318
29540
  if (showCurrent.status === 0) current = showCurrent.stdout.toString().trim();
29319
29541
  else logGitFailure('branches: show-current', showCurrent.error || { stderr: showCurrent.stderr });
29320
29542
  if (!current) {
29321
- const abbrev = spawnGitRead(['rev-parse', '--abbrev-ref', 'HEAD'], { cwd: dir, timeout: 3000 });
29543
+ const abbrev = await spawnGitRead(['rev-parse', '--abbrev-ref', 'HEAD'], { cwd: dir, timeout: 3000 });
29322
29544
  if (abbrev.status === 0) {
29323
29545
  const b = abbrev.stdout.toString().trim();
29324
29546
  if (b && b !== 'HEAD') current = b;
@@ -29327,7 +29549,7 @@ app.get('/api/terminal/branches', (req, res) => {
29327
29549
 
29328
29550
  // All local branches — plain `git branch` parsed manually; avoids --format=%(refname:short)
29329
29551
  // which can get mangled by cmd.exe variable expansion on Windows even with proper quoting.
29330
- const branchResult = spawnGitRead(['branch'], { cwd: dir });
29552
+ const branchResult = await spawnGitRead(['branch'], { cwd: dir });
29331
29553
  if (branchResult.status !== 0) logGitFailure('branch list', branchResult.error || { stderr: branchResult.stderr });
29332
29554
  const raw = branchResult.status === 0 ? branchResult.stdout.toString() : '';
29333
29555
  // Plain `git branch` output: each line is " branch" or "* branch" (current). Strip
@@ -29345,7 +29567,7 @@ app.get('/api/terminal/branches', (req, res) => {
29345
29567
  // Trust a workspace path with git — runs `git config --global --add safe.directory <path>`
29346
29568
  // so git stops refusing to operate on it (CVE-2022-24765 dubious-ownership protection).
29347
29569
  // Triggered by the add-project UI flows when the server detects the dubious-ownership error.
29348
- app.post('/api/terminal/trust-workspace', (req, res) => {
29570
+ app.post('/api/terminal/trust-workspace', async (req, res) => {
29349
29571
  const { path: dir } = req.body || {};
29350
29572
  if (!dir || typeof dir !== 'string') return res.status(400).json({ ok: false, error: 'path required' });
29351
29573
  if (/[\x00-\x1f]/.test(dir) || dir === '*' || dir === '/' || dir === '\\') {
@@ -29361,7 +29583,7 @@ app.post('/api/terminal/trust-workspace', (req, res) => {
29361
29583
  }
29362
29584
 
29363
29585
  const gitPath = normalizeGitPath(abs);
29364
- const r = spawnGit(['config', '--global', '--add', 'safe.directory', gitPath]);
29586
+ const r = await spawnGit(['config', '--global', '--add', 'safe.directory', gitPath]);
29365
29587
  if (r.status !== 0) {
29366
29588
  const stderr = r.stderr?.toString?.()?.trim() || r.error?.message || 'git config failed';
29367
29589
  logGitFailure('config --add safe.directory', r.error || { stderr: r.stderr });
@@ -29369,12 +29591,12 @@ app.post('/api/terminal/trust-workspace', (req, res) => {
29369
29591
  }
29370
29592
 
29371
29593
  // Confirm by re-probing
29372
- const stillDubious = detectDubiousOwnership(abs);
29594
+ const stillDubious = await detectDubiousOwnership(abs);
29373
29595
  console.log(`[git] trusted workspace: ${gitPath}${stillDubious ? ' (but still flagged — check permissions)' : ''}`);
29374
29596
  res.json({ ok: true, path: gitPath, stillDubious });
29375
29597
  });
29376
29598
 
29377
- app.post('/api/terminal/checkout', (req, res) => {
29599
+ app.post('/api/terminal/checkout', async (req, res) => {
29378
29600
  const { path: dir, branch } = req.body;
29379
29601
  if (!dir || !branch) return res.status(400).json({ error: 'path and branch required' });
29380
29602
 
@@ -29383,7 +29605,7 @@ app.post('/api/terminal/checkout', (req, res) => {
29383
29605
  return res.status(400).json({ error: 'Invalid branch name' });
29384
29606
  }
29385
29607
 
29386
- const inside = spawnGitRead(['rev-parse', '--is-inside-work-tree'], { cwd: dir, timeout: 3000 });
29608
+ const inside = await spawnGitRead(['rev-parse', '--is-inside-work-tree'], { cwd: dir, timeout: 3000 });
29387
29609
  if (inside.status !== 0) {
29388
29610
  logGitFailure('checkout: rev-parse', inside.error || { stderr: inside.stderr });
29389
29611
  return res.status(400).json({ error: 'Not a git repository' });
@@ -29393,7 +29615,8 @@ app.post('/api/terminal/checkout', (req, res) => {
29393
29615
  // No '--' separator — that would make git treat the branch name as a file pathspec.
29394
29616
  // (The switch itself is a write and runs plainly, with the repository's post-checkout hook; the two questions
29395
29617
  // around it are reads.)
29396
- const result = spawnGit(['checkout', branch], { cwd: dir, timeout: 10000 });
29618
+ const result = await spawnGit(['checkout', branch], { cwd: dir, timeout: 10000 });
29619
+ gitStatusRunner.invalidate(dir);
29397
29620
  if (result.status !== 0) {
29398
29621
  const stderr = result.stderr?.toString?.()?.trim() || result.error?.message || 'Checkout failed';
29399
29622
  return res.status(500).json({ error: stderr });
@@ -29401,71 +29624,21 @@ app.post('/api/terminal/checkout', (req, res) => {
29401
29624
 
29402
29625
  // Return confirmed branch name + git's own output
29403
29626
  let current = '';
29404
- const showCurrent = spawnGitRead(['branch', '--show-current'], { cwd: dir, timeout: 3000 });
29627
+ const showCurrent = await spawnGitRead(['branch', '--show-current'], { cwd: dir, timeout: 3000 });
29405
29628
  if (showCurrent.status === 0) current = showCurrent.stdout.toString().trim();
29406
29629
  else logGitFailure('checkout: show-current', showCurrent.error || { stderr: showCurrent.stderr });
29407
29630
  const output = result.stderr?.toString?.()?.trim() || `Switched to branch '${current}'`;
29408
29631
  res.json({ ok: true, branch: current, output });
29409
29632
  });
29410
29633
 
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 };
29634
+ // All callers share the canonical worktree's flight and last completed result.
29635
+ async function computeGitStatus(dir, options = {}) {
29636
+ const result = projectStatus(await gitStatusRunner.read(dir, options), dir);
29637
+ if (!result.ok || result.partial || result.stale) return { ok: false, isGit: !!result.isGit, branch: result.branch, changes: result.changes || [],
29638
+ partial: true, stale: !!result.stale, timedOut: !!result.timedOut, retryAt: result.retryAt,
29639
+ scanState: result.timedOut ? 'timed-out' : 'failed', error: 'Git status is incomplete. Refresh before switching branches or assuming the worktree is clean.' };
29640
+ if (!result.isGit) return { ok: true, isGit: false, branch: null, changes: [] };
29641
+ return { ok: true, isGit: true, branch: result.branch, changes: result.changes };
29469
29642
  }
29470
29643
 
29471
29644
  // The folder a git route is asked about: inside a registered folder, as the disk resolves it (the rule of
@@ -29479,11 +29652,9 @@ function registeredGitFolder(asked, res) {
29479
29652
  // be started, failed or took too long, as the command lines the /api/git routes used to run did. For reads only
29480
29653
  // (every caller is one): run as lib/git-read.js runs a read, so no program the repository's own configuration
29481
29654
  // 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`);
29655
+ async function gitOutput(dir, args, { timeout = 5000, maxBuffer } = {}) {
29656
+ const result = await gitStatusRunner.run(args, { cwd: dir, timeoutMs: timeout, ...(maxBuffer ? { maxBuffer } : {}) });
29657
+ if (!result.ok) throw new Error(result.stderr?.toString?.()?.trim?.() || `git ${args[0]} failed`);
29487
29658
  return result.stdout.toString();
29488
29659
  }
29489
29660
 
@@ -29493,37 +29664,17 @@ function gitOutput(dir, args, { timeout = 5000, maxBuffer } = {}) {
29493
29664
  app.get('/api/git/status', editorOwnerGuard, async (req, res) => {
29494
29665
  const dir = req.query.path;
29495
29666
  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
- }
29667
+ if (req.query.refresh === '1') gitStatusRunner.invalidate(dir, { refresh: true });
29668
+ res.json(await computeGitStatus(dir, { signal: gitRequestSignal(res) }));
29518
29669
  });
29519
29670
 
29520
- app.post('/api/git/commit', editorOwnerGuard, (req, res) => {
29671
+ app.post('/api/git/commit', editorOwnerGuard, async (req, res) => {
29521
29672
  const { path: dir, message, files } = req.body;
29522
29673
  if (!dir || typeof dir !== 'string' || !message) return res.status(400).json({ error: 'path and message required' });
29523
29674
  if (typeof message !== 'string' || message.trim().length === 0) return res.status(400).json({ error: 'Commit message cannot be empty' });
29524
29675
 
29525
29676
  try {
29526
- gitOutput(dir, ['rev-parse', '--is-inside-work-tree'], { timeout: 3000 });
29677
+ await gitOutput(dir, ['rev-parse', '--is-inside-work-tree'], { timeout: 3000 });
29527
29678
  } catch {
29528
29679
  return res.status(400).json({ error: 'Not a git repository' });
29529
29680
  }
@@ -29535,16 +29686,19 @@ app.post('/api/git/commit', editorOwnerGuard, (req, res) => {
29535
29686
  for (const f of files) {
29536
29687
  if (typeof f !== 'string' || f.includes('..')) return res.status(400).json({ error: 'Invalid file path' });
29537
29688
  }
29538
- const result = spawnSync('git', ['add', '--', ...files], { cwd: dir, env: withGitFallback(process.env), stdio: 'pipe', timeout: 10000 });
29689
+ const result = await gitStatusRunner.runRaw(['add', '--', ...files], { cwd: dir, timeoutMs: 10000 });
29690
+ gitStatusRunner.invalidate(dir);
29539
29691
  if (result.status !== 0) return res.status(500).json({ error: result.stderr?.toString?.()?.trim() || 'Stage failed' });
29540
29692
  } else {
29541
29693
  // Stage all changes
29542
- const result = spawnSync('git', ['add', '-A'], { cwd: dir, env: withGitFallback(process.env), stdio: 'pipe', timeout: 10000 });
29694
+ const result = await gitStatusRunner.runRaw(['add', '-A'], { cwd: dir, timeoutMs: 10000 });
29695
+ gitStatusRunner.invalidate(dir);
29543
29696
  if (result.status !== 0) return res.status(500).json({ error: result.stderr?.toString?.()?.trim() || 'Stage failed' });
29544
29697
  }
29545
29698
 
29546
29699
  // Commit
29547
- const result = spawnSync('git', ['commit', '-m', message.trim()], { cwd: dir, env: withGitFallback(process.env), stdio: 'pipe', timeout: 15000 });
29700
+ const result = await gitStatusRunner.runRaw(['commit', '-m', message.trim()], { cwd: dir, timeoutMs: 15000 });
29701
+ gitStatusRunner.invalidate(dir);
29548
29702
  if (result.status !== 0) {
29549
29703
  const stderr = result.stderr?.toString?.()?.trim() || '';
29550
29704
  const stdout = result.stdout?.toString?.()?.trim() || '';
@@ -29558,32 +29712,32 @@ app.post('/api/git/commit', editorOwnerGuard, (req, res) => {
29558
29712
  }
29559
29713
  });
29560
29714
 
29561
- app.get('/api/git/diff-summary', editorOwnerGuard, (req, res) => {
29715
+ app.get('/api/git/diff-summary', editorOwnerGuard, async (req, res) => {
29562
29716
  const asked = req.query.path;
29563
29717
  if (!asked || typeof asked !== 'string') return res.status(400).json({ error: 'path required' });
29564
29718
  const dir = registeredGitFolder(asked, res);
29565
29719
  if (!dir) return;
29566
29720
 
29567
29721
  try {
29568
- gitOutput(dir, ['rev-parse', '--is-inside-work-tree'], { timeout: 3000 });
29722
+ await gitOutput(dir, ['rev-parse', '--is-inside-work-tree'], { timeout: 3000 });
29569
29723
  } catch {
29570
29724
  return res.status(400).json({ error: 'Not a git repository' });
29571
29725
  }
29572
29726
 
29573
29727
  try {
29574
29728
  // Get diff stat for tracked files
29575
- const stat = gitOutput(dir, ['diff', '--stat', '--stat-width=120']).trim();
29729
+ const stat = (await gitOutput(dir, ['diff', '--stat', '--stat-width=120'])).trim();
29576
29730
 
29577
29731
  // Get diff stat for staged files
29578
- const stagedStat = gitOutput(dir, ['diff', '--cached', '--stat', '--stat-width=120']).trim();
29732
+ const stagedStat = (await gitOutput(dir, ['diff', '--cached', '--stat', '--stat-width=120'])).trim();
29579
29733
 
29580
29734
  // Get untracked files
29581
- const untracked = gitOutput(dir, ['ls-files', '--others', '--exclude-standard']).trim();
29735
+ const untracked = (await gitOutput(dir, ['ls-files', '--others', '--exclude-standard'])).trim();
29582
29736
 
29583
29737
  // 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);
29738
+ const diff = (await gitOutput(dir, ['diff', '-U1', '--no-color'], { timeout: 8000, maxBuffer: 64 * 1024 })).substring(0, 4000);
29585
29739
 
29586
- const stagedDiff = gitOutput(dir, ['diff', '--cached', '-U1', '--no-color'], { timeout: 8000, maxBuffer: 64 * 1024 }).substring(0, 4000);
29740
+ const stagedDiff = (await gitOutput(dir, ['diff', '--cached', '-U1', '--no-color'], { timeout: 8000, maxBuffer: 64 * 1024 })).substring(0, 4000);
29587
29741
 
29588
29742
  // Build a commit message from the changes
29589
29743
  const changedFiles = [];
@@ -29651,7 +29805,7 @@ app.use(createGitDiffRouter({ fileRoots: allowedFileRoots, guard: editorOwnerGua
29651
29805
 
29652
29806
  // ── Git Log ──
29653
29807
 
29654
- app.get('/api/git/log', editorOwnerGuard, (req, res) => {
29808
+ app.get('/api/git/log', editorOwnerGuard, async (req, res) => {
29655
29809
  const asked = req.query.path;
29656
29810
  if (!asked || typeof asked !== 'string') return res.status(400).json({ error: 'path required' });
29657
29811
  const dir = registeredGitFolder(asked, res);
@@ -29659,13 +29813,13 @@ app.get('/api/git/log', editorOwnerGuard, (req, res) => {
29659
29813
  const count = Math.min(Math.max(parseInt(req.query.count) || 10, 1), 50);
29660
29814
 
29661
29815
  try {
29662
- gitOutput(dir, ['rev-parse', '--is-inside-work-tree'], { timeout: 3000 });
29816
+ await gitOutput(dir, ['rev-parse', '--is-inside-work-tree'], { timeout: 3000 });
29663
29817
  } catch {
29664
29818
  return res.status(400).json({ error: 'Not a git repository' });
29665
29819
  }
29666
29820
 
29667
29821
  try {
29668
- const raw = gitOutput(dir, ['log', '--oneline', '--no-decorate', '-n', String(count)]).trim();
29822
+ const raw = (await gitOutput(dir, ['log', '--oneline', '--no-decorate', '-n', String(count)])).trim();
29669
29823
  const commits = raw ? raw.split('\n').map(line => {
29670
29824
  const spaceIdx = line.indexOf(' ');
29671
29825
  return { hash: line.substring(0, spaceIdx), message: line.substring(spaceIdx + 1) };
@@ -29683,17 +29837,17 @@ app.post('/api/git/generate-message', editorOwnerGuard, async (req, res) => {
29683
29837
  if (!dir || typeof dir !== 'string') return res.status(400).json({ error: 'path required' });
29684
29838
 
29685
29839
  try {
29686
- gitOutput(dir, ['rev-parse', '--is-inside-work-tree'], { timeout: 3000 });
29840
+ await gitOutput(dir, ['rev-parse', '--is-inside-work-tree'], { timeout: 3000 });
29687
29841
  } catch {
29688
29842
  return res.status(400).json({ error: 'Not a git repository' });
29689
29843
  }
29690
29844
 
29691
29845
  // Gather diff data — separate try/catch per command so one failure doesn't skip the rest
29692
29846
  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 {}
29847
+ try { diffText = (await gitOutput(dir, ['diff', '--no-color'], { timeout: 8000, maxBuffer: 2 * 1024 * 1024 })).substring(0, 4000); } catch {}
29848
+ try { stagedText = (await gitOutput(dir, ['diff', '--cached', '--no-color'], { timeout: 8000, maxBuffer: 2 * 1024 * 1024 })).substring(0, 4000); } catch {}
29849
+ try { untrackedList = (await gitOutput(dir, ['ls-files', '--others', '--exclude-standard'])).trim(); } catch {}
29850
+ try { recentCommits = (await gitOutput(dir, ['log', '--oneline', '--no-decorate', '-n', '5'], { timeout: 3000 })).trim(); } catch {}
29697
29851
 
29698
29852
  const allDiff = (diffText + '\n' + stagedText).trim();
29699
29853
  if (!allDiff && !untrackedList) {
@@ -29744,8 +29898,8 @@ app.post('/api/git/generate-message', editorOwnerGuard, async (req, res) => {
29744
29898
  // Fallback: heuristic (same logic as /api/git/diff-summary)
29745
29899
  try {
29746
29900
  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 {}
29901
+ try { stat = (await gitOutput(dir, ['diff', '--stat', '--stat-width=120'], { maxBuffer: 2 * 1024 * 1024 })).trim(); } catch {}
29902
+ try { stagedStat = (await gitOutput(dir, ['diff', '--cached', '--stat', '--stat-width=120'], { maxBuffer: 2 * 1024 * 1024 })).trim(); } catch {}
29749
29903
  const changedFiles = [];
29750
29904
  for (const line of (stat + '\n' + stagedStat).split('\n').filter(l => l.includes('|'))) {
29751
29905
  const m = line.match(/^\s*(.+?)\s*\|/);
@@ -29823,7 +29977,7 @@ app.put('/api/cli/config', (req, res) => {
29823
29977
  app.post('/api/cli/detect/:profileId', async (req, res) => {
29824
29978
  try {
29825
29979
  const { profileId } = req.params;
29826
- if (!['claude-code', 'codex', 'gemini', 'opencode'].includes(profileId)) return res.status(400).json({ ok: false, error: 'Unknown profile' });
29980
+ if (!['claude-code', 'codex', 'antigravity', 'opencode'].includes(profileId)) return res.status(400).json({ ok: false, error: 'Unknown profile' });
29827
29981
  const status = await cliResolver.resolve(profileId, { force: true, configured: null });
29828
29982
  res.json({
29829
29983
  ok: true,
@@ -29873,7 +30027,7 @@ function stripAnsi(str) {
29873
30027
  // stdin: false — message appended as the last CLI arg (flag takes a value)
29874
30028
  const CLI_SIDECAR_CONFIG = {
29875
30029
  '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
30030
+ 'antigravity': { args: ['-p'], stdin: false }, // agy -p <prompt>: one print-mode turn
29877
30031
  'codex': { args: ['-q'], stdin: false }, // -q <msg>; flag takes a value
29878
30032
  'shell': null,
29879
30033
  };
@@ -29888,7 +30042,7 @@ const CLI_LIVE_CONFIG = {
29888
30042
  maxIdleMs: 120000, // 2 min absolute max wait
29889
30043
  useBracketedPaste: true,
29890
30044
  },
29891
- 'gemini': {
30045
+ 'antigravity': {
29892
30046
  promptPatterns: [/>\s*$/m, /\u276F\s*$/m],
29893
30047
  idleMs: 3000,
29894
30048
  maxIdleMs: 120000,
@@ -30005,7 +30159,7 @@ function spawnSidecarProcess(linkId, sessionId, message) {
30005
30159
  const sidecarConfig = CLI_SIDECAR_CONFIG[profile];
30006
30160
  if (!sidecarConfig) throw new Error(`No sidecar config for profile: ${profile}`);
30007
30161
 
30008
- // Get the CLI command from config (e.g. 'claude', 'gemini', 'codex')
30162
+ // Get the CLI command from config (e.g. 'claude', 'agy', 'codex')
30009
30163
  const cliConfig = loadCliConfig();
30010
30164
  const command = (cliConfig[profile]?.command || '').trim();
30011
30165
  if (!command) throw new Error(`No CLI command configured for ${profile}`);
@@ -30025,7 +30179,7 @@ function spawnSidecarProcess(linkId, sessionId, message) {
30025
30179
  // stdin-based: args are fixed, message piped after spawn (e.g. Claude Code)
30026
30180
  } else if (IS_WIN) {
30027
30181
  // 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".
30182
+ // causing CLIs that take a value after -p (agy) to misparse -p "hello world" as -p + positional "hello" "world".
30029
30183
  // Use PowerShell which correctly passes $msg as a single argument value.
30030
30184
  tmpFile = join(os.tmpdir(), `synabun-relay-${randomBytes(4).toString('hex')}.txt`);
30031
30185
  writeFileSync(tmpFile, message, 'utf-8');
@@ -32391,8 +32545,10 @@ async function createBrowserSession(options = {}) {
32391
32545
  || savedCfg.connectMode === 'morelogin';
32392
32546
  if (_wantMoreLogin) {
32393
32547
  try {
32394
- if (!moreLogin.isMoreLoginInstalled().installed) {
32395
- throw new Error('MoreLogin desktop app is not installed');
32548
+ // An install outside the known folders (a custom folder on Windows) still
32549
+ // counts when its local API answers.
32550
+ if (!moreLogin.isMoreLoginInstalled().installed && !(await moreLogin.probeMoreLoginApi({ savedCfg })).running) {
32551
+ throw new Error(`MoreLogin desktop app was not found and its local API does not answer on port ${moreLogin.getMoreLoginPort(savedCfg)}`);
32396
32552
  }
32397
32553
  // Installed but possibly not running (e.g. an autonomous schedule fires with
32398
32554
  // nobody at the machine to open it) — auto-launch and wait for its local API
@@ -33730,30 +33886,41 @@ app.use('/api/browser', (req, res, next) => { res.setHeader('X-Synabun-Browser-D
33730
33886
  // action, a new tab, CDP). Closing it, or listing its tabs, stays allowed.
33731
33887
  app.use('/api/browser/sessions/:id', (req, res, next) => {
33732
33888
  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
- });
33889
+ const exempt = req.method === 'DELETE' || (req.method === 'GET' && req.path === '/tabs');
33890
+ const gate = () => {
33891
+ const unsafe = protectedContext?.__browserNetworkGuardError;
33892
+ if (unsafe && !exempt) {
33893
+ return res.status(503).json({ error: `Browser network protection unavailable: ${unsafe}`, networkGuard: true });
33894
+ }
33895
+ // Also cancel requests already waiting/probing when the protection disappears.
33896
+ // Cancellation prevents their next page action; an action that already began
33897
+ // retains the existing truthful "outcome uncertain" response semantics.
33898
+ if (req.method !== 'DELETE' && protectedContext?.on) {
33899
+ const cancel = reason => req.browserRequest?.cancelForProtection?.(reason);
33900
+ protectedContext.on('synabun-network-guard-fault', cancel);
33901
+ const release = () => protectedContext.removeListener('synabun-network-guard-fault', cancel);
33902
+ res.once('finish', release);
33903
+ res.once('close', release);
33904
+ }
33905
+ if (!req.get('X-Synabun-Terminal') || exempt) return next();
33906
+ const session = browserSessions.get(req.params.id);
33907
+ if (!session) return next();
33908
+ const cfg = loadBrowserConfig();
33909
+ if (cfg.connectMode !== 'morelogin' || sessionServesMoreLoginDefault(session, cfg)) return next();
33910
+ const envId = moreLoginDefaultEnvId(cfg);
33911
+ res.status(409).json({
33912
+ 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.`,
33913
+ code: 'NOT_DEFAULT_BROWSER',
33914
+ });
33915
+ };
33916
+ // A faulted network guard re-arms on demand: a fresh protected attachment replaces
33917
+ // the broken one before this request runs, so a session never needs a server
33918
+ // restart to recover. Until a re-arm succeeds every action stays refused.
33919
+ if (protectedContext?.__browserNetworkGuardError && !exempt) {
33920
+ recoverBrowserNetworkGuard(protectedContext).then(gate).catch(next);
33921
+ return;
33922
+ }
33923
+ return gate();
33757
33924
  });
33758
33925
  // Read-only semantic context for the browser judgments (the judgments themselves run in the MCP layer).
33759
33926
  app.use('/api/browser', createBrowserAssistRouter({ browserSessions, getTargetPage, normalizeSelector }));
@@ -35560,7 +35727,7 @@ app.post('/api/browser/sessions/:id/upload', async (req, res) => {
35560
35727
  });
35561
35728
 
35562
35729
  // Browser config endpoint
35563
- app.get('/api/browser/config', (req, res) => {
35730
+ app.get('/api/browser/config', async (req, res) => {
35564
35731
  const config = loadBrowserConfig();
35565
35732
  const detected = findBrowserExecutable();
35566
35733
  // Check if Chrome is currently running (informs profile mirroring warnings)
@@ -35581,7 +35748,11 @@ app.get('/api/browser/config', (req, res) => {
35581
35748
  let attachSeeded = false;
35582
35749
  try { attachSeeded = existsSync(getAttachProfileRoot()); } catch {}
35583
35750
  let moreloginInstalled = false;
35584
- try { moreloginInstalled = moreLogin.isMoreLoginInstalled().installed; } catch {}
35751
+ // The exe in a known folder, or a local API that answers (a custom install folder): a short probe only when needed.
35752
+ try {
35753
+ moreloginInstalled = moreLogin.isMoreLoginPresent()
35754
+ || (await moreLogin.probeMoreLoginApi({ savedCfg: config, timeout: 1500 })).running;
35755
+ } catch {}
35585
35756
  const unreadable = browserConfigUnreadable(config);
35586
35757
  res.json({ config, detectedPath: detected, chromeRunning, attachSeeded, moreloginInstalled, ...(unreadable && { configError: `browser-config.json is unreadable (${unreadable}); saving these settings replaces it` }) });
35587
35758
  });
@@ -35626,12 +35797,20 @@ app.get('/api/browser/detect-profiles', (req, res) => {
35626
35797
  app.get('/api/browser/morelogin-envs', async (req, res) => {
35627
35798
  try {
35628
35799
  const cfg = loadBrowserConfig();
35629
- const installed = moreLogin.isMoreLoginInstalled().installed;
35630
35800
  const port = moreLogin.getMoreLoginPort(cfg);
35631
- if (!installed) return res.json({ ok: true, installed: false, running: false, port, envs: [] });
35801
+ // Probe first: the install-folder check knows only the default folders, and an
35802
+ // answering local API proves the app is there wherever it was installed.
35632
35803
  const probe = await moreLogin.probeMoreLoginApi({ savedCfg: cfg, port });
35804
+ const installed = probe.running || moreLogin.isMoreLoginInstalled().installed;
35805
+ if (!installed) return res.json({ ok: true, installed: false, running: false, port, envs: [], error: probe.error || null });
35633
35806
  if (!probe.running) return res.json({ ok: true, installed: true, running: false, port, envs: [], error: probe.error || null });
35807
+ // The API answered with an error (signature required, API off): say why, never an empty list.
35808
+ if (probe.apiError) return res.json({ ok: false, installed: true, running: true, port, envs: [], error: probe.apiError });
35634
35809
  const envs = await moreLogin.listProfiles({ savedCfg: cfg, port });
35810
+ // The API counted profiles but none could be read: a shape problem, never "no profiles yet".
35811
+ if (probe.count > 0 && !envs.length) {
35812
+ 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.` });
35813
+ }
35635
35814
  res.json({
35636
35815
  ok: true, installed: true, running: true, port, envs,
35637
35816
  // Observed website-policy blocks — MoreLogin's local API never exposes the
@@ -35882,10 +36061,11 @@ const httpServer = app.listen(PORT, async () => {
35882
36061
  // Activation is an explicit user choice via Settings → Browser / the MoreLogin tab.
35883
36062
  setTimeout(async () => {
35884
36063
  try {
35885
- if (!moreLogin.isMoreLoginInstalled().installed) return;
35886
- const cfg = loadBrowserConfig();
36064
+ const mlBootCfg = loadBrowserConfig();
36065
+ const cfg = mlBootCfg;
35887
36066
  const port = moreLogin.getMoreLoginPort(cfg);
35888
36067
  const probe = await moreLogin.probeMoreLoginApi({ savedCfg: cfg, port });
36068
+ if (!probe.running && !moreLogin.isMoreLoginInstalled().installed) return;
35889
36069
  const isDefault = cfg.connectMode === 'morelogin';
35890
36070
  console.log(` MoreLogin: detected${probe.running ? ` · API on :${port} (${probe.count} profile(s))` : ' · app not running'}${isDefault ? ' · default AI browser' : ''}`);
35891
36071
  } catch { /* best-effort */ }
@@ -35949,6 +36129,12 @@ const httpServer = app.listen(PORT, async () => {
35949
36129
  if (oc.updated > 0) console.log(` Session cache (opencode): indexed ${oc.updated}/${oc.sessions}`);
35950
36130
  } catch (err) { console.warn(' Session cache (opencode) error:', err.message); }
35951
36131
  },
36132
+ () => {
36133
+ const agy = refreshAntigravityCatalog({ force: true });
36134
+ if (agy?.updated > 0) console.log(` Session cache (antigravity): indexed ${agy.updated}/${agy.sessions}`);
36135
+ // Runs and brains end on their own: their conversations are picked up by this pass, every five minutes.
36136
+ setInterval(() => refreshAntigravityCatalog({ force: true }), 5 * 60 * 1000).unref?.();
36137
+ },
35952
36138
  ];
35953
36139
  const next = () => {
35954
36140
  const pass = passes.shift();
@@ -35988,6 +36174,18 @@ const httpServer = app.listen(PORT, async () => {
35988
36174
  console.warn(' Hook dedup warning:', err.message);
35989
36175
  }
35990
36176
 
36177
+ // Antigravity replaced Gemini CLI: what was saved under `gemini` (a CLI path, a keybind, loop templates and
36178
+ // schedules, the launch profile and resume tab in the UI state) is read as Antigravity's from this boot on
36179
+ // (lib/antigravity/migrate-gemini.js). Idempotent; a file it cannot read is left as it is. Never stops the boot.
36180
+ try {
36181
+ const migrated = migrateGeminiFiles({ dataHome: DATA_HOME, log: (line) => console.log(` ${line}`) });
36182
+ if (migrated.changed.includes('cli-config.json')) { _cliConfigCache = null; cliResolver.invalidate(); }
36183
+ const ui = loadUiState();
36184
+ if (migrateGeminiUiState(ui)) saveUiState(ui);
36185
+ } catch (err) {
36186
+ console.warn(' Antigravity migration warning:', err.message);
36187
+ }
36188
+
35991
36189
  // Keep the installed SynaBun rules current: an untouched copy of an older
35992
36190
  // ruleset is rewritten (unless the user switched that off), an edited copy is
35993
36191
  // left alone, a deleted one counts as an opt-out. Never installs for a tool
@@ -36003,6 +36201,16 @@ const httpServer = app.listen(PORT, async () => {
36003
36201
  console.warn(' Rules reconcile warning:', err.message);
36004
36202
  }
36005
36203
 
36204
+ // SynaBun's Antigravity homes (lib/antigravity/home.js): a run's home lives a week past its last use (a run resumes
36205
+ // within minutes of a restart), an Assistant brain's two months (its conversation can be picked up later).
36206
+ try {
36207
+ const root = antigravityHomesRoot(DATA_HOME);
36208
+ const removed = [...pruneAntigravityHomes(root, { kinds: ['run', 'models'], keep: antigravityPanel.activeHomes() }), ...pruneAntigravityHomes(root, { kinds: ['brain'], maxAgeMs: 60 * 24 * 60 * 60_000 })];
36209
+ if (removed.length) console.log(` Antigravity: removed ${removed.length} unused SynaBun home${removed.length === 1 ? '' : 's'}`);
36210
+ } catch (err) {
36211
+ console.warn(' Antigravity homes warning:', err.message);
36212
+ }
36213
+
36006
36214
  // Clean up orphaned loop state files from previous server runs.
36007
36215
  // After a restart, PTY sessions are gone but loop files remain with awaitingNext: true.
36008
36216
  // No terminal session exists to drive them, so delete them to prevent ghost loops.
@@ -36134,7 +36342,7 @@ httpServer.on('upgrade', function onUpgrade(req, socket, head) {
36134
36342
  if (guest) {
36135
36343
  if (url.pathname.startsWith('/ws/terminal/') && !invitePermissions.terminal) { socket.destroy(); return; }
36136
36344
  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/')) {
36345
+ if (['/ws/claude-skin', '/ws/codex-skin', '/ws/opencode-v2', '/ws/antigravity'].includes(url.pathname) || url.pathname.startsWith('/ws/assistant/')) {
36138
36346
  socket.destroy();
36139
36347
  return;
36140
36348
  }
@@ -36165,7 +36373,7 @@ httpServer.on('upgrade', function onUpgrade(req, socket, head) {
36165
36373
  return;
36166
36374
  }
36167
36375
 
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') {
36376
+ 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
36377
  wss.handleUpgrade(req, socket, head, (ws) => {
36170
36378
  wss.emit('connection', ws, req);
36171
36379
  });
@@ -36232,6 +36440,12 @@ wss.on('connection', (ws, req) => {
36232
36440
  return;
36233
36441
  }
36234
36442
 
36443
+ // ── Route: Antigravity side panel (Google's agy over ACP) ──
36444
+ if (url.pathname === '/ws/antigravity') {
36445
+ antigravityPanel.handleSocket(ws);
36446
+ return;
36447
+ }
36448
+
36235
36449
  // ── Terminal sessions ── are handed off to the pty-host in the upgrade
36236
36450
  // handler (lib/pty-host); nothing else should reach this point.
36237
36451
  try { ws.close(1008, 'Unsupported WebSocket path'); } catch {}
@@ -36816,11 +37030,15 @@ process.on('exit', () => {
36816
37030
  try { stopOpencodeServer(); } catch {}
36817
37031
  try { editorCompletionServe.stop('exit'); } catch {}
36818
37032
  try { _claudeBridge?.shutdownAllBridges?.(); } catch {}
37033
+ try { antigravityPanel.closeAll(); } catch {}
36819
37034
  try { saveSessionSnapshot(); } catch {}
36820
37035
  try { flushUiStateSync(); } catch {}
36821
37036
  try { flushWhiteboardSync(); } catch {}
36822
37037
  try { flushCostDataSync(); } catch {}
36823
37038
  try { memoryMap.flushSync(); } catch {}
37039
+ // The Assistant's session records (a rename among them): this handler runs before the Assistant's own
37040
+ // exit handler, which the SIGKILL below never lets run.
37041
+ try { _assistantRuntime?.flushIndex?.(); } catch {}
36824
37042
  if (_preserveExitCode) return;
36825
37043
  try { process.kill(process.pid, 'SIGKILL'); } catch {}
36826
37044
  });