@notis_ai/cli 0.2.17 → 0.2.19

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 (395) hide show
  1. package/README.md +542 -146
  2. package/bin/check-runtime.js +15 -0
  3. package/bin/notis.js +2 -0
  4. package/config/notis_app_design_rules.json +320 -0
  5. package/config/portal-route-roots.json +45 -0
  6. package/dist/agent-hooks/notis-agent-hook.mjs +42323 -18258
  7. package/dist/base-skills/notis-cli/.notis-base-skill.json +1 -0
  8. package/dist/base-skills/notis-cli/SKILL.md +51 -280
  9. package/dist/base-skills/notis-cli/references/app-delivery.md +17 -0
  10. package/dist/base-skills/notis-cli/references/intelligence.md +94 -0
  11. package/dist/base-skills/notis-cli/references/native-databases.md +21 -0
  12. package/dist/base-skills/notis-cli/references/tool-examples.md +56 -0
  13. package/dist/base-skills/notis-cli/references/troubleshooting.md +67 -0
  14. package/dist/base-skills/notis-query/.notis-base-skill.json +1 -0
  15. package/dist/base-skills/notis-query/SKILL.md +35 -699
  16. package/dist/base-skills/notis-query/references/database-discovery.md +59 -0
  17. package/dist/base-skills/notis-query/references/documents.md +74 -0
  18. package/dist/base-skills/notis-query/references/query.md +76 -0
  19. package/dist/base-skills/notis-spaces/.notis-base-skill.json +1 -0
  20. package/dist/base-skills/notis-spaces/SKILL.md +218 -0
  21. package/dist/base-skills/notis-spaces/references/architecture.md +116 -0
  22. package/dist/base-skills/notis-spaces/references/context.md +81 -0
  23. package/dist/base-skills/notis-spaces/references/design.md +141 -0
  24. package/dist/base-skills/notis-spaces/references/fixtures.md +29 -0
  25. package/dist/base-skills/notis-spaces/references/memory.md +58 -0
  26. package/dist/base-skills/notis-spaces/references/reading.md +73 -0
  27. package/dist/base-skills/notis-spaces/references/release.md +120 -0
  28. package/dist/base-skills/notis-spaces/references/reports.md +77 -0
  29. package/dist/base-skills/notis-spaces/references/sdk.md +273 -0
  30. package/dist/base-skills/notis-spaces/references/store.md +118 -0
  31. package/dist/base-skills/notis-spaces/references/troubleshooting.md +28 -0
  32. package/dist/base-skills/notis-spaces/references/views.md +229 -0
  33. package/{template/packages → dist}/sdk/package.json +9 -10
  34. package/dist/sdk/src/agentContext.ts +36 -0
  35. package/dist/sdk/src/bundleModule.ts +175 -0
  36. package/dist/sdk/src/components/Dialog.tsx +103 -0
  37. package/dist/sdk/src/components/DocumentEditor.tsx +113 -0
  38. package/dist/sdk/src/components/DocumentPage.tsx +184 -0
  39. package/{template/packages → dist}/sdk/src/components/Markdown.tsx +9 -3
  40. package/{template/packages → dist}/sdk/src/components/MarkdownEditor.tsx +26 -5
  41. package/{template/packages → dist}/sdk/src/components/MultiSelectActionBar.tsx +95 -12
  42. package/{template/packages → dist}/sdk/src/components/MultiSelectCheckbox.tsx +4 -2
  43. package/dist/sdk/src/components/NotisCommentBoundary.tsx +172 -0
  44. package/{template/packages → dist}/sdk/src/components/NotisSelectionBoundary.tsx +9 -5
  45. package/dist/sdk/src/components/RecordComponents.tsx +111 -0
  46. package/dist/sdk/src/components/Skeleton.tsx +38 -0
  47. package/dist/sdk/src/config.ts +31 -0
  48. package/{template/packages → dist}/sdk/src/documents.ts +21 -6
  49. package/dist/sdk/src/hooks/useAction.ts +28 -0
  50. package/dist/sdk/src/hooks/useAgentContext.ts +23 -0
  51. package/{template/packages → dist}/sdk/src/hooks/useBackend.ts +3 -2
  52. package/dist/sdk/src/hooks/useCloudComputer.ts +94 -0
  53. package/dist/sdk/src/hooks/useCloudComputerShell.ts +114 -0
  54. package/{template/packages → dist}/sdk/src/hooks/useCollectionInteractions.ts +157 -147
  55. package/dist/sdk/src/hooks/useDatabaseSchema.ts +50 -0
  56. package/{template/packages → dist}/sdk/src/hooks/useDatabaseSubscription.ts +2 -2
  57. package/dist/sdk/src/hooks/useDocument.ts +51 -0
  58. package/{template/packages → dist}/sdk/src/hooks/useDocuments.ts +25 -59
  59. package/{template/packages → dist}/sdk/src/hooks/useHandover.ts +7 -7
  60. package/dist/sdk/src/hooks/useLongPressSelection.ts +79 -0
  61. package/dist/sdk/src/hooks/useMarqueeSelection.ts +194 -0
  62. package/{template/packages → dist}/sdk/src/hooks/useMultiSelect.ts +2 -8
  63. package/dist/sdk/src/hooks/useNativeDocumentBody.ts +50 -0
  64. package/dist/sdk/src/hooks/useNativeDocuments.ts +143 -0
  65. package/{template/packages → dist}/sdk/src/hooks/useNotis.ts +6 -1
  66. package/dist/sdk/src/hooks/useNotisNavigation.ts +70 -0
  67. package/dist/sdk/src/hooks/useQuery.ts +81 -0
  68. package/dist/sdk/src/hooks/useToolQuery.ts +12 -0
  69. package/dist/sdk/src/hooks/useTopBarSearch.ts +102 -0
  70. package/dist/sdk/src/hooks/useTopBarTitleLeaf.ts +75 -0
  71. package/{template/packages → dist}/sdk/src/hooks/useUpsertDocument.ts +1 -1
  72. package/dist/sdk/src/hooks/useViewParams.ts +195 -0
  73. package/dist/sdk/src/hooks/useViewerReads.ts +129 -0
  74. package/{template/packages → dist}/sdk/src/index.ts +64 -1
  75. package/{template/packages → dist}/sdk/src/interactions/actions.ts +14 -1
  76. package/{template/packages → dist}/sdk/src/interactions/shortcuts.tsx +129 -23
  77. package/dist/sdk/src/interactions/visibility.ts +13 -0
  78. package/{template/packages → dist}/sdk/src/interactions.ts +5 -1
  79. package/dist/sdk/src/nativeData.ts +39 -0
  80. package/{template/packages → dist}/sdk/src/provider.tsx +9 -4
  81. package/dist/sdk/src/queryCache.ts +292 -0
  82. package/dist/sdk/src/queryErrors.ts +51 -0
  83. package/dist/sdk/src/runtime.ts +675 -0
  84. package/dist/sdk/src/runtimeModules.ts +33 -0
  85. package/dist/sdk/src/space.ts +442 -0
  86. package/dist/sdk/src/standardViewParams.ts +36 -0
  87. package/{template/packages → dist}/sdk/src/styles.css +81 -16
  88. package/dist/sdk/src/tailwind.ts +66 -0
  89. package/dist/sdk/src/ui/charts/RenderChartData.tsx +25 -0
  90. package/dist/sdk/src/ui/engine/core.ts +392 -0
  91. package/dist/sdk/src/ui/engine/dom.ts +183 -0
  92. package/dist/sdk/src/ui/engine/hooks.ts +195 -0
  93. package/dist/sdk/src/ui/engine/instance.ts +63 -0
  94. package/dist/sdk/src/ui/engine/keycapContext.ts +2 -0
  95. package/dist/sdk/src/ui/engine/registry.ts +262 -0
  96. package/dist/sdk/src/ui/engine/types.ts +62 -0
  97. package/dist/sdk/src/ui/icons.tsx +91 -0
  98. package/dist/sdk/src/ui/util.ts +83 -0
  99. package/dist/sdk/src/viewPeriod.ts +26 -0
  100. package/dist/sdk/src/vite.ts +92 -0
  101. package/dist/sdk/src/words/actions.ts +295 -0
  102. package/dist/sdk/src/words/clashes.ts +158 -0
  103. package/dist/sdk/src/words/format.ts +136 -0
  104. package/dist/sdk/src/words/index.ts +13 -0
  105. package/dist/sdk/src/words/keymap.ts +72 -0
  106. package/dist/sdk/src/words/keys.ts +297 -0
  107. package/dist/sdk/src/words/shell.ts +46 -0
  108. package/dist/sdk/src/words/types.ts +65 -0
  109. package/dist/skill-sync/index.js +832 -151
  110. package/dist/skill-sync/index.js.map +4 -4
  111. package/dist/skill-sync-worker.mjs +3941 -0
  112. package/dist/space-harness/host.js +1460 -0
  113. package/dist/space-sdk/dist/chunk-ZSN57ATH.js +5 -0
  114. package/dist/space-sdk/dist/index.js +15 -0
  115. package/dist/space-sdk/dist/ui.js +18 -0
  116. package/dist/space-sdk/package.json +33 -0
  117. package/dist/space-sdk/src/agentContext.d.ts +36 -0
  118. package/dist/space-sdk/src/components/Dialog.d.ts +20 -0
  119. package/dist/space-sdk/src/components/DocumentEditor.d.ts +46 -0
  120. package/dist/space-sdk/src/components/DocumentPage.d.ts +53 -0
  121. package/dist/space-sdk/src/components/Markdown.d.ts +18 -0
  122. package/dist/space-sdk/src/components/MarkdownEditor.d.ts +11 -0
  123. package/dist/space-sdk/src/components/MultiSelectActionBar.d.ts +31 -0
  124. package/dist/space-sdk/src/components/MultiSelectCheckbox.d.ts +14 -0
  125. package/dist/space-sdk/src/components/MultiSelectDragOverlay.d.ts +8 -0
  126. package/dist/space-sdk/src/components/NotisCommentBoundary.d.ts +36 -0
  127. package/dist/space-sdk/src/components/NotisSelectionBoundary.d.ts +16 -0
  128. package/dist/space-sdk/src/components/RecordComponents.d.ts +64 -0
  129. package/dist/space-sdk/src/components/ShortcutHints.d.ts +12 -0
  130. package/dist/space-sdk/src/components/Skeleton.d.ts +23 -0
  131. package/dist/space-sdk/src/config.ts +31 -0
  132. package/dist/space-sdk/src/documents.d.ts +59 -0
  133. package/dist/space-sdk/src/hooks/useAction.d.ts +8 -0
  134. package/dist/space-sdk/src/hooks/useActiveResource.d.ts +3 -0
  135. package/dist/space-sdk/src/hooks/useAgentContext.d.ts +7 -0
  136. package/dist/space-sdk/src/hooks/useBackend.d.ts +23 -0
  137. package/dist/space-sdk/src/hooks/useCloudComputer.d.ts +45 -0
  138. package/dist/space-sdk/src/hooks/useCloudComputerShell.d.ts +49 -0
  139. package/dist/space-sdk/src/hooks/useCollectionInteractions.d.ts +118 -0
  140. package/dist/space-sdk/src/hooks/useDatabaseSchema.d.ts +14 -0
  141. package/dist/space-sdk/src/hooks/useDatabaseSubscription.d.ts +26 -0
  142. package/dist/space-sdk/src/hooks/useDocument.d.ts +22 -0
  143. package/dist/space-sdk/src/hooks/useDocuments.d.ts +31 -0
  144. package/dist/space-sdk/src/hooks/useHandover.d.ts +43 -0
  145. package/dist/space-sdk/src/hooks/useLongPressSelection.d.ts +9 -0
  146. package/dist/space-sdk/src/hooks/useMarqueeSelection.d.ts +57 -0
  147. package/dist/space-sdk/src/hooks/useMultiSelect.d.ts +51 -0
  148. package/dist/space-sdk/src/hooks/useNativeDocumentBody.d.ts +24 -0
  149. package/dist/space-sdk/src/hooks/useNativeDocuments.d.ts +55 -0
  150. package/dist/space-sdk/src/hooks/useNotis.d.ts +25 -0
  151. package/dist/space-sdk/src/hooks/useNotisNavigation.d.ts +33 -0
  152. package/dist/space-sdk/src/hooks/useQuery.d.ts +22 -0
  153. package/dist/space-sdk/src/hooks/useTool.d.ts +20 -0
  154. package/dist/space-sdk/src/hooks/useToolQuery.d.ts +3 -0
  155. package/dist/space-sdk/src/hooks/useTools.d.ts +15 -0
  156. package/dist/space-sdk/src/hooks/useTopBarSearch.d.ts +48 -0
  157. package/dist/space-sdk/src/hooks/useTopBarTitleLeaf.d.ts +24 -0
  158. package/dist/space-sdk/src/hooks/useUpsertDocument.d.ts +27 -0
  159. package/dist/space-sdk/src/hooks/useViewParams.d.ts +76 -0
  160. package/dist/space-sdk/src/hooks/useViewerReads.d.ts +38 -0
  161. package/dist/space-sdk/src/index.d.ts +98 -0
  162. package/dist/space-sdk/src/interactions/actions.d.ts +33 -0
  163. package/dist/space-sdk/src/interactions/shortcuts.d.ts +42 -0
  164. package/dist/space-sdk/src/interactions/visibility.d.ts +2 -0
  165. package/dist/space-sdk/src/nativeData.d.ts +66 -0
  166. package/dist/space-sdk/src/provider.d.ts +17 -0
  167. package/dist/space-sdk/src/queryCache.d.ts +45 -0
  168. package/dist/space-sdk/src/queryErrors.ts +51 -0
  169. package/dist/space-sdk/src/runtime.d.ts +615 -0
  170. package/dist/space-sdk/src/runtimeModules.ts +33 -0
  171. package/dist/space-sdk/src/space.ts +442 -0
  172. package/dist/space-sdk/src/standardViewParams.d.ts +15 -0
  173. package/dist/space-sdk/src/tailwind-candidates.d.ts +3418 -0
  174. package/dist/space-sdk/src/tailwind.ts +66 -0
  175. package/dist/space-sdk/src/ui/actions/ActionToast.d.ts +15 -0
  176. package/dist/space-sdk/src/ui/actions/actionWords.d.ts +16 -0
  177. package/dist/space-sdk/src/ui/actions/index.d.ts +7 -0
  178. package/dist/space-sdk/src/ui/actions/useActions.d.ts +69 -0
  179. package/dist/space-sdk/src/ui/actions/useToast.d.ts +41 -0
  180. package/dist/space-sdk/src/ui/blocks/Tabs.d.ts +139 -0
  181. package/dist/space-sdk/src/ui/blocks/blocks.d.ts +217 -0
  182. package/dist/space-sdk/src/ui/blocks/index.d.ts +4 -0
  183. package/dist/space-sdk/src/ui/board/BoardView.d.ts +36 -0
  184. package/dist/space-sdk/src/ui/board/boardModel.d.ts +46 -0
  185. package/dist/space-sdk/src/ui/board/boardWords.d.ts +9 -0
  186. package/dist/space-sdk/src/ui/board/index.d.ts +5 -0
  187. package/dist/space-sdk/src/ui/calendar/CalendarView.d.ts +49 -0
  188. package/dist/space-sdk/src/ui/calendar/DayPicker.d.ts +23 -0
  189. package/dist/space-sdk/src/ui/calendar/calendarMath.d.ts +81 -0
  190. package/dist/space-sdk/src/ui/charts/Chart.d.ts +203 -0
  191. package/dist/space-sdk/src/ui/charts/ChartTable.d.ts +37 -0
  192. package/dist/space-sdk/src/ui/charts/RenderChartData.d.ts +16 -0
  193. package/dist/space-sdk/src/ui/charts/StatRow.d.ts +59 -0
  194. package/dist/space-sdk/src/ui/charts/chartData.d.ts +84 -0
  195. package/dist/space-sdk/src/ui/comments/CommentPane.d.ts +58 -0
  196. package/dist/space-sdk/src/ui/comments/commentModel.d.ts +157 -0
  197. package/dist/space-sdk/src/ui/comments/commentWords.d.ts +60 -0
  198. package/dist/space-sdk/src/ui/comments/index.d.ts +4 -0
  199. package/dist/space-sdk/src/ui/comments/useComments.d.ts +146 -0
  200. package/dist/space-sdk/src/ui/engine/HelpSheet.d.ts +12 -0
  201. package/dist/space-sdk/src/ui/engine/KeyboardEngine.d.ts +27 -0
  202. package/dist/space-sdk/src/ui/engine/core.d.ts +73 -0
  203. package/dist/space-sdk/src/ui/engine/hooks.d.ts +64 -0
  204. package/dist/space-sdk/src/ui/engine/index.d.ts +14 -0
  205. package/dist/space-sdk/src/ui/engine/instance.d.ts +21 -0
  206. package/dist/space-sdk/src/ui/engine/keycapContext.d.ts +2 -0
  207. package/dist/space-sdk/src/ui/engine/registry.d.ts +77 -0
  208. package/dist/space-sdk/src/ui/engine/step.d.ts +11 -0
  209. package/dist/space-sdk/src/ui/engine/types.d.ts +54 -0
  210. package/dist/space-sdk/src/ui/fields/fieldWords.d.ts +56 -0
  211. package/dist/space-sdk/src/ui/fields/fields.d.ts +162 -0
  212. package/dist/space-sdk/src/ui/fields/index.d.ts +5 -0
  213. package/dist/space-sdk/src/ui/fields/useFieldDraft.d.ts +22 -0
  214. package/dist/space-sdk/src/ui/gallery/AttachmentGallery.d.ts +53 -0
  215. package/dist/space-sdk/src/ui/gallery/ImageViewer.d.ts +27 -0
  216. package/dist/space-sdk/src/ui/gallery/galleryWords.d.ts +95 -0
  217. package/dist/space-sdk/src/ui/gallery/index.d.ts +5 -0
  218. package/dist/space-sdk/src/ui/icons.d.ts +13 -0
  219. package/dist/space-sdk/src/ui/index.d.ts +54 -0
  220. package/dist/space-sdk/src/ui/keycap/Keycap.d.ts +22 -0
  221. package/dist/space-sdk/src/ui/keycap/index.d.ts +2 -0
  222. package/dist/space-sdk/src/ui/layout/Panels.d.ts +50 -0
  223. package/dist/space-sdk/src/ui/layout/chatPanelEdge.d.ts +77 -0
  224. package/dist/space-sdk/src/ui/layout/index.d.ts +5 -0
  225. package/dist/space-sdk/src/ui/layout/panelMath.d.ts +91 -0
  226. package/dist/space-sdk/src/ui/list/ListReader.d.ts +380 -0
  227. package/dist/space-sdk/src/ui/list/Row.d.ts +145 -0
  228. package/dist/space-sdk/src/ui/list/SelectionBar.d.ts +65 -0
  229. package/dist/space-sdk/src/ui/list/index.d.ts +14 -0
  230. package/dist/space-sdk/src/ui/list/rowState.d.ts +22 -0
  231. package/dist/space-sdk/src/ui/list/useListKeys.d.ts +89 -0
  232. package/dist/space-sdk/src/ui/list/useReorder.d.ts +86 -0
  233. package/dist/space-sdk/src/ui/list/useSelection.d.ts +90 -0
  234. package/dist/space-sdk/src/ui/open/OpenAsMenu.d.ts +15 -0
  235. package/dist/space-sdk/src/ui/open/index.d.ts +6 -0
  236. package/dist/space-sdk/src/ui/open/openModeStore.d.ts +39 -0
  237. package/dist/space-sdk/src/ui/open/useOpenMode.d.ts +17 -0
  238. package/dist/space-sdk/src/ui/overlay/Menu.d.ts +88 -0
  239. package/dist/space-sdk/src/ui/overlay/Popover.d.ts +28 -0
  240. package/dist/space-sdk/src/ui/overlay/Tooltip.d.ts +19 -0
  241. package/dist/space-sdk/src/ui/overlay/index.d.ts +9 -0
  242. package/dist/space-sdk/src/ui/overlay/layerRoot.d.ts +24 -0
  243. package/dist/space-sdk/src/ui/overlay/position.d.ts +31 -0
  244. package/dist/space-sdk/src/ui/period/PeriodPicker.d.ts +30 -0
  245. package/dist/space-sdk/src/ui/period/dateFormat.d.ts +50 -0
  246. package/dist/space-sdk/src/ui/period/dateWords.d.ts +88 -0
  247. package/dist/space-sdk/src/ui/period/periods.d.ts +70 -0
  248. package/dist/space-sdk/src/ui/period/usePeriod.d.ts +23 -0
  249. package/dist/space-sdk/src/ui/present/index.d.ts +3 -0
  250. package/dist/space-sdk/src/ui/present/presentWords.d.ts +27 -0
  251. package/dist/space-sdk/src/ui/present/screens.d.ts +36 -0
  252. package/dist/space-sdk/src/ui/present/useAudienceScreen.d.ts +38 -0
  253. package/dist/space-sdk/src/ui/styles.d.ts +21 -0
  254. package/dist/space-sdk/src/ui/timeline/TimelineView.d.ts +44 -0
  255. package/dist/space-sdk/src/ui/timeline/timelineMath.d.ts +123 -0
  256. package/dist/space-sdk/src/viewPeriod.d.ts +8 -0
  257. package/dist/space-sdk/src/vite.ts +92 -0
  258. package/dist/space-sdk/src/words/actions.ts +295 -0
  259. package/dist/space-sdk/src/words/clashes.ts +158 -0
  260. package/dist/space-sdk/src/words/format.ts +136 -0
  261. package/dist/space-sdk/src/words/index.ts +13 -0
  262. package/dist/space-sdk/src/words/keymap.ts +72 -0
  263. package/dist/space-sdk/src/words/keys.ts +297 -0
  264. package/dist/space-sdk/src/words/shell.ts +46 -0
  265. package/dist/space-sdk/src/words/types.ts +65 -0
  266. package/dist/view-renderer/dist/host.css +1 -0
  267. package/dist/view-renderer/dist/host.js +2374 -0
  268. package/dist/view-renderer/dist/theme.css +73 -0
  269. package/dist/view-renderer/src/dom.js +212 -0
  270. package/dist/view-renderer/src/errors.js +8 -0
  271. package/dist/view-renderer/src/host.tsx +250 -0
  272. package/dist/view-renderer/src/index.js +4 -0
  273. package/dist/view-renderer/src/png.js +213 -0
  274. package/dist/view-renderer/src/pool.js +110 -0
  275. package/dist/view-renderer/src/remote-broker.js +59 -0
  276. package/dist/view-renderer/src/renderer.js +271 -0
  277. package/package.json +21 -11
  278. package/skills/notis-cli/AGENT_INSTRUCTIONS.md +63 -12
  279. package/skills/notis-onboarding/BRIEF.md +14 -9
  280. package/skills/notis-query/cli.md +1 -1
  281. package/skills/notis-spaces/cli.md +540 -0
  282. package/src/cli.js +8 -8
  283. package/src/command-specs/agents.js +121 -8
  284. package/src/command-specs/auth.js +3 -3
  285. package/src/command-specs/diagnostics.js +97 -20
  286. package/src/command-specs/documents.js +158 -0
  287. package/src/command-specs/files.js +29 -0
  288. package/src/command-specs/handover.js +331 -79
  289. package/src/command-specs/helpers.js +50 -40
  290. package/src/command-specs/index.js +14 -5
  291. package/src/command-specs/meta.js +38 -9
  292. package/src/command-specs/onboarding.js +1 -1
  293. package/src/command-specs/profile.js +42 -1
  294. package/src/command-specs/skills.js +314 -9
  295. package/src/command-specs/smoke.js +16 -4
  296. package/src/command-specs/spaces.js +783 -0
  297. package/src/command-specs/tools.js +118 -21
  298. package/src/command-specs/version-history.js +20 -0
  299. package/src/command-specs/views.js +69 -0
  300. package/src/runtime/agent-browser.js +178 -267
  301. package/src/runtime/agent-setup.js +172 -13
  302. package/src/runtime/app-boundary-validator.js +338 -2
  303. package/src/runtime/app-data-literals.js +82 -0
  304. package/src/runtime/app-platform.js +750 -1568
  305. package/src/runtime/auth-recovery.js +17 -0
  306. package/src/runtime/base-skills.d.ts +11 -0
  307. package/src/runtime/base-skills.js +100 -3
  308. package/src/runtime/computer-identity.d.ts +30 -0
  309. package/src/runtime/computer-identity.js +151 -0
  310. package/src/runtime/config-lock.js +135 -0
  311. package/src/runtime/confined-space-build.js +74 -0
  312. package/src/runtime/confined-space-verify.js +49 -0
  313. package/src/runtime/delegated-context.js +5 -21
  314. package/src/runtime/native-record-files.js +47 -0
  315. package/src/runtime/oauth.js +342 -31
  316. package/src/runtime/profiles.js +19 -112
  317. package/src/runtime/retired-sdk-files.js +24 -0
  318. package/src/runtime/retry.js +150 -0
  319. package/src/runtime/skill-sync/cloud-client.ts +33 -8
  320. package/src/runtime/skill-sync/index.ts +443 -106
  321. package/src/runtime/skill-sync/local-scanner.ts +248 -32
  322. package/src/runtime/skill-sync/native-identity.ts +102 -0
  323. package/src/runtime/skill-sync/native-plan.ts +70 -0
  324. package/src/runtime/skill-sync/symlink-manager.ts +66 -16
  325. package/src/runtime/skill-sync/sync-plan.ts +34 -2
  326. package/src/runtime/skill-sync/types.ts +54 -0
  327. package/src/runtime/skill-sync/write-cloud-skill.ts +4 -4
  328. package/src/runtime/skill-sync-service.js +109 -0
  329. package/src/runtime/source-root.js +32 -0
  330. package/src/runtime/space-collection-scaffold.js +107 -0
  331. package/src/runtime/space-database-structures.js +229 -0
  332. package/src/runtime/space-fixture-broker.js +68 -0
  333. package/src/runtime/space-lock.js +337 -0
  334. package/src/runtime/space-platform.js +133 -0
  335. package/src/runtime/space-source.js +290 -0
  336. package/src/runtime/space-test-server.js +354 -0
  337. package/src/runtime/space-verification.js +94 -0
  338. package/src/runtime/space-verify-browser-worker.js +51 -0
  339. package/src/runtime/space-view-manifest.js +368 -0
  340. package/src/runtime/sync-skills.d.ts +5 -1
  341. package/src/runtime/sync-skills.js +11 -2
  342. package/src/runtime/tool-confirmation.js +138 -0
  343. package/src/runtime/transport.js +118 -24
  344. package/src/runtime/upsell.js +219 -0
  345. package/src/runtime/view-render.js +49 -0
  346. package/src/skill-sync-worker-entry.js +2 -0
  347. package/src/skill-sync-worker.js +114 -0
  348. package/src/space-harness-host.ts +122 -0
  349. package/template/.harness/index.html.tmpl +9 -352
  350. package/template/app/globals.css +29 -3
  351. package/template/app/layout.tsx +2 -6
  352. package/template/components/ui/badge.tsx +7 -4
  353. package/template/components/ui/button.tsx +1 -1
  354. package/template/components/ui/native-select.tsx +24 -0
  355. package/template/notis.config.ts +4 -36
  356. package/template/package-lock.json +1800 -1867
  357. package/template/package.json +16 -14
  358. package/template/postcss.config.mjs +1 -1
  359. package/template/tailwind.config.ts +1 -6
  360. package/template/tsconfig.json +1 -0
  361. package/template/vite.config.ts +2 -2
  362. package/dist/base-skills/notis-apps/SKILL.md +0 -613
  363. package/skills/notis-apps/cli.md +0 -285
  364. package/src/command-specs/apps.js +0 -3000
  365. package/src/runtime/app-changelog.js +0 -79
  366. package/src/runtime/app-dev-consumers.js +0 -154
  367. package/src/runtime/app-dev-host-lock.js +0 -80
  368. package/src/runtime/app-dev-process-identity.js +0 -111
  369. package/src/runtime/app-dev-roots.js +0 -284
  370. package/src/runtime/app-dev-server.js +0 -1140
  371. package/src/runtime/app-dev-sessions.js +0 -185
  372. package/src/runtime/app-registry-scaffolds.js +0 -367
  373. package/src/runtime/assets/store-screenshot-dark.png +0 -0
  374. package/src/runtime/cli-mode.generated.js +0 -5
  375. package/src/runtime/cli-mode.js +0 -34
  376. package/src/runtime/store-screenshot.js +0 -143
  377. package/template/CHANGELOG.md +0 -5
  378. package/template/app/page.tsx +0 -60
  379. package/template/components/ui/card.tsx +0 -56
  380. package/template/packages/sdk/src/components/DocumentEditor.tsx +0 -103
  381. package/template/packages/sdk/src/config.ts +0 -259
  382. package/template/packages/sdk/src/hooks/useCloudComputer.ts +0 -97
  383. package/template/packages/sdk/src/hooks/useDatabaseSchema.ts +0 -85
  384. package/template/packages/sdk/src/hooks/useDocument.ts +0 -78
  385. package/template/packages/sdk/src/hooks/useNotisNavigation.ts +0 -52
  386. package/template/packages/sdk/src/hooks/useTopBarSearch.ts +0 -73
  387. package/template/packages/sdk/src/runtime.ts +0 -439
  388. package/template/packages/sdk/src/vite.ts +0 -56
  389. package/template/packages/sdk/tsconfig.json +0 -15
  390. /package/{template/packages → dist}/sdk/src/components/MultiSelectDragOverlay.tsx +0 -0
  391. /package/{template/packages → dist}/sdk/src/components/ShortcutHints.tsx +0 -0
  392. /package/{template/packages → dist}/sdk/src/hooks/useActiveResource.ts +0 -0
  393. /package/{template/packages → dist}/sdk/src/hooks/useTool.ts +0 -0
  394. /package/{template/packages → dist}/sdk/src/hooks/useTools.ts +0 -0
  395. /package/{template/packages → dist}/sdk/src/ui.ts +0 -0
