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
@@ -11,14 +11,15 @@
11
11
  //
12
12
  // Invocation is kept as intent, apart from any one runtime's spelling of it: who may start the skill (the user
13
13
  // by name, the model on its own), read from every dialect a folder carries (Claude Code fields, Codex's
14
- // agents/openai.yaml, OpenCode's metadata) and compared with what a target would read from the exported bytes,
15
- // so an install never lets a model start a skill its author kept explicit-only (invocationPlan).
14
+ // agents/openai.yaml, OpenCode's metadata, Antigravity's disable-slash-command) and compared with what a target
15
+ // would read from the exported bytes, so an install never lets a model start a skill its author kept
16
+ // explicit-only (invocationPlan).
16
17
  //
17
18
  // No DOM, no I/O: the page and the server load it; YAML is parsed by the caller.
18
19
 
19
20
  import { findHiddenChars } from './ss-unicode.js';
20
21
 
21
- export const PROFILE_SET_VERSION = '2026-10-22';
22
+ export const PROFILE_SET_VERSION = '2026-10-30';
22
23
 
23
24
  const RETRIEVED = '2026-10-21';
24
25
  const SOURCES = Object.freeze({
@@ -29,6 +30,7 @@ const SOURCES = Object.freeze({
29
30
  codexPrompts: { title: 'Codex custom prompts (deprecated)', url: 'https://learn.chatgpt.com/docs/custom-prompts', retrieved: RETRIEVED },
30
31
  opencode: { title: 'OpenCode skills', url: 'https://opencode.ai/docs/skills/', retrieved: RETRIEVED },
31
32
  opencodeCommands: { title: 'OpenCode commands', url: 'https://opencode.ai/docs/commands/', retrieved: RETRIEVED },
33
+ antigravity: { title: 'Antigravity CLI reference', url: 'https://antigravity.google/docs/cli/reference', retrieved: '2026-10-30' },
32
34
  });
33
35
 
34
36
  export const PORTABLE_FIELDS = Object.freeze(['name', 'description', 'license', 'compatibility', 'metadata', 'allowed-tools']);
@@ -39,6 +41,9 @@ export const CLAUDE_FIELDS = Object.freeze([
39
41
  ]);
40
42
  const OPENCODE_FIELDS = Object.freeze(['name', 'description', 'license', 'compatibility', 'metadata', 'disable-model-invocation']);
41
43
  const CODEX_FIELDS = Object.freeze(['name', 'description', 'metadata']);
44
+ const ANTIGRAVITY_FIELDS = Object.freeze(['name', 'description', 'disable-slash-command']);
45
+ /** The most of a SKILL.md body Antigravity reads (UTF-8 bytes after the frontmatter). */
46
+ export const ANTIGRAVITY_BODY_BYTES = 24 * 1024;
42
47
 
43
48
  export const PORTABLE_NAME_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
44
49
  export const CODEX_SIDECAR = 'agents/openai.yaml';
@@ -121,6 +126,19 @@ export const PROFILES = Object.freeze({
121
126
  ],
122
127
  reload: { file: 'new-session', newRoot: 'new-session', note: 'A running OpenCode may not see a new skill; start a new session (or restart the server) to be sure.' },
123
128
  }),
129
+ 'antigravity-native': Object.freeze({
130
+ id: 'antigravity-native', version: '2026-10-30', title: 'Antigravity', runtime: 'antigravity', status: 'current',
131
+ sources: [SOURCES.antigravity], verified: null,
132
+ fields: { recognized: ANTIGRAVITY_FIELDS, required: ['name', 'description'], strictTopLevel: false },
133
+ limits: { nameChars: 64, descriptionChars: 1024, bodyBytes: ANTIGRAVITY_BODY_BYTES },
134
+ invocation: { model: 'chosen by its description; no field keeps the model from starting it', user: '/name; `disable-slash-command: true` hides it' },
135
+ substitutions: null,
136
+ roots: [
137
+ { scope: 'user', path: '~/.gemini/config/skills', note: 'where SynaBun installs' },
138
+ { scope: 'project', path: '<project>/.agents/skills', note: 'the Codex project folder, which Antigravity reads too' },
139
+ ],
140
+ reload: { file: 'automatic', newRoot: 'automatic', note: 'Antigravity reads its skill folders again by itself; there is no command to run.' },
141
+ }),
124
142
  'codex-prompt': Object.freeze({
125
143
  id: 'codex-prompt', version: '2026-10-21', title: 'Codex custom prompt (deprecated)', runtime: 'codex', status: 'legacy',
126
144
  sources: [SOURCES.codexPrompts], verified: null,
@@ -144,7 +162,7 @@ export const PROFILES = Object.freeze({
144
162
  });
145
163
 
146
164
  /** The profile an install into a runtime is checked against. */
147
- export const TARGET_PROFILE = Object.freeze({ claude: 'claude-code', codex: 'codex-native', opencode: 'opencode-native', agents: 'portable-six-field' });
165
+ export const TARGET_PROFILE = Object.freeze({ claude: 'claude-code', codex: 'codex-native', opencode: 'opencode-native', antigravity: 'antigravity-native', agents: 'portable-six-field' });
148
166
 
149
167
  export function getProfile(id) {
150
168
  return Object.prototype.hasOwnProperty.call(PROFILES, id) ? PROFILES[id] : null;
@@ -162,6 +180,11 @@ export function profileList() {
162
180
 
163
181
  const has = (data, key) => !!data && Object.prototype.hasOwnProperty.call(data, key);
164
182
  const isMap = (v) => !!v && typeof v === 'object' && !Array.isArray(v);
183
+ const ENCODER = new TextEncoder();
184
+ /** A text's length in UTF-8 bytes (0 for anything else). */
185
+ const utf8Length = (text) => (typeof text === 'string' ? ENCODER.encode(text).length : 0);
186
+ /** "24 KB", "25.3 KB", "812 bytes". */
187
+ const kb = (bytes) => (bytes < 1024 ? `${bytes} bytes` : `${Math.round((bytes / 1024) * 10) / 10} KB`);
165
188
  const kindOf = (value) => {
166
189
  if (value === undefined) return 'absent';
167
190
  if (value === null) return 'empty';
@@ -202,9 +225,11 @@ export function readInvocationIntent({ data, sidecar = null }) {
202
225
  const meta = isMap(data?.metadata) ? data.metadata : null;
203
226
  if (meta && has(meta, 'opencode/autoinvoke')) said.opencodeModel = openSwitch(meta['opencode/autoinvoke']);
204
227
  if (meta && has(meta, 'opencode/slash')) said.opencodeSlash = openSwitch(meta['opencode/slash']);
228
+ // Antigravity's `disable-slash-command: true` keeps /name from the person, as Claude Code's user-invocable: false does.
229
+ if (has(data, 'disable-slash-command')) said.antigravityUser = claudeBoolean(data['disable-slash-command']) === true ? false : (claudeBoolean(data['disable-slash-command']) === false ? true : null);
205
230
  const modelSays = [said.claudeModel, said.codexModel, said.opencodeModel].filter((v) => v === true || v === false);
206
231
  const model = !modelSays.includes(false);
207
- const user = said.claudeUser !== false;
232
+ const user = said.claudeUser !== false && said.antigravityUser !== false;
208
233
  const conflicts = [];
209
234
  if (modelSays.includes(false) && modelSays.includes(true)) conflicts.push('model');
210
235
  return { user, model, slash: said.opencodeSlash ?? null, said, conflicts };
@@ -228,6 +253,10 @@ export function effectiveInvocation(profileId, { data, sidecar = null }) {
228
253
  const claudeOff = claudeBoolean(data?.['disable-model-invocation']) === true;
229
254
  return { model: !(openSwitch(meta?.['opencode/autoinvoke']) === false || claudeOff), user: true, slash: openSwitch(meta?.['opencode/slash']) === true };
230
255
  }
256
+ if (runtime === 'antigravity') {
257
+ // Only a YAML true is taken as hiding /name (anything else is not counted on); nothing keeps its model from starting a skill.
258
+ return { model: true, user: data?.['disable-slash-command'] !== true };
259
+ }
231
260
  return { model: true, user: true };
232
261
  }
233
262
 
@@ -254,6 +283,7 @@ export function invocationPlan(profileId, { intent, data, sidecar = null }) {
254
283
  }
255
284
  if (!intent.user && now.user) {
256
285
  if (runtime === 'claude') transforms.push({ file: 'SKILL.md', op: 'frontmatter', key: 'user-invocable', value: false, reason: 'Keeps /name hidden in Claude Code, as the skill\'s other settings say.' });
286
+ else if (runtime === 'antigravity') transforms.push({ file: 'SKILL.md', op: 'frontmatter', key: 'disable-slash-command', value: true, reason: 'Keeps /name hidden in Antigravity, as the skill\'s other settings say.' });
257
287
  else diagnostics.push({ ruleId: 'L-08-user-invocation-broader', severity: 'warning', message: `${profile.title} has no setting that hides a skill from the person: there it can still be started by name.`, file: 'SKILL.md', line: null });
258
288
  }
259
289
  return { transforms, diagnostics };
@@ -306,7 +336,7 @@ export function lintForProfile(profileId, { parsed, folderName = '', headPath =
306
336
  if (hidden.length || /[<>]/.test(name)) add('L-03-name-chars', 'error', `"name" holds characters ${profile.title} refuses (${hidden.length ? hidden.map((h) => h.label).join(', ') : 'an angle bracket'}).`, lineOf('name'));
307
337
  const portableName = PORTABLE_NAME_RE.test(name);
308
338
  if (!portableName) {
309
- if (strictPortable || runtime === 'opencode') add('L-03-name-format', 'error', `"name" must be lowercase letters, digits and single hyphens for ${profile.title}.`, lineOf('name'));
339
+ if (strictPortable || runtime === 'opencode' || runtime === 'antigravity') add('L-03-name-format', 'error', `"name" must be lowercase letters, digits and single hyphens for ${profile.title}.`, lineOf('name'));
310
340
  else add('L-03-name-format', 'warning', `"name" is not lowercase letters, digits and single hyphens: other runtimes may refuse it.`, lineOf('name'));
311
341
  }
312
342
  if (folderName && name !== folderName) {
@@ -398,6 +428,14 @@ export function lintForProfile(profileId, { parsed, folderName = '', headPath =
398
428
  if (has(data, 'hooks') && !isMap(data.hooks)) add('L-04-hooks-type', 'error', `"hooks" must be a mapping of events; it is ${kindOf(data.hooks)}.`, lineOf('hooks'));
399
429
  }
400
430
 
431
+ // Antigravity's own field and limit
432
+ if (runtime === 'antigravity') {
433
+ if (has(data, 'disable-slash-command') && typeof data['disable-slash-command'] !== 'boolean') add('L-04-disable-slash-command-type', 'error', `"disable-slash-command" must be true or false; it is ${kindOf(data['disable-slash-command'])}.`, lineOf('disable-slash-command'));
434
+ const bytes = utf8Length(parsed?.body);
435
+ const max = profile.limits.bodyBytes || ANTIGRAVITY_BODY_BYTES;
436
+ if (bytes > max) add('L-03-body-size', 'error', `The body is ${kb(bytes)}; ${profile.title} reads ${kb(max)} at most. Move detail into files the skill points to.`, parsed?.bodyLine || null);
437
+ }
438
+
401
439
  // Keys the profile does not read
402
440
  for (const key of Object.keys(data)) {
403
441
  if (profile.fields.recognized.includes(key)) continue;
@@ -20,7 +20,7 @@ export const REVIEW_STATE_LABEL = Object.freeze({
20
20
  withdrawn: 'Withdrawn',
21
21
  });
22
22
 
23
- export const RUNTIME_LABEL = Object.freeze({ claude: 'Claude Code', codex: 'Codex', opencode: 'OpenCode', agents: 'Shared (~/.agents/skills)' });
23
+ export const RUNTIME_LABEL = Object.freeze({ claude: 'Claude Code', codex: 'Codex', opencode: 'OpenCode', antigravity: 'Antigravity', agents: 'Shared (~/.agents/skills)' });
24
24
 
25
25
  /** A target as a person reads it: the runtime, and the project or the account when it is not the default one. */
26
26
  export function targetLabel(t) {
@@ -123,7 +123,7 @@ export function targetChoices({ sharedAgentsRoot = false } = {}, suggested = [])
123
123
  choices.push({ key, runtime: target.runtime, target, label: targetLabel(target), on: true });
124
124
  }
125
125
  const plainAsked = new Set(asked.filter(plainTarget).map((t) => t.runtime));
126
- for (const runtime of ['claude', 'codex', 'opencode', ...(sharedAgentsRoot ? ['agents'] : [])]) {
126
+ for (const runtime of ['claude', 'codex', 'opencode', 'antigravity', ...(sharedAgentsRoot ? ['agents'] : [])]) {
127
127
  if (plainAsked.has(runtime)) continue;
128
128
  const target = { runtime };
129
129
  choices.push({ key: JSON.stringify(target), runtime, target, label: targetLabel(target), on: !asked.length && runtime === 'claude' });
@@ -165,7 +165,7 @@ export async function deleteFromLibrary(ctx, ws) {
165
165
  if (!ctx.alive(gen)) return;
166
166
  const installs = skill.installations || [];
167
167
  if (installs.length) {
168
- const where = installs.slice(0, 6).map((i) => `• ${t(`skillsStudio.runtime.${['claude', 'codex', 'opencode', 'agents'].includes(i.runtime) ? i.runtime : 'claude'}`)} · ${i.destination || ''}`);
168
+ const where = installs.slice(0, 6).map((i) => `• ${t(`skillsStudio.runtime.${['claude', 'codex', 'opencode', 'antigravity', 'agents'].includes(i.runtime) ? i.runtime : 'claude'}`)} · ${i.destination || ''}`);
169
169
  const answer = await ctx.ask({
170
170
  title: t('skillsStudio.ws.libraryDeleteInstalledTitle', { name: ws.label }),
171
171
  lines: [tp('skillsStudio.ws.libraryDeleteInstalled', installs.length), ...where],
@@ -25,7 +25,7 @@ import {
25
25
 
26
26
  const PROMPT_KEY = 'skills-studio-create-prompt';
27
27
  const SCAN_COUNTED = ['blocked', 'incomplete', 'review', 'findings'];
28
- const PROVIDER_RUNTIME = new Map([['claude-code', 'claude'], ['codex', 'codex'], ['opencode', 'opencode']]);
28
+ const PROVIDER_RUNTIME = new Map([['claude-code', 'claude'], ['codex', 'codex'], ['opencode', 'opencode'], ['antigravity', 'antigravity']]);
29
29
 
30
30
  // What the box keeps between drawings of the place (the form itself is kept in storage).
31
31
  const AI = {
@@ -13,7 +13,8 @@ import { CREATION_LIMITS, DRAFT_PROVIDERS, isActiveCreation, normalizeCreationRe
13
13
  export const AI_FORM_KEY = 'skills-studio-create-ai';
14
14
  /** The places a source is added from (describe is the prompt itself). */
15
15
  export const AI_SOURCE_KINDS = Object.freeze(['conversation', 'url', 'repo', 'memory', 'import']);
16
- export const DRAFT_RUNTIMES = Object.freeze(['claude', 'codex', 'opencode']);
16
+ /** The runtimes a drafted skill is meant for (its install targets, ss-creation.js CREATION_TARGET_RUNTIMES), not who drafts it. */
17
+ export const DRAFT_RUNTIMES = Object.freeze(['claude', 'codex', 'opencode', 'antigravity']);
17
18
  export const WORK_MINUTES = Object.freeze([20, 30, 45, 60, 90]);
18
19
  /** The steps a creation shows, in order. */
19
20
  export const CREATION_STEPS = Object.freeze(['sources', 'drafting', 'checking', 'review']);
@@ -97,7 +97,8 @@ export function healthIcon(code) {
97
97
  case 'invalid': case 'not-text': return ICONS.err;
98
98
  case 'warnings': return ICONS.warn;
99
99
  case 'update': return ICONS.update;
100
- case 'shadowed': case 'twice': case 'same-description': return ICONS.flag;
100
+ case 'shadowed': case 'twice': case 'same-description': case 'duplicate-record': return ICONS.flag;
101
+ case 'stale-install': return ICONS.update;
101
102
  case 'bridge': return ICONS.bridge;
102
103
  case 'readonly': return ICONS.lock;
103
104
  case 'draft': return ICONS.draft;
@@ -0,0 +1,359 @@
1
+ // ═══════════════════════════════════════════
2
+ // SynaBun Neural Interface — Skills Studio 2.0: the fix panel and "Fix safe issues"
3
+ // ═══════════════════════════════════════════
4
+ //
5
+ // A health pill of the Library (or of a workspace's head) opens the fix panel of its row: every problem in plain
6
+ // words, what each fix does, and its buttons (sx-fixes.js says which). The Library's attention notice offers
7
+ // "Fix safe issues…": every one-press repair is read first and listed (what will be added, which record archived),
8
+ // nothing is written until the owner applies that list, and what was done or not is said item by item.
9
+ // Every write is a Studio API call (X-SynaBun-UI): a library skill's text goes to its draft (PATCH /v2/drafts), an
10
+ // agent's file is saved over the revision it was read at, a second library record is archived (Restore brings it
11
+ // back). Install locations change only through Publish and the existing remove flows.
12
+
13
+ import { studioErrorText } from './sx-errors.js';
14
+ import { subjectOf } from './sx-subject.js';
15
+ import { issuesOf, planTextFix, safeFixCandidates, issueCounts, fingerprintOf } from './sx-fixes.js';
16
+ import { healthWords } from './sx-library-view.js';
17
+ import { HEAD } from './sx-docs.js';
18
+ import { createFrontmatter } from '../ss-frontmatter.js';
19
+ import { lintArtifact } from '../ss-lint.js';
20
+ import { html, setHTML, ICONS, healthIcon } from './sx-dom.js';
21
+
22
+ const YAML_MODULE = '/vendor/yaml/yaml-2.9.1.min.js';
23
+ let _fm = null;
24
+ const frontmatter = async () => (_fm ||= createFrontmatter(await import(YAML_MODULE)));
25
+
26
+ /** The most text repairs "Fix safe issues" reads in one go (each is one or two reads). */
27
+ export const MAX_TEXT_READS = 40;
28
+
29
+ /** What a row allows the fix panel to do. */
30
+ export function canOf(ctx, deleteKind) {
31
+ return (row) => {
32
+ const s = subjectOf(row, { guest: ctx.guest, caps: ctx.caps });
33
+ const library = s.mode === 'library' && !!row.skill?.skillId;
34
+ return {
35
+ edit: !ctx.guest && !row.binary && (library || (s.mode === 'inplace' && !!s.v1Id && row.kind === 'agent')),
36
+ open: true,
37
+ delete: !!deleteKind?.(ctx, row, s),
38
+ archive: !ctx.guest && (!ctx.capsKnown || ctx.caps.has('library-delete')),
39
+ dismiss: !ctx.guest,
40
+ };
41
+ };
42
+ }
43
+
44
+ /**
45
+ * Read a row's head file and the safe repair of it, without writing anything.
46
+ * @returns {Promise<{ changes: object[], apply: () => Promise<void> } | null>} null: nothing safe to do
47
+ */
48
+ export async function readTextFix(ctx, row) {
49
+ const { api } = ctx;
50
+ const s = subjectOf(row, { guest: ctx.guest, caps: ctx.caps });
51
+ if (s.mode === 'inplace' && s.v1Id && row.kind === 'agent') {
52
+ const got = await api.fetchSkillsArtifact(s.v1Id);
53
+ if (got.binary || got.readOnly) return null;
54
+ const plan = planTextFix({ raw: got.rawContent, diagnostics: got.diagnostics, type: 'agent', folderName: row.folderName });
55
+ if (!plan) return null;
56
+ return { changes: plan.changes, apply: () => api.saveSkillsArtifact(s.v1Id, plan.text, { expectedRevision: got.revision }) };
57
+ }
58
+ if (s.mode !== 'library' || !row.skill?.skillId) return null;
59
+ const skill = (await api.fetchSkill(row.skill.skillId)).skill;
60
+ let draft = null;
61
+ if (skill.pointers?.draft) {
62
+ try { draft = (await api.fetchSkillsDraft(skill.pointers.draft)).draft; } catch (err) { if (err?.status !== 404) throw err; }
63
+ }
64
+ const latest = skill.pointers?.latest || null;
65
+ const file = draft ? await api.fetchSkillsDraftFile(draft.draftId, HEAD) : latest ? await api.fetchSkillsRevisionFile(latest, HEAD) : null;
66
+ if (!file || typeof file.content !== 'string') return null;
67
+ const fm = await frontmatter();
68
+ const folderName = String(skill.name || row.folderName || '');
69
+ const lint = lintArtifact({ type: 'skill', parsed: fm.parse(file.content), folderName });
70
+ const plan = planTextFix({ raw: file.content, diagnostics: lint.all, type: 'skill', folderName });
71
+ if (!plan) return null;
72
+ return {
73
+ changes: plan.changes,
74
+ apply: async () => {
75
+ const target = draft || (await api.createSkillsDraft({ skillId: skill.skillId, baseRevisionId: latest }, { idempotencyKey: ctx.key('fix-draft') })).draft;
76
+ await api.patchSkillsDraft(target.draftId, { expectedEtag: target.etag, ops: [{ op: 'put', path: HEAD, content: plan.text }] });
77
+ },
78
+ };
79
+ }
80
+
81
+ /** Archive the second library record of a skill (Restore in the Archived view brings it back). */
82
+ const archiveRecord = (ctx, row) => ctx.api.deleteLibrarySkill(row.skill.skillId, row.skill.etag ? { expectedEtag: row.skill.etag } : {});
83
+
84
+ /** The words of a step of the preview. */
85
+ function stepWords(ctx, step) {
86
+ const { t } = ctx;
87
+ if (step.kind === 'archive') return t('skillsStudio.fix.step.archive', { name: step.row.label, original: step.issue.health.name || '' });
88
+ return t('skillsStudio.fix.step.text', { name: step.row.label, added: step.changes.map((c) => c.added).join(' · ') });
89
+ }
90
+
91
+ /** Say what was done (a notice kept above the list until OK, a toast, the live region). */
92
+ function report(ctx, done, failed) {
93
+ const { t, tp } = ctx;
94
+ const line = failed.length ? t('skillsStudio.fix.resultPartial', { done: done.length, failed: failed.length, first: failed[0] }) : tp('skillsStudio.fix.resultDone', done.length);
95
+ ctx.lib.fixNotice = { kind: failed.length ? 'warn' : 'info', line, lines: [...done, ...failed] };
96
+ ctx.toast(line, failed.length ? 'error' : 'info');
97
+ ctx.announce(line);
98
+ }
99
+
100
+ /**
101
+ * "Fix safe issues…": read every candidate, list exactly what would change, apply only that list on the owner's word.
102
+ * @param {object} ctx @param {object[]} rows the Library's rows @param {Function} deleteKind
103
+ */
104
+ export async function fixAllSafe(ctx, rows, deleteKind) {
105
+ const { t, tp } = ctx;
106
+ const { steps } = safeFixCandidates(rows, canOf(ctx, deleteKind));
107
+ const gen = ctx.gen();
108
+ ctx.toast(t('skillsStudio.fix.reading'));
109
+ const ready = []; const unreadable = [];
110
+ let reads = 0;
111
+ for (const step of steps) {
112
+ if (step.kind !== 'text') { ready.push(step); continue; }
113
+ if (reads++ >= MAX_TEXT_READS) break;
114
+ try {
115
+ const fix = await readTextFix(ctx, step.row);
116
+ if (fix) ready.push({ ...step, changes: fix.changes, apply: fix.apply });
117
+ } catch (err) { unreadable.push(`${step.row.label}: ${studioErrorText(t, err)}`); }
118
+ if (!ctx.alive(gen)) return;
119
+ }
120
+ if (!ctx.alive(gen)) return;
121
+ const list = document.createElement('ul');
122
+ list.className = 'sx-fix-preview';
123
+ for (const step of ready) {
124
+ const li = document.createElement('li');
125
+ li.className = 'sx-fix-preview-row';
126
+ li.textContent = stepWords(ctx, step);
127
+ list.append(li);
128
+ }
129
+ if (!ready.length) {
130
+ await ctx.ask({ title: t('skillsStudio.fix.previewTitle'), lines: [t('skillsStudio.fix.previewNothing'), ...unreadable], buttons: [{ id: 'ok', label: t('skillsStudio.dialog.close'), kind: 'primary' }], cancelId: 'ok' });
131
+ return;
132
+ }
133
+ const answer = await ctx.ask({
134
+ title: t('skillsStudio.fix.previewTitle'),
135
+ lines: [t('skillsStudio.fix.previewIntro'), ...unreadable],
136
+ extra: list,
137
+ wide: true,
138
+ buttons: [{ id: 'cancel', label: t('skillsStudio.dialog.cancel') }, { id: 'apply', label: tp('skillsStudio.fix.apply', ready.length), kind: 'primary' }],
139
+ focus: 'apply',
140
+ });
141
+ if (answer !== 'apply' || !ctx.alive(gen)) return;
142
+ const done = []; const failed = [];
143
+ for (const step of ready) {
144
+ try {
145
+ if (step.kind === 'archive') await archiveRecord(ctx, step.row); else await step.apply();
146
+ done.push(stepWords(ctx, step));
147
+ } catch (err) { failed.push(`${step.row.label}: ${studioErrorText(t, err)}`); }
148
+ }
149
+ if (!ctx.alive(gen)) return;
150
+ report(ctx, done, failed);
151
+ await ctx.reload();
152
+ }
153
+
154
+ /** One issue as a card of the panel: its word, what it means, how it is fixed, and its buttons. */
155
+ function issueCard(ctx, row, issue, index) {
156
+ const { t } = ctx;
157
+ const card = document.createElement('section');
158
+ card.className = `sx-fix-card sx-fix-card--${issue.sev}`;
159
+ card.setAttribute('aria-labelledby', `sx-fix-${index}-title`);
160
+ const head = document.createElement('div');
161
+ head.className = 'sx-fix-card-head';
162
+ setHTML(head, html`${healthIcon(issue.code)}<strong id="sx-fix-${index}-title">${healthWords(issue.health, t)}</strong><span class="sx-fix-mode sx-fix-mode--${issue.mode}">${issue.mode === 'auto' ? t('skillsStudio.fix.mode.auto') : t('skillsStudio.fix.mode.guided')}</span>`);
163
+ const explain = document.createElement('p');
164
+ explain.className = 'sx-fix-explain';
165
+ explain.textContent = explainOf(ctx, row, issue);
166
+ card.append(head, explain);
167
+ const bar = document.createElement('div');
168
+ bar.className = 'sx-fix-actions';
169
+ for (const action of issue.actions) {
170
+ const b = document.createElement('button');
171
+ b.type = 'button';
172
+ b.className = `sx-btn sx-btn--sm${action === 'dismiss' ? ' sx-btn--ghost' : action === 'delete' ? ' sx-btn--danger' : (action === 'auto-text' || action === 'archive-duplicate') ? ' sx-btn--primary' : ''}`;
173
+ b.dataset.fix = `${index}:${action}`;
174
+ b.textContent = t(`skillsStudio.fix.action.${action}`);
175
+ bar.append(b);
176
+ }
177
+ if (!issue.actions.length) {
178
+ const p = document.createElement('p');
179
+ p.className = 'sx-dim';
180
+ p.textContent = t('skillsStudio.fix.nothingHere');
181
+ bar.append(p);
182
+ }
183
+ card.append(bar);
184
+ return card;
185
+ }
186
+
187
+ /** What a problem means for this row, in plain words. */
188
+ export function explainOf(ctx, row, issue) {
189
+ const { t } = ctx;
190
+ const h = issue.health;
191
+ const runtime = h.runtime ? t(`skillsStudio.runtime.${h.runtime}`) : '';
192
+ switch (issue.code) {
193
+ case 'shadowed': return t('skillsStudio.fix.explain.shadowed', { runtime: runtime || '—' });
194
+ case 'twice': return t('skillsStudio.fix.explain.twice', { runtime: runtime || '—' });
195
+ case 'same-description': return t('skillsStudio.fix.explain.same-description', { names: (h.names || []).join(', ') });
196
+ case 'duplicate-record': return `${t('skillsStudio.fix.explain.duplicate-record', { original: h.name || '' })}${h.why ? ` ${t(`skillsStudio.fix.duplicateWhy.${h.why}`)}` : ''}`;
197
+ default: return t(`skillsStudio.fix.explain.${issue.code}`);
198
+ }
199
+ }
200
+
201
+ /**
202
+ * The fix panel of one row. Resolves when the owner is done (a fix applied, a place opened, or closed).
203
+ * @param {object} ctx @param {object} row @param {{ deleteKind: Function, bulkDelete: Function }} hooks
204
+ */
205
+ export async function openFixPanel(ctx, row, { deleteKind, bulkDelete } = {}) {
206
+ const { t } = ctx;
207
+ const issues = issuesOf(row, canOf(ctx, deleteKind)(row));
208
+ const box = document.createElement('div');
209
+ box.className = 'sx-fix-panel';
210
+ if (!issues.length) {
211
+ const p = document.createElement('p');
212
+ p.textContent = t('skillsStudio.fix.none');
213
+ box.append(p);
214
+ }
215
+ issues.forEach((issue, i) => box.append(issueCard(ctx, row, issue, i)));
216
+ const answer = await ctx.ask({
217
+ title: t('skillsStudio.fix.title', { name: row.label }),
218
+ lines: issues.length ? [t('skillsStudio.fix.intro')] : [],
219
+ extra: box,
220
+ wide: true,
221
+ buttons: [{ id: 'close', label: t('skillsStudio.dialog.close') }],
222
+ cancelId: 'close',
223
+ onOpen: (ui) => box.addEventListener('click', (e) => { const b = e.target.closest?.('[data-fix]'); if (b) ui.done(b.dataset.fix); }),
224
+ });
225
+ if (!answer || answer === 'close') return;
226
+ const [index, action] = [Number(answer.split(':')[0]), answer.split(':').slice(1).join(':')];
227
+ const issue = issues[index];
228
+ if (!issue) return;
229
+ await runAction(ctx, row, issue, action, { bulkDelete });
230
+ }
231
+
232
+ async function runAction(ctx, row, issue, action, { bulkDelete }) {
233
+ const { t, tp } = ctx;
234
+ const gen = ctx.gen();
235
+ try {
236
+ switch (action) {
237
+ case 'dismiss':
238
+ ctx.dismissIssues([issue.fingerprint]);
239
+ ctx.toast(t('skillsStudio.fix.dismissed'));
240
+ return;
241
+ case 'open-edit': ctx.openRow(row.key, { tab: 'edit' }); return;
242
+ case 'open-publish': ctx.openRow(row.key, { tab: 'publish' }); return;
243
+ case 'open-update': ctx.openRow(row.key); return;
244
+ case 'keep-mine':
245
+ ctx.keepUpdate(row.folderName, row.update?.upstreamRevisionId);
246
+ ctx.toast(t('skillsStudio.toast.keptMine', { name: row.folderName }));
247
+ ctx.render();
248
+ return;
249
+ case 'move-bridges': await ctx.openSkillFolders(); return;
250
+ case 'delete':
251
+ ctx.lib.selected = new Set([row.key]);
252
+ await bulkDelete?.(ctx);
253
+ return;
254
+ case 'auto-text': {
255
+ const fix = await readTextFix(ctx, row);
256
+ if (!ctx.alive(gen)) return;
257
+ if (!fix) {
258
+ const go = await ctx.ask({ title: t('skillsStudio.fix.title', { name: row.label }), lines: [t('skillsStudio.fix.textNothing')], buttons: [{ id: 'cancel', label: t('skillsStudio.dialog.close') }, { id: 'open', label: t('skillsStudio.fix.action.open-edit'), kind: 'primary' }], focus: 'open' });
259
+ if (go === 'open') ctx.openRow(row.key, { tab: 'edit' });
260
+ return;
261
+ }
262
+ const step = { kind: 'text', row, issue, changes: fix.changes };
263
+ const ok = await ctx.ask({ title: t('skillsStudio.fix.previewTitle'), lines: [t('skillsStudio.fix.previewIntro'), stepWords(ctx, step), row.skill ? t('skillsStudio.fix.textToDraft') : t('skillsStudio.fix.textInPlace')], buttons: [{ id: 'cancel', label: t('skillsStudio.dialog.cancel') }, { id: 'apply', label: tp('skillsStudio.fix.apply', 1), kind: 'primary' }], focus: 'apply' });
264
+ if (ok !== 'apply' || !ctx.alive(gen)) return;
265
+ await fix.apply();
266
+ report(ctx, [stepWords(ctx, step)], []);
267
+ await ctx.reload();
268
+ return;
269
+ }
270
+ case 'archive-duplicate': {
271
+ const step = { kind: 'archive', row, issue };
272
+ const ok = await ctx.ask({ title: t('skillsStudio.fix.previewTitle'), lines: [stepWords(ctx, step)], buttons: [{ id: 'cancel', label: t('skillsStudio.dialog.cancel') }, { id: 'apply', label: tp('skillsStudio.fix.apply', 1), kind: 'primary' }], focus: 'apply' });
273
+ if (ok !== 'apply' || !ctx.alive(gen)) return;
274
+ await archiveRecord(ctx, row);
275
+ report(ctx, [stepWords(ctx, step)], []);
276
+ await ctx.reload();
277
+ return;
278
+ }
279
+ case 'choose-copy': await chooseCopy(ctx, row, issue); return;
280
+ default: return;
281
+ }
282
+ } catch (err) {
283
+ if (ctx.alive(gen)) ctx.toast(t('skillsStudio.fix.failed', { error: studioErrorText(t, err) }), 'error');
284
+ }
285
+ }
286
+
287
+ /** Two records of one skill that differ: the owner picks the one that keeps the name; the other is archived. */
288
+ async function chooseCopy(ctx, row, issue) {
289
+ const { t, tp } = ctx;
290
+ const original = ctx.rows.find((r) => r.key === issue.health.of);
291
+ const pair = [original, row].filter(Boolean);
292
+ const archivable = (r) => !(r.skill?.installations || []).length && !r.review;
293
+ const box = document.createElement('div');
294
+ box.className = 'sx-fix-choose';
295
+ box.setAttribute('role', 'radiogroup');
296
+ box.setAttribute('aria-label', t('skillsStudio.fix.chooseTitle'));
297
+ let picked = null;
298
+ for (const r of pair) {
299
+ const label = document.createElement('label');
300
+ label.className = 'sx-fix-choice';
301
+ const input = document.createElement('input');
302
+ input.type = 'radio';
303
+ input.name = 'sx-fix-keep';
304
+ input.value = r.key;
305
+ // Keeping one archives the other: only a record that can be archived (not installed, not in review) may go.
306
+ const other = pair.find((x) => x !== r);
307
+ input.disabled = !other || !archivable(other);
308
+ input.addEventListener('change', () => { picked = r.key; });
309
+ const text = document.createElement('span');
310
+ const versions = Number(r.skill?.revisions) || 0;
311
+ const installs = (r.skill?.installations || []).length;
312
+ text.textContent = t('skillsStudio.fix.keepThis', { name: r.label, versions: tp('skillsStudio.fix.versions', versions), installs: tp('skillsStudio.fix.installs', installs) });
313
+ label.append(input, text);
314
+ box.append(label);
315
+ }
316
+ if (!pair.some((r) => archivable(r))) {
317
+ await ctx.ask({ title: t('skillsStudio.fix.chooseTitle'), lines: [t('skillsStudio.fix.chooseNone')], buttons: [{ id: 'ok', label: t('skillsStudio.dialog.close'), kind: 'primary' }], cancelId: 'ok' });
318
+ return;
319
+ }
320
+ const answer = await ctx.ask({
321
+ title: t('skillsStudio.fix.chooseTitle'),
322
+ lines: [t('skillsStudio.fix.chooseIntro')],
323
+ extra: box,
324
+ buttons: [{ id: 'cancel', label: t('skillsStudio.dialog.cancel') }, { id: 'keep', label: t('skillsStudio.fix.keepConfirm'), kind: 'primary' }],
325
+ accept: () => !!picked,
326
+ });
327
+ if (answer !== 'keep' || !picked) return;
328
+ const gone = pair.find((r) => r.key !== picked);
329
+ await archiveRecord(ctx, gone);
330
+ report(ctx, [t('skillsStudio.fix.step.archive', { name: gone.label, original: pair.find((r) => r.key === picked)?.label || '' })], []);
331
+ await ctx.reload();
332
+ }
333
+
334
+ /** The Library's attention notice: how many problems, and "Fix safe issues…" / "Show them". */
335
+ export function fixNoticeHTML(ctx, deleteKind) {
336
+ if (ctx.guest || ctx.lib.view === 'archived' || ctx.loading) return '';
337
+ const { t, tp } = ctx;
338
+ const out = [];
339
+ const n = ctx.lib.fixNotice;
340
+ if (n) {
341
+ out.push(html`<div class="sx-notice sx-notice--${n.kind === 'info' ? 'info' : 'warn'}" role="status" data-notice-box="fix">
342
+ ${n.kind === 'info' ? ICONS.ok : ICONS.warn}<span class="sx-notice-text">${n.line}${n.lines.slice(0, 8).map((l) => html`<span class="sx-block">${l}</span>`)}</span>
343
+ <button type="button" class="sx-btn sx-btn--ghost" data-lib="fix-notice-ok">${t('skillsStudio.notice.ok')}</button>
344
+ </div>`);
345
+ }
346
+ const counts = issueCounts(ctx.rows, canOf(ctx, deleteKind));
347
+ if (counts.total) {
348
+ const { steps } = safeFixCandidates(ctx.rows, canOf(ctx, deleteKind));
349
+ out.push(html`<div class="sx-notice sx-notice--warn sx-fix-notice" role="note" data-notice-box="attention">
350
+ ${ICONS.warn}<span class="sx-notice-text">${tp('skillsStudio.fix.notice', counts.total)}${steps.length ? ` ${tp('skillsStudio.fix.noticeSafe', steps.length)}` : ''}</span>
351
+ ${steps.length ? html`<button type="button" class="sx-btn sx-btn--primary" data-lib="fix-all">${t('skillsStudio.fix.fixAll')}</button>` : ''}
352
+ ${ctx.lib.health === 'attention' ? '' : html`<button type="button" class="sx-btn" data-lib="fix-show">${t('skillsStudio.fix.showIssues')}</button>`}
353
+ </div>`);
354
+ }
355
+ return html`${out}`;
356
+ }
357
+
358
+ /** The unreadable folders of the catalog, said with what can be done: read again, or leave them (dismissed). */
359
+ export const unreadableFingerprint = (count) => fingerprintOf('library', { code: 'unreadable', count });