@@ -1,11 +1,18 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { promises as fs } from 'node:fs';
3
+ import { dirname, join } from 'node:path';
1
4
  import type {
5
+ AgentTargets,
2
6
  LocalSkill,
3
7
  NotisSyncState,
4
8
  SkillSyncFailure,
5
9
  SyncPullResponse,
6
10
  SyncSettings,
11
+ SyncedSkill,
7
12
  } from "./types";
8
13
  import { normalizeAgentTargets } from "./types";
14
+ import { assertNativePull, checkedNativeReference, isNativeSkill, nativeState } from './native-identity';
15
+ import { bindNativeFolders, hasNativeFolderHistory, nativeLinkRows, nativeLocalReady, pendingLegacyUpgrade, previousNativeState, retainMissingNativeState } from './native-plan';
9
16
  import {
10
17
  downloadSkillBundle,
11
18
  fetchSyncSettings,
@@ -30,8 +37,9 @@ import {
30
37
  syncSymlinks,
31
38
  type DeletedAgentSymlink,
32
39
  } from "./symlink-manager";
33
- import { getPushCandidates } from "./sync-plan";
40
+ import { getPushCandidates, selectCloudSkillsToApply } from "./sync-plan";
34
41
  import { writeCloudSkillWithBundleFallback } from "./write-cloud-skill";
42
+ export { fetchSyncSettings } from './cloud-client';
35
43
 
36
44
  export interface RunSkillSyncResult {
37
45
  syncEnabled: boolean;
@@ -49,15 +57,22 @@ export interface RunSkillSyncResult {
49
57
  /** Skills the server rejected (e.g. an invalid SKILL.md description). The rest of
50
58
  * the batch still syncs; these are surfaced so the user knows what to fix. */
51
59
  failedPushes: SkillSyncFailure[];
60
+ failedLinks?: SkillSyncFailure[];
52
61
  }
53
62
 
54
63
  export interface RunSkillSyncOptions {
55
64
  /** Electron's scheduled invocation honors the account preference. A manual
56
65
  * CLI sync always runs, even when automatic Desktop refresh is disabled. */
57
66
  honorSyncEnabled?: boolean;
67
+ /** All account paths, scanner roots and agent links use this explicit home. */
68
+ syncHome?: string;
69
+ /** False when another account is the CLI default: sync this account's own
70
+ * folder only and leave the agent folders (Claude Code, Codex, Cursor) to the
71
+ * default's skills. Defaults to true. */
72
+ linkAgentFolders?: boolean;
58
73
  }
59
74
 
60
- const BASE_SKILL_NAMES = new Set(['notis-apps', 'notis-query', 'notis-cli']);
75
+ const BASE_SKILL_NAMES = new Set(['notis-spaces', 'notis-query', 'notis-cli']);
61
76
 
62
77
  function withoutBaseSkills(pullResponse: SyncPullResponse): SyncPullResponse {
63
78
  return {
@@ -76,14 +91,21 @@ function withoutBaseSkillState(state: NotisSyncState): NotisSyncState {
76
91
  }
77
92
 
78
93
  export interface MaterializeCloudSkillsResult {
94
+ /** Cloud skills verified present in the final scoped disk scan. */
95
+ materializedSkillNames: string[];
79
96
  pulled: number;
80
97
  downloaded: number;
81
98
  deleted: number;
82
99
  removed: number;
83
100
  lastSyncedAt: string | null;
101
+ failedLinks?: SkillSyncFailure[];
84
102
  }
85
103
 
86
104
  export interface MaterializeCloudSkillsOptions {
105
+ /** Server-verified Notis identity, when Desktop's auth subject differs. */
106
+ canonicalUserId?: string;
107
+ /** Previously authenticated settings must agree with the pull before I/O. */
108
+ syncSettings?: SyncSettings;
87
109
  /** Re-link only these cloud skills without removing or adopting other links. */
88
110
  relinkSkillNames?: readonly string[];
89
111
  }
@@ -125,6 +147,74 @@ const DEFAULT_RUN_SKILL_SYNC_DEPS: RunSkillSyncDependencies = {
125
147
  removeForeignAccountSymlinks,
126
148
  updateAgentTargets,
127
149
  };
150
+
151
+ // Another account is the CLI default and owns the agent folders: this account
152
+ // syncs its own folder without linking, unlinking or adopting anything there.
153
+ const MIRROR_ONLY_DEPS: Pick<
154
+ RunSkillSyncDependencies,
155
+ 'removeForeignAccountSymlinks' | 'removeAllSymlinksForSkill' | 'syncSymlinks'
156
+ > = {
157
+ removeForeignAccountSymlinks: async () => 0,
158
+ removeAllSymlinksForSkill: async () => 0,
159
+ syncSymlinks: async () => ({ linked: 0, removed: 0, skipped: 0, verifiedAgentLinks: {} }),
160
+ };
161
+
162
+ /**
163
+ * The account linking now owns the agent folders, so the links the other
164
+ * accounts on this computer last verified are gone. Forget them: otherwise that
165
+ * account's next linking sync would read each missing link as the user removing
166
+ * the skill and deactivate it.
167
+ */
168
+ async function forgetOtherAccountsAgentLinks(
169
+ paths: ReturnType<typeof getSkillSyncPathsForUser>,
170
+ ): Promise<void> {
171
+ const usersDir = dirname(dirname(paths.syncStatePath));
172
+ let entries: import('node:fs').Dirent[];
173
+ try {
174
+ entries = await fs.readdir(usersDir, { withFileTypes: true });
175
+ } catch {
176
+ return;
177
+ }
178
+ for (const entry of entries) {
179
+ if (!entry.isDirectory()) continue;
180
+ const statePath = join(usersDir, entry.name, '.notis-sync.json');
181
+ if (statePath === paths.syncStatePath) continue;
182
+ let state: { skills?: Record<string, { verifiedAgentLinks?: unknown }> } | null = null;
183
+ try {
184
+ state = JSON.parse(await fs.readFile(statePath, 'utf8'));
185
+ } catch {
186
+ continue;
187
+ }
188
+ if (!state || typeof state !== 'object' || !state.skills || typeof state.skills !== 'object') continue;
189
+ let changed = false;
190
+ for (const skill of Object.values(state.skills)) {
191
+ if (skill && typeof skill === 'object' && 'verifiedAgentLinks' in skill) {
192
+ delete skill.verifiedAgentLinks;
193
+ changed = true;
194
+ }
195
+ }
196
+ if (!changed) continue;
197
+ const temporaryPath = `${statePath}.${process.pid}.${Date.now()}.tmp`;
198
+ await fs.writeFile(temporaryPath, `${JSON.stringify(state, null, 2)}\n`, 'utf8');
199
+ await fs.rename(temporaryPath, statePath);
200
+ }
201
+ }
202
+
203
+ export function isolatedLinkOptions(paths: ReturnType<typeof getSkillSyncPathsForUser>) {
204
+ if (!paths.isolatedNamespace && !paths.homeDir) return undefined;
205
+ const root = paths.isolatedNamespace
206
+ ? join(paths.syncRoot, 'environments', paths.isolatedNamespace, 'agents') : paths.homeDir!;
207
+ return { agentSkillDirs: { notis: paths.skillsDir, claude_code: join(root, '.claude', 'skills'),
208
+ cursor: join(root, '.cursor', 'skills'), codex: join(root, '.codex', 'skills') },
209
+ legacyGlobalSkillsDir: join(root, '.agents', 'skills') };
210
+ }
211
+
212
+ function assertSyncEnvironment(pull: SyncPullResponse, settings: SyncSettings): void {
213
+ if ((pull.sync_namespace || null) !== (settings.sync_namespace || null)
214
+ || (settings.sync_scope !== undefined && pull.sync_scope !== settings.sync_scope)) {
215
+ throw new Error('Skill sync environment changed. Existing mirrors were retained.');
216
+ }
217
+ }
128
218
  function toSkillMap(skills: LocalSkill[]): Map<string, LocalSkill> {
129
219
  return new Map(skills.map((skill) => [skill.name, skill]));
130
220
  }
@@ -144,7 +234,17 @@ function decodeJwtSubject(jwt: string): string | null {
144
234
  }
145
235
  }
146
236
 
147
- function shouldWriteCloudSkill(
237
+ function cloudContentHash(skill: SyncPullResponse['skills'][number]): string {
238
+ return createHash('sha256').update(JSON.stringify({
239
+ md: skill.skill_md,
240
+ hash: skill.skill_folder_hash,
241
+ source: skill.skill_source_url,
242
+ files: skill.bundle_files?.slice().sort((a, b) => a.path.localeCompare(b.path)),
243
+ hydrationFailed: skill.bundle_hydration_failed === true,
244
+ })).digest('hex');
245
+ }
246
+
247
+ export function shouldWriteCloudSkill(
148
248
  cloudSkill: SyncPullResponse["skills"][number],
149
249
  localSkills: Map<string, LocalSkill>,
150
250
  previousState: NotisSyncState,
@@ -156,46 +256,150 @@ function shouldWriteCloudSkill(
156
256
  return true;
157
257
  }
158
258
 
259
+ if (isNativeSkill(cloudSkill)) {
260
+ if (localSkill.nativeScope && localSkill.nativeScope !== cloudSkill.native_scope) return false;
261
+ if (nativeLocalReady(cloudSkill, localSkill)) return false;
262
+ const nativePrevious = previousNativeState(previousState, cloudSkill);
263
+ return (localSkill.nativeFilesCurrent === true && localSkill.folderHash === localSkill.nativeBaseFolderHash)
264
+ || (!localSkill.nativeReference && nativePrevious?.cloudId === cloudSkill.id
265
+ && nativePrevious.folderHash === localSkill.folderHash);
266
+ }
267
+
268
+ const previous = previousState.skills[skillName];
269
+ if (previous?.folderHash === localSkill.folderHash
270
+ && previous.cloudContentHash === cloudContentHash(cloudSkill)) return false;
271
+
272
+ // Without a stored content baseline (skills served through a signed source URL keep
273
+ // none, because that URL changes on every pull) the only other signal is
274
+ // `skill_folder_hash`, which the server does NOT always compute the way the local
275
+ // folder hash is computed. A bundle-entry hash can never equal the hash of the
276
+ // extracted directory, and comparing the two replaced the folder on every sync
277
+ // tick, deleting whatever a running skill had written inside it. Compare cloud against cloud instead: rewrite when the cloud
278
+ // revision we last applied has actually moved.
279
+ if (previous
280
+ && previous.cloudContentHash === undefined
281
+ && previous.cloudId === cloudSkill.id
282
+ && previous.appliedCloudFolderHash !== undefined
283
+ && previous.appliedCloudFolderHash === cloudHash
284
+ && previous.cloudUpdatedAt === cloudSkill.updated_at
285
+ && previous.folderHash === localSkill.folderHash) {
286
+ return false;
287
+ }
288
+
159
289
  if (cloudSkill.source === "curated") {
160
290
  return cloudHash ? cloudHash !== localSkill.folderHash : true;
161
291
  }
162
292
 
163
- const previous = previousState.skills[skillName];
164
293
  const localChangedSinceLastSync =
165
294
  !previous || previous.folderHash !== localSkill.folderHash;
166
295
  return (
167
296
  !localChangedSinceLastSync &&
168
- Boolean(cloudHash) &&
169
- cloudHash !== localSkill.folderHash
297
+ ((Boolean(cloudHash) && cloudHash !== localSkill.folderHash)
298
+ || Boolean(previous?.cloudContentHash && previous.cloudContentHash !== cloudContentHash(cloudSkill)))
170
299
  );
171
300
  }
172
301
 
302
+ type AppliedCloudRevisions = Record<string, string | undefined>;
303
+
304
+ /**
305
+ * The cloud revision each folder's last successful write applied: this run's revision
306
+ * for the skills we just wrote, the previously recorded one otherwise. A failed write
307
+ * records nothing, so the next sync retries it.
308
+ */
309
+ function collectAppliedCloudRevisions(
310
+ pullResponse: SyncPullResponse,
311
+ previousState: NotisSyncState,
312
+ writtenSkillNames: ReadonlySet<string>,
313
+ ): AppliedCloudRevisions {
314
+ const applied: AppliedCloudRevisions = {};
315
+ for (const skill of selectCloudSkillsToApply(pullResponse.skills)) {
316
+ applied[skill.name] = writtenSkillNames.has(skill.name)
317
+ ? skill.skill_folder_hash || ""
318
+ : previousState.skills[skill.name]?.appliedCloudFolderHash;
319
+ }
320
+ return applied;
321
+ }
322
+
323
+ /**
324
+ * The files each folder's last successful write put on disk: this run's list for the
325
+ * skills we just wrote, the previously recorded one otherwise. Missing means "no
326
+ * record", which keeps local files rather than deleting what cannot be attributed.
327
+ */
328
+ function collectAppliedFiles(
329
+ pullResponse: SyncPullResponse,
330
+ previousState: NotisSyncState,
331
+ appliedFilesByName: Record<string, string[]>,
332
+ ): Record<string, string[] | undefined> {
333
+ const applied: Record<string, string[] | undefined> = {};
334
+ for (const skill of selectCloudSkillsToApply(pullResponse.skills)) {
335
+ applied[skill.name] = appliedFilesByName[skill.name]
336
+ ?? previousState.skills[skill.name]?.appliedFiles;
337
+ }
338
+ return applied;
339
+ }
340
+
173
341
  function buildSyncState(
174
342
  pullResponse: SyncPullResponse,
175
343
  localSkills: LocalSkill[],
176
344
  lastSyncedAt: string | null,
345
+ verifiedAgentLinks: Record<string, Partial<AgentTargets>> = {},
346
+ failedContentNames: ReadonlySet<string> = new Set(),
347
+ appliedRevisions: AppliedCloudRevisions = {},
348
+ appliedFiles: Record<string, string[] | undefined> = {},
349
+ previousState: NotisSyncState = { version: 1, lastSyncedAt: null, skills: {} },
177
350
  ): NotisSyncState {
178
351
  const localSkillMap = toSkillMap(localSkills);
179
- const skills = Object.fromEntries(
180
- pullResponse.skills.map((skill) => {
352
+ // The same row selection the writer used, so the state describes the revision that
353
+ // is actually on disk rather than whichever duplicate row the server listed last.
354
+ const skills = Object.fromEntries<SyncedSkill>(
355
+ selectCloudSkillsToApply(pullResponse.skills).map((skill): [string, SyncedSkill] => {
181
356
  const localSkill = localSkillMap.get(skill.name);
357
+ const appliedCloudFolderHash = appliedRevisions[skill.name];
358
+ const appliedFileList = appliedFiles[skill.name];
359
+ if (pendingLegacyUpgrade(skill, localSkill, previousState)) {
360
+ // Keep the legacy baseline so the upgrade retries; never mark a usable folder unavailable.
361
+ const { nativeUnavailable: _unavailable, ...previous } = previousNativeState(previousState, skill)!;
362
+ return [skill.name, { ...previous, verifiedAgentLinks: verifiedAgentLinks[skill.name] ?? {} }];
363
+ }
364
+ if (isNativeSkill(skill) && !nativeLocalReady(skill, localSkill)) {
365
+ const previous = previousNativeState(previousState, skill);
366
+ return [skill.name, { ...(previous || {
367
+ cloudId: skill.id, folderHash: localSkill?.nativeBaseFolderHash || '',
368
+ agentTargets: normalizeAgentTargets(skill.agent_targets), syncedAt: lastSyncedAt || new Date().toISOString(),
369
+ ...(localSkill?.nativeReference ? { nativeReference: localSkill.nativeReference, nativeScope: localSkill.nativeScope } : {}),
370
+ }), nativeUnavailable: true, verifiedAgentLinks: {} }];
371
+ }
182
372
  return [
183
373
  skill.name,
184
374
  {
185
375
  cloudId: skill.id,
376
+ ...nativeState(skill),
186
377
  folderHash: localSkill?.folderHash || skill.skill_folder_hash || "",
187
378
  agentTargets: normalizeAgentTargets(skill.agent_targets),
379
+ verifiedAgentLinks: skill.status === "active" ? verifiedAgentLinks[skill.name] ?? {} : {},
380
+ cloudUpdatedAt: skill.updated_at,
381
+ ...(!failedContentNames.has(skill.name) && !skill.skill_source_url
382
+ ? { cloudContentHash: cloudContentHash(skill) } : {}),
383
+ ...(appliedCloudFolderHash !== undefined ? { appliedCloudFolderHash } : {}),
384
+ ...(appliedFileList !== undefined ? { appliedFiles: appliedFileList } : {}),
188
385
  syncedAt: lastSyncedAt || new Date().toISOString(),
189
386
  },
190
387
  ];
191
388
  }),
192
389
  );
193
390
 
194
- return {
391
+ for (const local of localSkills) {
392
+ if (!local.nativeReference || Object.values(skills).some(row => row.cloudId === local.cloudId)) continue;
393
+ skills[local.name] = { cloudId: local.cloudId!, nativeReference: local.nativeReference,
394
+ nativeScope: local.nativeScope, nativeUnavailable: true, verifiedAgentLinks: {},
395
+ folderHash: local.nativeBaseFolderHash || '', agentTargets: normalizeAgentTargets(),
396
+ syncedAt: lastSyncedAt || new Date().toISOString() };
397
+ }
398
+ return retainMissingNativeState(previousState, {
195
399
  version: 1,
196
400
  lastSyncedAt,
197
401
  skills,
198
- };
402
+ });
199
403
  }
200
404
 
201
405
  function buildLocalSymlinkCandidates(
@@ -208,6 +412,10 @@ function buildLocalSymlinkCandidates(
208
412
  );
209
413
  const localOnlySkills = localSkills
210
414
  .filter((skill) => !cloudSkillNames.has(skill.name))
415
+ .filter(skill => !skill.nativeReference && !previousState.skills[skill.name]?.nativeReference
416
+ && !previousState.skills[skill.name]?.nativeUnavailable
417
+ && !hasNativeFolderHistory(previousState, skill.name)
418
+ && !/^native-[a-f0-9-]{36}$/.test(skill.name))
211
419
  .map((skill) => {
212
420
  const previous = previousState.skills[skill.name];
213
421
  return {
@@ -222,7 +430,7 @@ function buildLocalSymlinkCandidates(
222
430
  };
223
431
  });
224
432
 
225
- return [...pullResponse.skills, ...localOnlySkills];
433
+ return [...nativeLinkRows(pullResponse, localSkills, previousState), ...localOnlySkills];
226
434
  }
227
435
 
228
436
  function isEmptySyncState(state: NotisSyncState): boolean {
@@ -268,6 +476,10 @@ async function writePulledSkillsToScopedMirror(
268
476
  RunSkillSyncDependencies,
269
477
  "downloadSkillBundle" | "writeCloudSkillToDisk"
270
478
  >,
479
+ failures: SkillSyncFailure[] = [],
480
+ writtenSkillNames: Set<string> = new Set(),
481
+ appliedFilesByName: Record<string, string[]> = {},
482
+ acknowledgedNative: ReadonlyMap<string, string> = new Map(),
271
483
  ): Promise<number> {
272
484
  const localSkillMap = toSkillMap(localSkills);
273
485
  const warnSkillSync = (message: string, error: unknown): void => {
@@ -275,20 +487,40 @@ async function writePulledSkillsToScopedMirror(
275
487
  };
276
488
 
277
489
  let downloaded = 0;
278
- for (const cloudSkill of pullResponse.skills) {
279
- if (!shouldWriteCloudSkill(cloudSkill, localSkillMap, previousState)) {
490
+ for (const cloudSkill of selectCloudSkillsToApply(pullResponse.skills)) {
491
+ const local = localSkillMap.get(cloudSkill.name);
492
+ const acknowledged = isNativeSkill(cloudSkill) && local && acknowledgedNative.get(cloudSkill.id) === local.folderHash;
493
+ // An unchanged pre-native folder has no applied-file record, so the files it shares
494
+ // with the incoming native Skill are the previous sync's output, not local edits.
495
+ const legacyBaseline = Boolean(local && pendingLegacyUpgrade(cloudSkill, local, previousState)
496
+ && previousNativeState(previousState, cloudSkill)?.folderHash === local.folderHash);
497
+ if (!acknowledged && !shouldWriteCloudSkill(cloudSkill, localSkillMap, previousState)) {
498
+ if (isNativeSkill(cloudSkill) && !nativeLocalReady(cloudSkill, local)) {
499
+ failures.push({ name: cloudSkill.name, error: 'Local native Skill changes were retained; resolve the version or identity conflict before using them.' });
500
+ }
280
501
  continue;
281
502
  }
282
503
 
283
- if (
284
- await writeCloudSkillWithBundleFallback(cloudSkill, {
285
- downloadSkillBundle: deps.downloadSkillBundle,
286
- writeCloudSkillToDisk: (skill, bundleBytes) =>
287
- deps.writeCloudSkillToDisk(skill, bundleBytes, syncPaths),
288
- onWarning: warnSkillSync,
289
- })
290
- ) {
504
+ const outcome = await writeCloudSkillWithBundleFallback(cloudSkill, {
505
+ downloadSkillBundle: deps.downloadSkillBundle,
506
+ writeCloudSkillToDisk: (skill, bundleBytes) =>
507
+ deps.writeCloudSkillToDisk(skill, bundleBytes, syncPaths, {
508
+ // What the last write put there. Everything else in the folder was
509
+ // produced locally and survives this one.
510
+ previouslyApplied: previousNativeState(previousState, skill)?.appliedFiles,
511
+ ...(isNativeSkill(skill) && local ? { expectedFolderHash: local.folderHash } : {}),
512
+ ...(legacyBaseline ? { legacyBaseline } : {}),
513
+ }),
514
+ onWarning: warnSkillSync,
515
+ });
516
+ if (outcome) {
291
517
  downloaded += 1;
518
+ writtenSkillNames.add(cloudSkill.name);
519
+ if (typeof outcome === "object" && Array.isArray(outcome.appliedFiles)) {
520
+ appliedFilesByName[cloudSkill.name] = outcome.appliedFiles;
521
+ }
522
+ } else {
523
+ failures.push({ name: cloudSkill.name, error: "Skill content could not be downloaded or written; sync will retry" });
292
524
  }
293
525
  }
294
526
 
@@ -298,6 +530,8 @@ async function writePulledSkillsToScopedMirror(
298
530
  function assertSkillsPullAuthorized(
299
531
  pullResponse: SyncPullResponse,
300
532
  ): void {
533
+ if (!Array.isArray(pullResponse.skills)) throw new Error('Skill inventory is unavailable; existing files were retained.');
534
+ assertNativePull(pullResponse);
301
535
  if (
302
536
  pullResponse.entitlement_access?.code === "entitlement_upgrade_required"
303
537
  && pullResponse.entitlement_access.entitlement === "skills"
@@ -340,42 +574,99 @@ export async function materializeCloudSkillsForLocalShell(
340
574
  );
341
575
  }
342
576
 
343
- const syncPaths = getSkillSyncPathsForUser(authUserId);
344
- const pullResponse = await deps.pullSkills(serverUrl, jwt);
577
+ let pullResponse = await deps.pullSkills(serverUrl, jwt);
345
578
  assertSkillsPullAuthorized(pullResponse);
579
+ if (options.syncSettings) assertSyncEnvironment(pullResponse, options.syncSettings);
580
+ const syncPaths = getSkillSyncPathsForUser(options.canonicalUserId?.trim() || authUserId, undefined, pullResponse.sync_namespace);
346
581
  const previousState = await deps.readSyncState(syncPaths);
347
582
 
348
583
  const localSkills = await deps.scanLocalSkills(syncPaths);
584
+ pullResponse = bindNativeFolders(pullResponse, localSkills, previousState);
585
+ const failedDownloads: SkillSyncFailure[] = [];
586
+ const writtenSkillNames = new Set<string>();
587
+ const appliedFilesByName: Record<string, string[]> = {};
349
588
  const downloaded = await writePulledSkillsToScopedMirror(
350
589
  pullResponse,
351
590
  localSkills,
352
591
  previousState,
353
592
  syncPaths,
354
593
  deps,
594
+ failedDownloads,
595
+ writtenSkillNames,
596
+ appliedFilesByName,
355
597
  );
356
598
  const finalLocalSkills = await deps.scanLocalSkills(syncPaths);
357
599
  const lastSyncedAt = pullResponse.last_synced_at || new Date().toISOString();
358
600
 
359
601
  const relinkSkillNames = new Set(options.relinkSkillNames || []);
602
+ const failures = [...failedDownloads];
603
+ const verifiedLinks: Record<string, Partial<AgentTargets>> = {};
604
+ for (const skill of pullResponse.skills) {
605
+ const previous = previousState.skills[skill.name];
606
+ if (skill.updated_at && previous?.cloudId === skill.id && previous.cloudUpdatedAt === skill.updated_at) {
607
+ verifiedLinks[skill.name] = { ...previous.verifiedAgentLinks };
608
+ }
609
+ }
360
610
  if (relinkSkillNames.size > 0) {
361
- await deps.syncSymlinks(
362
- pullResponse.skills.filter((skill) => relinkSkillNames.has(skill.name)),
611
+ const relinked = await deps.syncSymlinks(
612
+ nativeLinkRows(pullResponse, finalLocalSkills, previousState).filter((skill) => relinkSkillNames.has(skill.name)),
363
613
  syncPaths.skillsDir,
364
- { removeUndesired: false },
614
+ { ...isolatedLinkOptions(syncPaths), removeUndesired: false },
365
615
  );
616
+ failures.push(...(relinked.failures ?? []).filter(
617
+ (failure) => !failedDownloads.some((download) => download.name === failure.name),
618
+ ));
619
+ for (const name of relinkSkillNames) {
620
+ verifiedLinks[name] = relinked.verifiedAgentLinks?.[name] ?? {};
621
+ }
366
622
  }
623
+ for (const failure of failedDownloads) delete verifiedLinks[failure.name];
367
624
 
368
- await deps.writeSyncState(
369
- buildSyncState(pullResponse, finalLocalSkills, lastSyncedAt),
370
- syncPaths,
625
+ const materializedState = buildSyncState(
626
+ pullResponse, finalLocalSkills, lastSyncedAt, verifiedLinks,
627
+ new Set(failedDownloads.map(item => item.name)),
628
+ collectAppliedCloudRevisions(pullResponse, previousState, writtenSkillNames),
629
+ collectAppliedFiles(pullResponse, previousState, appliedFilesByName),
630
+ previousState,
371
631
  );
632
+ // Pull-only refresh is not an upload acknowledgement. Keep content baselines
633
+ // unless we actually wrote cloud content, and retain cloud-missing entries so
634
+ // the next two-way sync can delete them instead of uploading them as new.
635
+ for (const [name, entry] of Object.entries(materializedState.skills)) {
636
+ if (writtenSkillNames.has(name)) continue;
637
+ const previous = previousState.skills[name];
638
+ if (previous) {
639
+ entry.folderHash = previous.folderHash;
640
+ entry.cloudContentHash = previous.cloudContentHash;
641
+ } else {
642
+ delete materializedState.skills[name];
643
+ }
644
+ }
645
+ materializedState.skills = { ...previousState.skills, ...materializedState.skills };
646
+ let removed = 0;
647
+ for (const [name, previous] of Object.entries(previousState.skills)) {
648
+ if (!previous.nativeReference && !previous.nativeUnavailable) continue;
649
+ const current = pullResponse.skills.find(skill => skill.id === previous.cloudId);
650
+ const local = current && finalLocalSkills.find(row => row.name === current.name);
651
+ if (!current || (!nativeLocalReady(current, local) && !pendingLegacyUpgrade(current, local, previousState))) {
652
+ removed += await deps.removeAllSymlinksForSkill(name, syncPaths.skillsDir, isolatedLinkOptions(syncPaths));
653
+ }
654
+ }
655
+ await deps.writeSyncState(materializedState, syncPaths);
372
656
 
373
657
  return {
658
+ materializedSkillNames: pullResponse.skills
659
+ .filter((skill) => finalLocalSkills.some((local) => local.name === skill.name)
660
+ && (!isNativeSkill(skill) || (skill.status === 'active' && skill.agent_targets?.notis !== false
661
+ && nativeLocalReady(skill, finalLocalSkills.find(local => local.name === skill.name))))
662
+ && !failedDownloads.some((failure) => failure.name === skill.name))
663
+ .map((skill) => skill.name),
374
664
  pulled: pullResponse.skills.length,
375
665
  downloaded,
376
666
  deleted: 0,
377
- removed: 0,
667
+ removed,
378
668
  lastSyncedAt,
669
+ ...(failures.length ? { failedLinks: failures } : {}),
379
670
  };
380
671
  }
381
672
 
@@ -397,6 +688,8 @@ async function deactivateDeletedAgentSkills(
397
688
  RunSkillSyncDependencies,
398
689
  "detectDeletedAgentSymlinks" | "updateAgentTargets" | "pullSkills"
399
690
  >,
691
+ failures: SkillSyncFailure[],
692
+ linkOptions?: ReturnType<typeof isolatedLinkOptions>,
400
693
  ): Promise<number> {
401
694
  // First sync (incl. legacy migration) has no reliable "we created this link" signal, so we
402
695
  // cannot tell a user deletion apart from a never-created link — skip detection entirely.
@@ -405,42 +698,18 @@ async function deactivateDeletedAgentSkills(
405
698
  }
406
699
 
407
700
  const deletions = await deps.detectDeletedAgentSymlinks(
408
- pullResponse.skills,
701
+ pullResponse.skills.map(skill => {
702
+ const previous = isNativeSkill(skill) ? Object.entries(previousState.skills).find(([, row]) => row.cloudId === skill.id) : undefined;
703
+ return previous ? { ...skill, name: previous[0] } : skill;
704
+ }),
409
705
  previousState,
410
706
  skillsDir,
707
+ linkOptions,
411
708
  );
412
709
  if (deletions.length === 0) {
413
710
  return 0;
414
711
  }
415
712
 
416
- // The agent-targets endpoint replaces the whole agent_targets column, so re-read the freshest
417
- // values right before writing and merge onto them, only flipping the deleted agent. This avoids
418
- // clobbering a concurrent portal/other-desktop change to a DIFFERENT agent with the stale
419
- // snapshot from the top-of-sync pull. (A narrow read-modify-write window remains; the endpoint
420
- // has no partial update.)
421
- const latestSkillsById = new Map(pullResponse.skills.map((skill) => [skill.id, skill]));
422
- let fresh: SyncPullResponse | null = null;
423
- try {
424
- fresh = await deps.pullSkills(serverUrl, jwt);
425
- } catch (error) {
426
- console.warn(
427
- "[skill-sync] Could not re-pull latest agent targets before deactivation; " +
428
- "using the top-of-sync snapshot.",
429
- error,
430
- );
431
- }
432
- if (fresh) {
433
- // A transport failure may fall back to the already-authorized snapshot, but
434
- // an explicit legacy denial must abort before any server or local mutation.
435
- assertSkillsPullAuthorized(fresh);
436
- for (const skill of fresh.skills) {
437
- latestSkillsById.set(skill.id, skill);
438
- }
439
- }
440
-
441
- // Group deletions by skill so multiple deleted agents for the SAME skill are flipped in one
442
- // PATCH. Otherwise each PATCH, rebuilt from the same snapshot, would overwrite the previous one
443
- // (deleting both claude_code and cursor for one skill would otherwise leave only the last off).
444
713
  const agentsBySkill = new Map<string, { skillName: string; agents: Set<DeletedAgentSymlink["agent"]> }>();
445
714
  for (const deletion of deletions) {
446
715
  const entry = agentsBySkill.get(deletion.skillId) ?? {
@@ -451,33 +720,47 @@ async function deactivateDeletedAgentSkills(
451
720
  agentsBySkill.set(deletion.skillId, entry);
452
721
  }
453
722
 
454
- const inMemoryById = new Map(pullResponse.skills.map((skill) => [skill.id, skill]));
723
+ const fresh = withoutBaseSkills(await deps.pullSkills(serverUrl, jwt));
724
+ assertSkillsPullAuthorized(fresh);
725
+ assertSyncEnvironment(fresh, pullResponse);
726
+ Object.assign(pullResponse, fresh);
727
+ let needsRefresh = false;
455
728
  let deactivated = 0;
456
729
  for (const [skillId, { skillName, agents }] of agentsBySkill) {
457
- const latest = latestSkillsById.get(skillId);
458
- if (!latest) {
459
- continue;
460
- }
461
- const nextTargets = { ...normalizeAgentTargets(latest.agent_targets) };
462
- for (const agent of agents) {
463
- nextTargets[agent] = false;
464
- }
730
+ const skill = pullResponse.skills.find((item) => item.id === skillId);
731
+ const previous = previousState.skills[skillName];
732
+ if (!skill) continue;
733
+ const native = isNativeSkill(skill);
734
+ if (native) {
735
+ if (!skill.native_settings || previous?.nativeSettingsRevision !== skill.native_settings.revision) continue;
736
+ } else if (!skill.updated_at || previous?.cloudUpdatedAt !== skill.updated_at) continue;
737
+ const patch = Object.fromEntries([...agents].map((agent) => [agent, false]));
465
738
  try {
466
- await deps.updateAgentTargets(serverUrl, jwt, skillId, nextTargets);
467
- // Mutate the in-memory pull response so the symlink passes below see the link as undesired.
468
- const inMemory = inMemoryById.get(skillId);
469
- if (inMemory) {
470
- inMemory.agent_targets = nextTargets;
739
+ const saved = await deps.updateAgentTargets(serverUrl, jwt, skillId, patch,
740
+ native ? undefined : skill.updated_at, native ? { reference: skill.native_reference!, settingsRevision: skill.native_settings!.revision } : undefined);
741
+ if (saved.success !== true || !saved.updated_at?.trim()
742
+ || (!native && saved.updated_at === skill.updated_at)
743
+ || !['notis', 'claude_code', 'cursor', 'codex'].every((agent) =>
744
+ typeof saved.agent_targets?.[agent as keyof AgentTargets] === 'boolean')
745
+ || ![...agents].every((agent) => saved.agent_targets[agent] === false)) {
746
+ throw new Error('Assignment update did not return a verified saved revision');
471
747
  }
748
+ skill.agent_targets = saved.agent_targets;
749
+ skill.updated_at = saved.updated_at;
750
+ if (native) needsRefresh = true;
472
751
  deactivated += agents.size;
473
752
  } catch (error) {
474
- console.warn(
475
- `[skill-sync] Failed to deactivate "${skillName}" for ${[...agents].join(", ")} ` +
476
- `after local symlink deletion:`,
477
- error,
478
- );
753
+ needsRefresh = true;
754
+ failures.push({ name: skillName, error: 'Could not save the local agent removal; refreshed saved assignments' });
755
+ console.warn(`[skill-sync] Assignment changed or could not be saved for "${skillName}"; refreshing before reconciliation.`, error);
479
756
  }
480
757
  }
758
+ if (needsRefresh) {
759
+ const refreshed = withoutBaseSkills(await deps.pullSkills(serverUrl, jwt));
760
+ assertSkillsPullAuthorized(refreshed);
761
+ assertSyncEnvironment(refreshed, pullResponse);
762
+ Object.assign(pullResponse, refreshed);
763
+ }
481
764
  return deactivated;
482
765
  }
483
766
 
@@ -487,9 +770,11 @@ export async function runSkillSync(
487
770
  dependencies: Partial<RunSkillSyncDependencies> = {},
488
771
  options: RunSkillSyncOptions = {},
489
772
  ): Promise<RunSkillSyncResult> {
773
+ const linkAgentFolders = options.linkAgentFolders !== false;
490
774
  const deps = {
491
775
  ...DEFAULT_RUN_SKILL_SYNC_DEPS,
492
776
  ...dependencies,
777
+ ...(linkAgentFolders ? {} : MIRROR_ONLY_DEPS),
493
778
  };
494
779
  const syncSettings: SyncSettings = await deps.fetchSyncSettings(
495
780
  serverUrl,
@@ -506,11 +791,13 @@ export async function runSkillSync(
506
791
  "Cannot sync skills without a server-verified account identity.",
507
792
  );
508
793
  }
509
- const syncPaths = getSkillSyncPathsForUser(syncUserId);
510
- const foreignLinksRemoved = await deps.removeForeignAccountSymlinks(
511
- syncPaths.skillsDir,
512
- );
794
+ const syncPaths = getSkillSyncPathsForUser(syncUserId, undefined, syncSettings.sync_namespace, options.syncHome);
513
795
  if (options.honorSyncEnabled !== false && !syncSettings.sync_enabled) {
796
+ // Disabled sync intentionally performs no pull. Only the authenticated
797
+ // settings scope may remove foreign links; account mirrors stay untouched.
798
+ const foreignLinksRemoved = await deps.removeForeignAccountSymlinks(
799
+ syncPaths.skillsDir, isolatedLinkOptions(syncPaths),
800
+ );
514
801
  return {
515
802
  syncEnabled: false,
516
803
  pushed: 0,
@@ -528,6 +815,13 @@ export async function runSkillSync(
528
815
 
529
816
  let pullResponse = withoutBaseSkills(await deps.pullSkills(serverUrl, jwt));
530
817
  assertSkillsPullAuthorized(pullResponse);
818
+ assertSyncEnvironment(pullResponse, syncSettings);
819
+ const foreignLinksRemoved = await deps.removeForeignAccountSymlinks(
820
+ syncPaths.skillsDir, isolatedLinkOptions(syncPaths),
821
+ );
822
+ if (linkAgentFolders && !syncPaths.isolatedNamespace) {
823
+ await forgetOtherAccountsAgentLinks(syncPaths);
824
+ }
531
825
 
532
826
  const cloudCuratedSkillNames = new Set(
533
827
  pullResponse.skills
@@ -535,56 +829,68 @@ export async function runSkillSync(
535
829
  .map((skill) => skill.name),
536
830
  );
537
831
  const protectedSkillNames = new Set([...cloudCuratedSkillNames, ...BASE_SKILL_NAMES]);
832
+ const scopedState = withoutBaseSkillState(await deps.readSyncState(syncPaths));
833
+ const assignmentFailures: SkillSyncFailure[] = [];
834
+ // A mirror-only sync has no links by design, so none of them is missing.
835
+ const deactivated = linkAgentFolders && !syncPaths.isolatedNamespace
836
+ && syncSettings.agent_targets_conditional_updates === true
837
+ ? await deactivateDeletedAgentSkills(
838
+ serverUrl, jwt, pullResponse, scopedState, scopedState, syncPaths.skillsDir, deps, assignmentFailures, isolatedLinkOptions(syncPaths),
839
+ )
840
+ : 0;
841
+ assertSyncEnvironment(pullResponse, syncSettings);
538
842
  const authUserId = decodeJwtSubject(jwt);
539
843
  let previousAuthState: NotisSyncState | null = null;
540
- if (authUserId && authUserId !== syncUserId) {
541
- const previousAuthPaths = getSkillSyncPathsForUser(authUserId);
844
+ if (!syncPaths.isolatedNamespace && authUserId && authUserId !== syncUserId) {
845
+ const previousAuthPaths = getSkillSyncPathsForUser(authUserId, undefined, undefined, options.syncHome);
542
846
  previousAuthState = await deps.readSyncState(previousAuthPaths);
543
847
  await deps.gatherTopLevelLocalSkills(syncPaths, {
544
848
  sourceRoots: [{ label: "previous-auth-scope", root: previousAuthPaths.skillsDir }],
545
849
  protectedSkillNames,
546
850
  });
547
851
  }
548
- await deps.gatherTopLevelLocalSkills(syncPaths, {
549
- protectedSkillNames,
550
- });
852
+ if (linkAgentFolders && !syncPaths.isolatedNamespace) {
853
+ // Skills found loose in the agent folders belong to the account linking them.
854
+ await deps.gatherTopLevelLocalSkills(syncPaths, { protectedSkillNames });
855
+ }
551
856
  const localSkills = (await deps.scanLocalSkills(syncPaths))
552
857
  .filter((skill) => !BASE_SKILL_NAMES.has(skill.name));
553
- const scopedState = withoutBaseSkillState(await deps.readSyncState(syncPaths));
554
858
  const previousState = withoutBaseSkillState(applyLegacyFirstRunState(
555
859
  localSkills,
556
860
  scopedState,
557
- isEmptySyncState(scopedState)
861
+ !syncPaths.isolatedNamespace && isEmptySyncState(scopedState)
558
862
  ? (!previousAuthState || isEmptySyncState(previousAuthState)
559
863
  ? await deps.readLegacySyncState(syncPaths)
560
864
  : previousAuthState)
561
865
  : null,
562
866
  ));
563
- // Honor locally-deleted per-agent symlinks as deactivations before reconciling, otherwise the
564
- // first syncSymlinks pass would recreate the link the user just deleted.
565
- const deactivated = await deactivateDeletedAgentSkills(
566
- serverUrl,
567
- jwt,
568
- pullResponse,
569
- previousState,
570
- scopedState,
571
- syncPaths.skillsDir,
572
- deps,
573
- );
574
- await deps.syncSymlinks(
867
+ pullResponse = bindNativeFolders(pullResponse, localSkills, previousState);
868
+ const gatheredSymlinkResult = await deps.syncSymlinks(
575
869
  buildLocalSymlinkCandidates(pullResponse, localSkills, previousState),
576
870
  syncPaths.skillsDir,
871
+ isolatedLinkOptions(syncPaths),
577
872
  );
578
873
  const pushCandidates = getPushCandidates(
579
874
  localSkills,
580
875
  previousState,
581
876
  cloudCuratedSkillNames,
582
877
  new Set(pullResponse.skills.map((skill) => skill.name)),
878
+ pullResponse.skills,
583
879
  );
584
880
 
585
881
  const failedPushes: SkillSyncFailure[] = [];
882
+ const acknowledgedNative = new Map<string, string>();
883
+ let writeLocalSkills = localSkills;
586
884
  if (pushCandidates.length > 0) {
587
885
  const pushResult = await deps.pushChangedSkills(serverUrl, jwt, pushCandidates);
886
+ for (const result of pushResult.skills || []) {
887
+ const saved = result as { id?: string; native_reference?: unknown };
888
+ const candidate = pushCandidates.find(local => local.nativeReference && local.cloudId === saved.id);
889
+ if (candidate && saved.native_reference) {
890
+ checkedNativeReference(saved.native_reference, candidate.cloudId);
891
+ acknowledgedNative.set(candidate.cloudId!, candidate.folderHash);
892
+ }
893
+ }
588
894
  if (Array.isArray(pushResult?.failed) && pushResult.failed.length > 0) {
589
895
  failedPushes.push(...pushResult.failed);
590
896
  console.warn(
@@ -594,6 +900,9 @@ export async function runSkillSync(
594
900
  }
595
901
  pullResponse = withoutBaseSkills(await deps.pullSkills(serverUrl, jwt));
596
902
  assertSkillsPullAuthorized(pullResponse);
903
+ assertSyncEnvironment(pullResponse, syncSettings);
904
+ writeLocalSkills = (await deps.scanLocalSkills(syncPaths)).filter(skill => !BASE_SKILL_NAMES.has(skill.name));
905
+ pullResponse = bindNativeFolders(pullResponse, writeLocalSkills, previousState);
597
906
  }
598
907
 
599
908
  // Delete phase: remove skills that were previously synced but are no longer in the cloud
@@ -601,18 +910,31 @@ export async function runSkillSync(
601
910
  let deleted = 0;
602
911
  for (const skillName of Object.keys(previousState.skills)) {
603
912
  if (!cloudSkillNames.has(skillName)) {
913
+ const previous = previousState.skills[skillName];
914
+ if (previous.nativeReference || previous.nativeUnavailable
915
+ || pullResponse.skills.some(skill => isNativeSkill(skill) && skill.id === previous.cloudId)) {
916
+ await deps.removeAllSymlinksForSkill(skillName, syncPaths.skillsDir, isolatedLinkOptions(syncPaths));
917
+ continue;
918
+ }
604
919
  await deps.deleteLocalSkill(skillName, syncPaths);
605
- await deps.removeAllSymlinksForSkill(skillName, syncPaths.skillsDir);
920
+ await deps.removeAllSymlinksForSkill(skillName, syncPaths.skillsDir, isolatedLinkOptions(syncPaths));
606
921
  deleted += 1;
607
922
  }
608
923
  }
609
924
 
925
+ const failedDownloads: SkillSyncFailure[] = [];
926
+ const writtenSkillNames = new Set<string>();
927
+ const appliedFilesByName: Record<string, string[]> = {};
610
928
  const downloaded = await writePulledSkillsToScopedMirror(
611
929
  pullResponse,
612
- localSkills,
930
+ writeLocalSkills,
613
931
  previousState,
614
932
  syncPaths,
615
933
  deps,
934
+ failedDownloads,
935
+ writtenSkillNames,
936
+ appliedFilesByName,
937
+ acknowledgedNative,
616
938
  );
617
939
 
618
940
  const finalLocalSkills = (await deps.scanLocalSkills(syncPaths))
@@ -620,11 +942,23 @@ export async function runSkillSync(
620
942
  const symlinkResult = await deps.syncSymlinks(
621
943
  buildLocalSymlinkCandidates(pullResponse, finalLocalSkills, previousState),
622
944
  syncPaths.skillsDir,
945
+ isolatedLinkOptions(syncPaths),
623
946
  );
947
+ const verifiedLinks = { ...(symlinkResult.verifiedAgentLinks ?? {}) };
948
+ for (const failure of failedDownloads) delete verifiedLinks[failure.name];
624
949
  const lastSyncedAt = pullResponse.last_synced_at || new Date().toISOString();
625
950
 
626
951
  await deps.writeSyncState(
627
- buildSyncState(pullResponse, finalLocalSkills, lastSyncedAt),
952
+ buildSyncState(
953
+ pullResponse,
954
+ finalLocalSkills,
955
+ lastSyncedAt,
956
+ verifiedLinks,
957
+ new Set(failedDownloads.map(item => item.name)),
958
+ collectAppliedCloudRevisions(pullResponse, previousState, writtenSkillNames),
959
+ collectAppliedFiles(pullResponse, previousState, appliedFilesByName),
960
+ previousState,
961
+ ),
628
962
  syncPaths,
629
963
  );
630
964
 
@@ -635,9 +969,12 @@ export async function runSkillSync(
635
969
  downloaded,
636
970
  deleted,
637
971
  deactivated,
638
- linked: symlinkResult.linked,
639
- removed: foreignLinksRemoved + symlinkResult.removed,
972
+ linked: gatheredSymlinkResult.linked + symlinkResult.linked,
973
+ removed: foreignLinksRemoved + gatheredSymlinkResult.removed + symlinkResult.removed,
640
974
  skipped: symlinkResult.skipped,
975
+ failedLinks: [...assignmentFailures, ...failedDownloads, ...(symlinkResult.failures ?? []).filter(
976
+ (failure) => !failedDownloads.some((download) => download.name === failure.name),
977
+ )],
641
978
  lastSyncedAt,
642
979
  failedPushes,
643
980
  };