@skaile/workspaces 6.0.0 → 6.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 (431) hide show
  1. package/CHANGELOG.md +98 -0
  2. package/dist/{asset-feeds-PXUWCXVI.js → asset-feeds-KPKCE64E.js} +31 -23
  3. package/dist/asset-feeds-KPKCE64E.js.map +1 -0
  4. package/dist/asset-manager/catalog-deployer.js +10 -10
  5. package/dist/asset-manager/fragments.js +1 -1
  6. package/dist/asset-manager/index.js +19 -19
  7. package/dist/asset-manager/installer.js +13 -13
  8. package/dist/asset-manager/renderers.js +11 -11
  9. package/dist/asset-manager/src/index.d.ts +339 -19
  10. package/dist/asset-manager/src/index.d.ts.map +1 -1
  11. package/dist/asset-manager/src/installer.d.ts +40 -0
  12. package/dist/asset-manager/src/installer.d.ts.map +1 -1
  13. package/dist/asset-manager/src/mcp-config.d.ts +21 -0
  14. package/dist/asset-manager/src/mcp-config.d.ts.map +1 -1
  15. package/dist/asset-manager/src/renderers.d.ts +7 -0
  16. package/dist/asset-manager/src/renderers.d.ts.map +1 -1
  17. package/dist/bridge/harnesses/claude-sdk.js +18 -22
  18. package/dist/bridge/harnesses/claude-sdk.js.map +1 -1
  19. package/dist/bridge/harnesses/codex.js +24 -17
  20. package/dist/bridge/harnesses/codex.js.map +1 -1
  21. package/dist/bridge/harnesses/continue.js +14 -14
  22. package/dist/bridge/harnesses/echo.js +4 -4
  23. package/dist/bridge/harnesses/gemini.js +10 -10
  24. package/dist/bridge/harnesses/goose.js +14 -14
  25. package/dist/bridge/harnesses/omp.js +15 -8
  26. package/dist/bridge/harnesses/omp.js.map +1 -1
  27. package/dist/bridge/harnesses/opencode.js +14 -14
  28. package/dist/bridge/harnesses/qwen.js +10 -10
  29. package/dist/bridge/index.js +19 -19
  30. package/dist/bridge/src/claude-sdk-surface.d.ts +17 -0
  31. package/dist/bridge/src/claude-sdk-surface.d.ts.map +1 -0
  32. package/dist/bridge/src/harness-model-check.d.ts +30 -0
  33. package/dist/bridge/src/harness-model-check.d.ts.map +1 -1
  34. package/dist/bridge/src/harnesses/_subprocess-cli.d.ts.map +1 -1
  35. package/dist/bridge/src/harnesses/claude-sdk.d.ts.map +1 -1
  36. package/dist/bridge/src/harnesses/codex.d.ts.map +1 -1
  37. package/dist/bridge/src/harnesses/omp.d.ts.map +1 -1
  38. package/dist/bridge/src/index.d.ts +2 -1
  39. package/dist/bridge/src/index.d.ts.map +1 -1
  40. package/dist/bridge/src/normalizer.d.ts +3 -1
  41. package/dist/bridge/src/normalizer.d.ts.map +1 -1
  42. package/dist/bridge/src/runner-private-env.d.ts +7 -0
  43. package/dist/bridge/src/runner-private-env.d.ts.map +1 -1
  44. package/dist/{chunk-HK7HMEPT.js → chunk-27SXN33H.js} +13 -5
  45. package/dist/chunk-27SXN33H.js.map +1 -0
  46. package/dist/{chunk-TKHJXPSB.js → chunk-2XM3ZH36.js} +183 -33
  47. package/dist/chunk-2XM3ZH36.js.map +1 -0
  48. package/dist/{chunk-E3GPGTW3.js → chunk-3HACK2Z7.js} +142 -28
  49. package/dist/chunk-3HACK2Z7.js.map +1 -0
  50. package/dist/{chunk-T2BDID2J.js → chunk-5JPW27BI.js} +10 -9
  51. package/dist/chunk-5JPW27BI.js.map +1 -0
  52. package/dist/{chunk-2UUKCROR.js → chunk-5LQ62LNT.js} +4 -4
  53. package/dist/{chunk-2UUKCROR.js.map → chunk-5LQ62LNT.js.map} +1 -1
  54. package/dist/{chunk-UL22SDGR.js → chunk-6CYKTHNT.js} +194 -127
  55. package/dist/chunk-6CYKTHNT.js.map +1 -0
  56. package/dist/{chunk-XFIEVEU7.js → chunk-6OHZUUR7.js} +5 -4
  57. package/dist/chunk-6OHZUUR7.js.map +1 -0
  58. package/dist/{chunk-E572OHGY.js → chunk-76FLBODK.js} +4 -3
  59. package/dist/chunk-76FLBODK.js.map +1 -0
  60. package/dist/{chunk-GN4VVSAA.js → chunk-7DRFKAJZ.js} +7 -7
  61. package/dist/{chunk-GN4VVSAA.js.map → chunk-7DRFKAJZ.js.map} +1 -1
  62. package/dist/chunk-7H6XRCPG.js +31 -0
  63. package/dist/chunk-7H6XRCPG.js.map +1 -0
  64. package/dist/{chunk-MCUJTNY5.js → chunk-7Y6S4XLL.js} +3 -3
  65. package/dist/{chunk-MCUJTNY5.js.map → chunk-7Y6S4XLL.js.map} +1 -1
  66. package/dist/{chunk-DHYSUJ3H.js → chunk-BEYNH2LS.js} +4 -4
  67. package/dist/{chunk-DHYSUJ3H.js.map → chunk-BEYNH2LS.js.map} +1 -1
  68. package/dist/{chunk-GEQSUC6U.js → chunk-BOT64P3U.js} +4 -4
  69. package/dist/{chunk-GEQSUC6U.js.map → chunk-BOT64P3U.js.map} +1 -1
  70. package/dist/{chunk-L4YKW5AY.js → chunk-BRK5QZKI.js} +121 -20
  71. package/dist/chunk-BRK5QZKI.js.map +1 -0
  72. package/dist/{chunk-YGRZNFRO.js → chunk-BY5WIRCU.js} +58 -5
  73. package/dist/chunk-BY5WIRCU.js.map +1 -0
  74. package/dist/{chunk-Z7ZEZZB6.js → chunk-C5HFK34M.js} +3 -3
  75. package/dist/chunk-C5HFK34M.js.map +1 -0
  76. package/dist/{chunk-DUMTLYSO.js → chunk-COAFAYBS.js} +31 -7
  77. package/dist/chunk-COAFAYBS.js.map +1 -0
  78. package/dist/{chunk-7XSAW5OC.js → chunk-E5NQRHEB.js} +30 -7
  79. package/dist/chunk-E5NQRHEB.js.map +1 -0
  80. package/dist/{chunk-BFMYYNDT.js → chunk-F3D25RGM.js} +11 -3
  81. package/dist/chunk-F3D25RGM.js.map +1 -0
  82. package/dist/{chunk-VBI7HGHB.js → chunk-GF5JXQIZ.js} +3 -3
  83. package/dist/{chunk-VBI7HGHB.js.map → chunk-GF5JXQIZ.js.map} +1 -1
  84. package/dist/{chunk-ZFUZ5AKM.js → chunk-I7LC5TDY.js} +3 -3
  85. package/dist/{chunk-ZFUZ5AKM.js.map → chunk-I7LC5TDY.js.map} +1 -1
  86. package/dist/{chunk-3IXIEHUB.js → chunk-ICSGNT5P.js} +7 -7
  87. package/dist/{chunk-3IXIEHUB.js.map → chunk-ICSGNT5P.js.map} +1 -1
  88. package/dist/{chunk-WEH33JSG.js → chunk-ITFCWMOY.js} +93 -29
  89. package/dist/chunk-ITFCWMOY.js.map +1 -0
  90. package/dist/{chunk-WDXZA7QW.js → chunk-IZQOO6MN.js} +2 -2
  91. package/dist/{chunk-WDXZA7QW.js.map → chunk-IZQOO6MN.js.map} +1 -1
  92. package/dist/{chunk-WDAIWDLK.js → chunk-JFLQICIU.js} +5 -5
  93. package/dist/{chunk-WDAIWDLK.js.map → chunk-JFLQICIU.js.map} +1 -1
  94. package/dist/{chunk-ARWKK325.js → chunk-JJJ2G4K2.js} +5 -5
  95. package/dist/{chunk-ARWKK325.js.map → chunk-JJJ2G4K2.js.map} +1 -1
  96. package/dist/{chunk-GOCWM7FN.js → chunk-JMIKGAUA.js} +43 -4
  97. package/dist/chunk-JMIKGAUA.js.map +1 -0
  98. package/dist/{chunk-X3RMRJNR.js → chunk-JRN5QF4C.js} +8 -3
  99. package/dist/chunk-JRN5QF4C.js.map +1 -0
  100. package/dist/{chunk-SNEE6UGY.js → chunk-K3R2UKG7.js} +18 -12
  101. package/dist/chunk-K3R2UKG7.js.map +1 -0
  102. package/dist/{chunk-VXEYX73S.js → chunk-KB56WBQ3.js} +4 -4
  103. package/dist/{chunk-VXEYX73S.js.map → chunk-KB56WBQ3.js.map} +1 -1
  104. package/dist/{chunk-OHNKYHRR.js → chunk-KJSC2ZD6.js} +264 -18
  105. package/dist/chunk-KJSC2ZD6.js.map +1 -0
  106. package/dist/{chunk-UQ33Y4DM.js → chunk-KPKGWXUJ.js} +4 -4
  107. package/dist/{chunk-UQ33Y4DM.js.map → chunk-KPKGWXUJ.js.map} +1 -1
  108. package/dist/{chunk-AF432MS3.js → chunk-KXJMQTOU.js} +3 -3
  109. package/dist/{chunk-AF432MS3.js.map → chunk-KXJMQTOU.js.map} +1 -1
  110. package/dist/chunk-LRBP3234.js +302 -0
  111. package/dist/chunk-LRBP3234.js.map +1 -0
  112. package/dist/{chunk-UVVVR4XO.js → chunk-M4OG2YN4.js} +4 -4
  113. package/dist/{chunk-UVVVR4XO.js.map → chunk-M4OG2YN4.js.map} +1 -1
  114. package/dist/{chunk-QOM5Z62G.js → chunk-MAZ3UXJK.js} +17 -24
  115. package/dist/chunk-MAZ3UXJK.js.map +1 -0
  116. package/dist/{chunk-Y2TO2U7W.js → chunk-MM4YO3EL.js} +3 -3
  117. package/dist/{chunk-Y2TO2U7W.js.map → chunk-MM4YO3EL.js.map} +1 -1
  118. package/dist/{chunk-OULSJRXV.js → chunk-OCQNUV4T.js} +7 -7
  119. package/dist/{chunk-OULSJRXV.js.map → chunk-OCQNUV4T.js.map} +1 -1
  120. package/dist/{chunk-SZXGRHQQ.js → chunk-PJJSBZZP.js} +4 -4
  121. package/dist/{chunk-SZXGRHQQ.js.map → chunk-PJJSBZZP.js.map} +1 -1
  122. package/dist/{chunk-OAHVKSQJ.js → chunk-PKBLJI23.js} +1471 -126
  123. package/dist/chunk-PKBLJI23.js.map +1 -0
  124. package/dist/{chunk-NOA6EJXE.js → chunk-PKE3PXOK.js} +3 -3
  125. package/dist/{chunk-NOA6EJXE.js.map → chunk-PKE3PXOK.js.map} +1 -1
  126. package/dist/{chunk-WYHQUCHR.js → chunk-PMWRM4EJ.js} +109 -48
  127. package/dist/chunk-PMWRM4EJ.js.map +1 -0
  128. package/dist/{chunk-H6UVIKQ2.js → chunk-PNUM7GUY.js} +3 -3
  129. package/dist/{chunk-H6UVIKQ2.js.map → chunk-PNUM7GUY.js.map} +1 -1
  130. package/dist/{chunk-SRODW6M2.js → chunk-Q5RN4PWV.js} +3 -3
  131. package/dist/{chunk-SRODW6M2.js.map → chunk-Q5RN4PWV.js.map} +1 -1
  132. package/dist/{chunk-LN6QB7XI.js → chunk-QIHL3YPL.js} +4 -4
  133. package/dist/{chunk-LN6QB7XI.js.map → chunk-QIHL3YPL.js.map} +1 -1
  134. package/dist/{chunk-U56GGCC2.js → chunk-QK7AG5AV.js} +4 -4
  135. package/dist/{chunk-U56GGCC2.js.map → chunk-QK7AG5AV.js.map} +1 -1
  136. package/dist/{chunk-OTIB4D2K.js → chunk-QPWR3YMY.js} +15 -5
  137. package/dist/chunk-QPWR3YMY.js.map +1 -0
  138. package/dist/{chunk-4DWBSTYA.js → chunk-QXKUFROW.js} +22 -5
  139. package/dist/chunk-QXKUFROW.js.map +1 -0
  140. package/dist/{chunk-4XWLB7JB.js → chunk-RQMIZONH.js} +95 -35
  141. package/dist/chunk-RQMIZONH.js.map +1 -0
  142. package/dist/{chunk-LSI2IVFT.js → chunk-RXGPFAMN.js} +4 -3
  143. package/dist/chunk-RXGPFAMN.js.map +1 -0
  144. package/dist/{chunk-3JKGY2YC.js → chunk-S2H3RBK7.js} +182 -33
  145. package/dist/chunk-S2H3RBK7.js.map +1 -0
  146. package/dist/{chunk-VJRTDKXS.js → chunk-SJNZ4WPA.js} +165 -106
  147. package/dist/chunk-SJNZ4WPA.js.map +1 -0
  148. package/dist/{chunk-KX2PT5Q4.js → chunk-T2VBPVWZ.js} +77 -18
  149. package/dist/chunk-T2VBPVWZ.js.map +1 -0
  150. package/dist/{chunk-6Z5KTFJT.js → chunk-TSTRH4SM.js} +3 -3
  151. package/dist/{chunk-6Z5KTFJT.js.map → chunk-TSTRH4SM.js.map} +1 -1
  152. package/dist/{chunk-7CV3HFWJ.js → chunk-TZERN27L.js} +5 -5
  153. package/dist/{chunk-7CV3HFWJ.js.map → chunk-TZERN27L.js.map} +1 -1
  154. package/dist/{chunk-JDR6ILCW.js → chunk-U673DB5U.js} +4 -4
  155. package/dist/{chunk-JDR6ILCW.js.map → chunk-U673DB5U.js.map} +1 -1
  156. package/dist/{chunk-FKU57Q5Q.js → chunk-URCJIV7T.js} +3 -3
  157. package/dist/{chunk-FKU57Q5Q.js.map → chunk-URCJIV7T.js.map} +1 -1
  158. package/dist/{chunk-ADPM6AYF.js → chunk-VSX2G7LA.js} +3 -3
  159. package/dist/{chunk-ADPM6AYF.js.map → chunk-VSX2G7LA.js.map} +1 -1
  160. package/dist/{chunk-F2GOZMRA.js → chunk-X37JUNEW.js} +49 -49
  161. package/dist/chunk-X37JUNEW.js.map +1 -0
  162. package/dist/{chunk-NMI2TWMH.js → chunk-Z3IRZBXB.js} +158 -344
  163. package/dist/chunk-Z3IRZBXB.js.map +1 -0
  164. package/dist/{chunk-7TPYPVAH.js → chunk-ZWJCRHP3.js} +3 -3
  165. package/dist/{chunk-7TPYPVAH.js.map → chunk-ZWJCRHP3.js.map} +1 -1
  166. package/dist/{claude-code-VJGYBXQR.js → claude-code-BBMNVH2J.js} +5 -5
  167. package/dist/{claude-code-VJGYBXQR.js.map → claude-code-BBMNVH2J.js.map} +1 -1
  168. package/dist/cli/index.js +964 -501
  169. package/dist/cli/index.js.map +1 -1
  170. package/dist/cli/src/agent-pins.d.ts +11 -0
  171. package/dist/cli/src/agent-pins.d.ts.map +1 -1
  172. package/dist/cli/src/commands/asset-cmd.d.ts.map +1 -1
  173. package/dist/cli/src/commands/catalog.d.ts +1 -1
  174. package/dist/cli/src/commands/catalog.d.ts.map +1 -1
  175. package/dist/cli/src/commands/config.d.ts.map +1 -1
  176. package/dist/cli/src/commands/diff.d.ts.map +1 -1
  177. package/dist/cli/src/commands/harness.d.ts +24 -0
  178. package/dist/cli/src/commands/harness.d.ts.map +1 -0
  179. package/dist/cli/src/commands/integration.d.ts.map +1 -1
  180. package/dist/cli/src/commands/library-cmd.d.ts.map +1 -1
  181. package/dist/cli/src/commands/library-index.d.ts +28 -3
  182. package/dist/cli/src/commands/library-index.d.ts.map +1 -1
  183. package/dist/cli/src/commands/library-publish.d.ts.map +1 -1
  184. package/dist/cli/src/commands/migrate-assets.d.ts.map +1 -1
  185. package/dist/cli/src/commands/outdated.d.ts.map +1 -1
  186. package/dist/cli/src/commands/plugin-registry-cmd.d.ts.map +1 -1
  187. package/dist/cli/src/commands/project.d.ts +19 -1
  188. package/dist/cli/src/commands/project.d.ts.map +1 -1
  189. package/dist/cli/src/commands/rebuild.d.ts.map +1 -1
  190. package/dist/cli/src/commands/run.d.ts.map +1 -1
  191. package/dist/cli/src/commands/serve-preflight.d.ts +31 -1
  192. package/dist/cli/src/commands/serve-preflight.d.ts.map +1 -1
  193. package/dist/cli/src/commands/serve.d.ts +10 -0
  194. package/dist/cli/src/commands/serve.d.ts.map +1 -1
  195. package/dist/cli/src/commands/session-logs/local-query.d.ts +2 -0
  196. package/dist/cli/src/commands/session-logs/local-query.d.ts.map +1 -1
  197. package/dist/cli/src/commands/source-manifest-publish.d.ts.map +1 -1
  198. package/dist/cli/src/commands/source-manifest.d.ts +19 -7
  199. package/dist/cli/src/commands/source-manifest.d.ts.map +1 -1
  200. package/dist/cli/src/commands/source.d.ts +9 -2
  201. package/dist/cli/src/commands/source.d.ts.map +1 -1
  202. package/dist/cli/src/commands/store.d.ts.map +1 -1
  203. package/dist/cli/src/commands/update.d.ts.map +1 -1
  204. package/dist/cli/src/commands/validate-instances.d.ts.map +1 -1
  205. package/dist/cli/src/commands/validate.d.ts.map +1 -1
  206. package/dist/cli/src/commands/why.d.ts.map +1 -1
  207. package/dist/cli/src/config-checks.d.ts +55 -0
  208. package/dist/cli/src/config-checks.d.ts.map +1 -0
  209. package/dist/cli/src/harness-error.d.ts +12 -1
  210. package/dist/cli/src/harness-error.d.ts.map +1 -1
  211. package/dist/cli/src/helpers.d.ts +18 -8
  212. package/dist/cli/src/helpers.d.ts.map +1 -1
  213. package/dist/cli/src/plugin-store/index.d.ts +10 -1
  214. package/dist/cli/src/plugin-store/index.d.ts.map +1 -1
  215. package/dist/cli/src/plugin-store/spec.d.ts +8 -0
  216. package/dist/cli/src/plugin-store/spec.d.ts.map +1 -1
  217. package/dist/cli/src/skill-checks.d.ts +27 -0
  218. package/dist/cli/src/skill-checks.d.ts.map +1 -1
  219. package/dist/cli/src/store-addable.d.ts +11 -0
  220. package/dist/cli/src/store-addable.d.ts.map +1 -1
  221. package/dist/connectors/config.js +5 -5
  222. package/dist/connectors/index.js +23 -23
  223. package/dist/connectors/src/connector-manager.d.ts.map +1 -1
  224. package/dist/connectors-47EM6C2P.js +48 -0
  225. package/dist/{connectors-CXZLXAFZ.js.map → connectors-47EM6C2P.js.map} +1 -1
  226. package/dist/core/framework.js +1 -1
  227. package/dist/core/index.js +9 -9
  228. package/dist/core/manifest.js +2 -2
  229. package/dist/core/models.js +1 -1
  230. package/dist/core/provider-registry-credentials.js +2 -2
  231. package/dist/core/provider-registry.js +1 -1
  232. package/dist/core/publish-manifest.js +3 -3
  233. package/dist/core/ref.js +1 -1
  234. package/dist/core/runtime-assets.js +5 -5
  235. package/dist/core/src/agent-instances.d.ts.map +1 -1
  236. package/dist/core/src/config-key-diagnostics.d.ts +9 -0
  237. package/dist/core/src/config-key-diagnostics.d.ts.map +1 -0
  238. package/dist/core/src/framework.d.ts +5 -3
  239. package/dist/core/src/framework.d.ts.map +1 -1
  240. package/dist/core/src/index.d.ts +3 -2
  241. package/dist/core/src/index.d.ts.map +1 -1
  242. package/dist/core/src/lock.d.ts +12 -0
  243. package/dist/core/src/lock.d.ts.map +1 -1
  244. package/dist/core/src/manifest.d.ts +25 -0
  245. package/dist/core/src/manifest.d.ts.map +1 -1
  246. package/dist/core/src/models.d.ts +15 -0
  247. package/dist/core/src/models.d.ts.map +1 -1
  248. package/dist/core/src/provider-registry.d.ts +6 -0
  249. package/dist/core/src/provider-registry.d.ts.map +1 -1
  250. package/dist/core/src/workspace-config.d.ts +18 -1
  251. package/dist/core/src/workspace-config.d.ts.map +1 -1
  252. package/dist/core/src/workspace-yaml-editor.d.ts +9 -2
  253. package/dist/core/src/workspace-yaml-editor.d.ts.map +1 -1
  254. package/dist/core/workspace-config.js +4 -4
  255. package/dist/deploy/index.js +9 -9
  256. package/dist/discovery/index.js +5 -5
  257. package/dist/discovery/src/builtin-providers.d.ts.map +1 -1
  258. package/dist/discovery/src/parse-manifest.d.ts.map +1 -1
  259. package/dist/{ensure-sources-UPTHZKGF.js → ensure-sources-K5EFNC3L.js} +36 -22
  260. package/dist/ensure-sources-K5EFNC3L.js.map +1 -0
  261. package/dist/factory-assets/connectors/box.js +23 -23
  262. package/dist/factory-assets/connectors/deploy.js +23 -23
  263. package/dist/factory-assets/connectors/devserver.js +23 -23
  264. package/dist/factory-assets/connectors/flow/adapter.js +23 -23
  265. package/dist/factory-assets/connectors/flow/contract/normalize.d.ts.map +1 -1
  266. package/dist/factory-assets/connectors/flow/contract.js +1 -1
  267. package/dist/factory-assets/connectors/flow/engine.js +11 -11
  268. package/dist/factory-assets/connectors/flow/run-flow.d.ts.map +1 -1
  269. package/dist/factory-assets/connectors/flow/run-flow.js +25 -25
  270. package/dist/factory-assets/connectors/flow.js +23 -23
  271. package/dist/factory-assets/connectors/git.js +23 -23
  272. package/dist/factory-assets/connectors/gmail.js +23 -23
  273. package/dist/factory-assets/connectors/googledrive.js +23 -23
  274. package/dist/factory-assets/connectors/local.js +23 -23
  275. package/dist/factory-assets/connectors/mattermost.js +23 -23
  276. package/dist/factory-assets/connectors/memory.js +23 -23
  277. package/dist/factory-assets/connectors/minio.js +23 -23
  278. package/dist/factory-assets/connectors/postgres.js +23 -23
  279. package/dist/factory-assets/connectors/s3.js +23 -23
  280. package/dist/factory-assets/connectors/sharepoint.js +23 -23
  281. package/dist/factory-assets/connectors/sqlite.js +23 -23
  282. package/dist/factory-assets/connectors/static-server.js +23 -23
  283. package/dist/factory-assets/connectors/tunnel.js +23 -23
  284. package/dist/factory-assets/connectors/webdav.js +23 -23
  285. package/dist/factory-assets/connectors/xstate-store.js +23 -23
  286. package/dist/factory-assets/connectors/xstate.js +23 -23
  287. package/dist/factory-assets/skills/authoring-skaile-assets/SKILL.md +1 -1
  288. package/dist/factory-assets/skills/authoring-skaile-manifests/SKILL.md +2 -2
  289. package/dist/factory-assets/skills/authoring-skaile-manifests/references/dependency-standard.md +3 -3
  290. package/dist/flows-KLBNSKLO.js +26 -0
  291. package/dist/{flows-TJQIMGUX.js.map → flows-KLBNSKLO.js.map} +1 -1
  292. package/dist/helpers-LUSV2ICC.js +52 -0
  293. package/dist/{helpers-DE53KG3E.js.map → helpers-LUSV2ICC.js.map} +1 -1
  294. package/dist/library/index.js +14 -14
  295. package/dist/library/install.js +5 -5
  296. package/dist/library/src/index.d.ts +2 -2
  297. package/dist/library/src/index.d.ts.map +1 -1
  298. package/dist/library/src/install/index.d.ts +1 -1
  299. package/dist/library/src/install/index.d.ts.map +1 -1
  300. package/dist/library/src/install/install-from-manifest.d.ts +13 -0
  301. package/dist/library/src/install/install-from-manifest.d.ts.map +1 -1
  302. package/dist/library/src/install/source-fetch-github.d.ts.map +1 -1
  303. package/dist/library/src/local/library-dir.d.ts +22 -5
  304. package/dist/library/src/local/library-dir.d.ts.map +1 -1
  305. package/dist/library/src/local/local-catalog-source.d.ts.map +1 -1
  306. package/dist/library/src/manifest-writer.d.ts.map +1 -1
  307. package/dist/library/src/remote/remote-catalog-source.d.ts +8 -0
  308. package/dist/library/src/remote/remote-catalog-source.d.ts.map +1 -1
  309. package/dist/library/src/remote/rest-catalog-source.d.ts +3 -2
  310. package/dist/library/src/remote/rest-catalog-source.d.ts.map +1 -1
  311. package/dist/{library-publish-KKQ2ULLX.js → library-publish-XMRCKRNR.js} +59 -58
  312. package/dist/library-publish-XMRCKRNR.js.map +1 -0
  313. package/dist/model-endpoint/index.js +12 -12
  314. package/dist/model-endpoint/src/openai-chat.d.ts +7 -2
  315. package/dist/model-endpoint/src/openai-chat.d.ts.map +1 -1
  316. package/dist/model-endpoint/src/openai-shared.d.ts +8 -0
  317. package/dist/model-endpoint/src/openai-shared.d.ts.map +1 -1
  318. package/dist/model-endpoint/src/server.d.ts.map +1 -1
  319. package/dist/model-endpoint/src/types.d.ts +1 -1
  320. package/dist/model-endpoint/src/types.d.ts.map +1 -1
  321. package/dist/model-endpoint/src/usage-cost.d.ts +23 -0
  322. package/dist/model-endpoint/src/usage-cost.d.ts.map +1 -0
  323. package/dist/open-library-XBR46DJL.js +56 -0
  324. package/dist/{open-library-RJGJIW5H.js.map → open-library-XBR46DJL.js.map} +1 -1
  325. package/dist/open-registry-W562DCDL.js +32 -0
  326. package/dist/{open-registry-3QS3J7KO.js.map → open-registry-W562DCDL.js.map} +1 -1
  327. package/dist/paths-2AZXB3QL.js +22 -0
  328. package/dist/{paths-QAV4C3KX.js.map → paths-2AZXB3QL.js.map} +1 -1
  329. package/dist/plugin-store-36SIL4TV.js +23 -0
  330. package/dist/{plugin-store-MSH6GK6T.js.map → plugin-store-36SIL4TV.js.map} +1 -1
  331. package/dist/project-D7GN7WLS.js +86 -0
  332. package/dist/{project-HZW2MZPA.js.map → project-D7GN7WLS.js.map} +1 -1
  333. package/dist/resolver/index.js +10 -10
  334. package/dist/runner/index.js +40 -40
  335. package/dist/runner/src/capability-roundtrip.d.ts +7 -6
  336. package/dist/runner/src/capability-roundtrip.d.ts.map +1 -1
  337. package/dist/runner/src/composition/content-source.d.ts.map +1 -1
  338. package/dist/runner/src/composition/errors.d.ts +16 -1
  339. package/dist/runner/src/composition/errors.d.ts.map +1 -1
  340. package/dist/runner/src/credential-notice.d.ts +3 -3
  341. package/dist/runner/src/credential-notice.d.ts.map +1 -1
  342. package/dist/runner/src/serve.d.ts +20 -7
  343. package/dist/runner/src/serve.d.ts.map +1 -1
  344. package/dist/sdk/asset-manager.js +19 -19
  345. package/dist/sdk/bridge.js +19 -19
  346. package/dist/sdk/core.js +9 -9
  347. package/dist/sdk/flow.js +11 -11
  348. package/dist/sdk/index.js +40 -40
  349. package/dist/sdk/resolver.js +10 -10
  350. package/dist/sdk/runner.js +40 -40
  351. package/dist/sdk/transport/ws/server.js +10 -10
  352. package/dist/sdk/transport/ws.js +10 -10
  353. package/dist/sdk/transport.js +10 -10
  354. package/dist/serve-preflight-CGA5G3AE.js +135 -0
  355. package/dist/serve-preflight-CGA5G3AE.js.map +1 -0
  356. package/dist/{setup-AGYB4PKR.js → setup-IBDNJAOU.js} +32 -32
  357. package/dist/{setup-AGYB4PKR.js.map → setup-IBDNJAOU.js.map} +1 -1
  358. package/dist/skills-CTTO7SZK.js +26 -0
  359. package/dist/{skills-2LMQ2IAD.js.map → skills-CTTO7SZK.js.map} +1 -1
  360. package/dist/source-2DJ5KNOI.js +85 -0
  361. package/dist/{source-GBMYD3RO.js.map → source-2DJ5KNOI.js.map} +1 -1
  362. package/dist/source-fetch-github-RWHV4I5Z.js +4 -0
  363. package/dist/{source-fetch-github-ST4MFL3D.js.map → source-fetch-github-RWHV4I5Z.js.map} +1 -1
  364. package/dist/{source-manifest-publish-FK22TVQT.js → source-manifest-publish-IYWUMUCY.js} +55 -57
  365. package/dist/source-manifest-publish-IYWUMUCY.js.map +1 -0
  366. package/dist/transport/index.js +10 -10
  367. package/dist/transport/ws/server.js +10 -10
  368. package/dist/transport/ws.js +10 -10
  369. package/dist/tui/index.js +48 -41
  370. package/dist/tui/index.js.map +1 -1
  371. package/dist/tui/src/repl.d.ts.map +1 -1
  372. package/dist/types/manifests.js +1 -1
  373. package/dist/types/src/install-manifest.d.ts +1 -1
  374. package/dist/types/src/install-manifest.d.ts.map +1 -1
  375. package/dist/types/src/manifests/index.d.ts +1 -1
  376. package/dist/types/src/manifests/index.d.ts.map +1 -1
  377. package/dist/types/src/manifests/scaffold-body.d.ts +2 -0
  378. package/dist/types/src/manifests/scaffold-body.d.ts.map +1 -1
  379. package/dist/workspace-plugin/adapters/mcp.js +30 -30
  380. package/dist/workspace-plugin/adapters/omp.js +15 -15
  381. package/dist/workspace-plugin/index.js +2 -2
  382. package/package.json +1 -1
  383. package/dist/asset-feeds-PXUWCXVI.js.map +0 -1
  384. package/dist/chunk-3JKGY2YC.js.map +0 -1
  385. package/dist/chunk-4DWBSTYA.js.map +0 -1
  386. package/dist/chunk-4XWLB7JB.js.map +0 -1
  387. package/dist/chunk-7XSAW5OC.js.map +0 -1
  388. package/dist/chunk-BFMYYNDT.js.map +0 -1
  389. package/dist/chunk-DUMTLYSO.js.map +0 -1
  390. package/dist/chunk-E3GPGTW3.js.map +0 -1
  391. package/dist/chunk-E572OHGY.js.map +0 -1
  392. package/dist/chunk-F2GOZMRA.js.map +0 -1
  393. package/dist/chunk-GOCWM7FN.js.map +0 -1
  394. package/dist/chunk-HK7HMEPT.js.map +0 -1
  395. package/dist/chunk-KX2PT5Q4.js.map +0 -1
  396. package/dist/chunk-L4YKW5AY.js.map +0 -1
  397. package/dist/chunk-LSI2IVFT.js.map +0 -1
  398. package/dist/chunk-NMI2TWMH.js.map +0 -1
  399. package/dist/chunk-OAHVKSQJ.js.map +0 -1
  400. package/dist/chunk-OHNKYHRR.js.map +0 -1
  401. package/dist/chunk-OTIB4D2K.js.map +0 -1
  402. package/dist/chunk-PUK7TQT4.js +0 -13
  403. package/dist/chunk-PUK7TQT4.js.map +0 -1
  404. package/dist/chunk-QOM5Z62G.js.map +0 -1
  405. package/dist/chunk-SNEE6UGY.js.map +0 -1
  406. package/dist/chunk-T2BDID2J.js.map +0 -1
  407. package/dist/chunk-TKHJXPSB.js.map +0 -1
  408. package/dist/chunk-UL22SDGR.js.map +0 -1
  409. package/dist/chunk-VJRTDKXS.js.map +0 -1
  410. package/dist/chunk-WEH33JSG.js.map +0 -1
  411. package/dist/chunk-WYHQUCHR.js.map +0 -1
  412. package/dist/chunk-X3RMRJNR.js.map +0 -1
  413. package/dist/chunk-XFIEVEU7.js.map +0 -1
  414. package/dist/chunk-YGRZNFRO.js.map +0 -1
  415. package/dist/chunk-Z7ZEZZB6.js.map +0 -1
  416. package/dist/connectors-CXZLXAFZ.js +0 -48
  417. package/dist/ensure-sources-UPTHZKGF.js.map +0 -1
  418. package/dist/flows-TJQIMGUX.js +0 -26
  419. package/dist/helpers-DE53KG3E.js +0 -38
  420. package/dist/library-publish-KKQ2ULLX.js.map +0 -1
  421. package/dist/open-library-RJGJIW5H.js +0 -42
  422. package/dist/open-registry-3QS3J7KO.js +0 -32
  423. package/dist/paths-QAV4C3KX.js +0 -22
  424. package/dist/plugin-store-MSH6GK6T.js +0 -23
  425. package/dist/project-HZW2MZPA.js +0 -84
  426. package/dist/serve-preflight-L4POEHNQ.js +0 -108
  427. package/dist/serve-preflight-L4POEHNQ.js.map +0 -1
  428. package/dist/skills-2LMQ2IAD.js +0 -26
  429. package/dist/source-GBMYD3RO.js +0 -85
  430. package/dist/source-fetch-github-ST4MFL3D.js +0 -4
  431. package/dist/source-manifest-publish-FK22TVQT.js.map +0 -1
@@ -1,22 +1,23 @@
1
- import { unregisteredLibraryNames, renamedLibraryName, resolveConfig, RemoteCatalogSource, OfflineError, describeCatalogError, CatalogHttpError, scanUnlistedAssets, missingRootAssets, storeManifestFile, missingLibraryDirMessage, filesEntryPaths } from './chunk-UL22SDGR.js';
2
- import { SEMVER_RE, invalidSemverMessage, flowFileNameError, readManifestSourceConfig, discoverAssetsInTree, isManifestMode } from './chunk-QOM5Z62G.js';
3
- import { parseFlowDefinition } from './chunk-X3RMRJNR.js';
4
- import { installTargetOf, readKeptLegacyCopies, writeKeptLegacyCopies, canonicalEntryRef, diffDeployedAgent, deployedAssetDrift, assertNoLibrarySymlinks, deployAll, removeAsset, createScaffold, installAgent } from './chunk-3JKGY2YC.js';
1
+ import { unregisteredLibraryNames, renamedLibraryName, resolveConfig, RemoteCatalogSource, OfflineError, describeCatalogError, CatalogHttpError, scanUnlistedAssets, missingRootAssets, storeManifestFile, missingLibraryDirMessage, lastLibraryName, filesEntryPaths } from './chunk-6CYKTHNT.js';
2
+ import { SEMVER_RE, invalidSemverMessage, flowFileNameError, computeDeterministicHash, readManifestSourceConfig, discoverAssetsInTree, isManifestMode } from './chunk-MAZ3UXJK.js';
3
+ import { parseFlowDefinition } from './chunk-JRN5QF4C.js';
4
+ import { installTargetOf, readKeptLegacyCopies, writeKeptLegacyCopies, canonicalEntryRef, diffDeployedAgent, deployedAssetDrift, assertNoLibrarySymlinks, deployAll, removeAsset, composedDriftRefs, sourceComposedFiles, cachedComposedFiles, createScaffold, installAgent } from './chunk-S2H3RBK7.js';
5
5
  import { appendHistory, getRecentHistory, clearHistory } from './chunk-KTBKW2FI.js';
6
6
  import { commitChanges } from './chunk-UQ6LFBPZ.js';
7
- import { harnessTargetSupportsAgents, CODEX_IDENTITY_DIR } from './chunk-DUMTLYSO.js';
8
- import { isScaffoldBody, isScaffoldDescription } from './chunk-E572OHGY.js';
9
- import { resolveConfiguredHarnessName, GLOBAL_INSTALL_DIR_NAME, resolveLibraryDir, buildLibraryLockEntries, writeLock, computeHash, buildLockFile, readLock, assertLockHashes, verifyLock, mergeLockFile, resyncLockSources, extractForPatch, generatePatch, savePatch, applyPatch, LOCK_SCHEMA_VERSION } from './chunk-L4YKW5AY.js';
10
- import { resolveRuntimeAssets, factoryAssetRoots } from './chunk-OTIB4D2K.js';
7
+ import { harnessTargetSupportsAgents, CODEX_IDENTITY_DIR, agentRenderFiles } from './chunk-COAFAYBS.js';
8
+ import { isScaffoldBody, isScaffoldDescription } from './chunk-76FLBODK.js';
9
+ import { resolveConfiguredHarnessName, FACTORY_SOURCE_URL, GLOBAL_INSTALL_DIR_NAME, resolveLibraryDir, buildLibraryLockEntries, writeLock, computeHash, buildLockFile, readLock, assertLockHashes, verifyLock, mergeLockFile, resyncLockSources, extractForPatch, generatePatch, savePatch, applyPatch, LOCK_SCHEMA_VERSION } from './chunk-BRK5QZKI.js';
10
+ import { resolveRuntimeAssets, factoryAssetRoots } from './chunk-QPWR3YMY.js';
11
11
  import { deployedBase, isDeployed, deployedPaths, deployedDir } from './chunk-5KRMNUL4.js';
12
- import { resolveSkWorkspaceConfig, effectiveAgentInstances, loadMcpServerDeclarations, resolveGlobalHarnesses, resolveGlobalSkWorkspaceConfig, getGlobalCacheDir, sourceCacheKey, getRepoCommit, matchPin, buildProvenanceIndex, coerceVersion, sourceShortName, readLinks, isCommitOnRemote, resolveAll, agentSourceCacheDir, ensureRepo, scanRepo, SKAILE_YAML_DEFAULT, checkRepoStatus, resolveAgentDefinitionDir, resolveAsset, MAX_PRESET_DEPTH, PresetDepthError, DEFAULT_AGENT_INSTANCE_ID } from './chunk-OHNKYHRR.js';
12
+ import { resolveSkWorkspaceConfig, effectiveAgentInstances, mcpDeclarationFromMdContent, loadMcpServerDeclarations, resolveGlobalHarnesses, resolveGlobalSkWorkspaceConfig, getGlobalCacheDir, sourceCacheKey, getRepoCommit, matchPin, buildProvenanceIndex, coerceVersion, sourceShortName, readLinks, isCommitOnRemote, findWorkspaceRoot, resolveAll, agentSourceCacheDir, ensureRepo, resolveAgentDefinitionDir, scanRepo, SKAILE_YAML_DEFAULT, checkRepoStatus, resolveAsset, MAX_PRESET_DEPTH, PresetDepthError, DEFAULT_AGENT_INSTANCE_ID } from './chunk-KJSC2ZD6.js';
13
13
  import { globalSkaileDir, globalCacheRoot, migrateLegacyDir } from './chunk-L5VSIPQD.js';
14
14
  import { HARNESS_TARGETS, SUPPORTED_HARNESSES } from './chunk-VTETQCR6.js';
15
- import { loadSkPublishManifest, listAssetFiles, isNonAssetDir, isNonAssetFile } from './chunk-UQ33Y4DM.js';
16
- import { scanDirectory, parseFrontmatter, parseRequires, dedupeRequirements, readAssetBody } from './chunk-GOCWM7FN.js';
17
- import { parseAssetRef, parseAssetRefLoose, assetRefToStr, slugifyAssetName, depToStr, parseDep } from './chunk-WDXZA7QW.js';
15
+ import { listAssetFiles, loadSkPublishManifest, decodeSkaileManifest, isNonAssetDir, isNonAssetFile } from './chunk-KPKGWXUJ.js';
16
+ import { scanDirectory, parseFrontmatter, parseRequires, dedupeRequirements, readAssetBody } from './chunk-JMIKGAUA.js';
17
+ import { parseAssetRef, parseAssetRefLoose, assetRefToStr, slugifyAssetName, depToStr, parseDep } from './chunk-IZQOO6MN.js';
18
18
  import { spawnSync } from 'child_process';
19
- import { existsSync, rmSync, readFileSync, readdirSync, lstatSync, writeFileSync, unlinkSync, mkdirSync } from 'fs';
19
+ import { existsSync, rmSync, readFileSync, writeFileSync, readdirSync, lstatSync, statSync, unlinkSync, mkdirSync } from 'fs';
20
+ import { homedir } from 'os';
20
21
  import path, { resolve, dirname, sep, join, relative, posix } from 'path';
21
22
  import { gt } from 'semver';
22
23
  import { parse, stringify } from 'yaml';
@@ -595,6 +596,14 @@ function requiredFieldsAdvice(id, keys) {
595
596
  const setting = keys.map((k) => `${k}: env:${k}`).join(", ");
596
597
  return `needs ${keys.join(", ")} (required): set it with an mcp_servers entry (id: ${id}, env: ${setting}) in skaile.yaml and export the variable, then run skaile install`;
597
598
  }
599
+ function mcpNotRunnableReason(mdContent, name) {
600
+ try {
601
+ const decl = mcpDeclarationFromMdContent(mdContent, name);
602
+ return decl ? skipReason(decl) : void 0;
603
+ } catch {
604
+ return void 0;
605
+ }
606
+ }
598
607
  function mcpConfigFileOf(harnessTarget) {
599
608
  return TARGET_MCP_CONFIG[harnessTarget];
600
609
  }
@@ -616,6 +625,38 @@ async function mcpRenderedFootprint(projectDir, harnessTarget, name) {
616
625
  ...dir && hasPayload ? { payloadDir: `${path.relative(projectDir, dir).split(path.sep).join("/")}/` } : {}
617
626
  };
618
627
  }
628
+ async function mcpCommittedRenderIntact(projectDir, harnessTarget, name) {
629
+ const relConfig = TARGET_MCP_CONFIG[harnessTarget];
630
+ if (!relConfig) return false;
631
+ if ((await readNotedSkips(projectDir, harnessTarget))[name] !== void 0) return true;
632
+ let ledger;
633
+ let servers;
634
+ try {
635
+ ledger = await readManaged(projectDir, harnessTarget);
636
+ const file = await readJsonStrict(path.join(projectDir, relConfig));
637
+ servers = file?.mcpServers ?? {};
638
+ } catch {
639
+ return false;
640
+ }
641
+ if (!Object.hasOwn(servers, name)) return false;
642
+ const entry = servers[name];
643
+ const recorded = ledger.ids.includes(name) ? ledger.rendered[name] : void 0;
644
+ if (recorded !== void 0 && entryHash(entry) !== recorded) return false;
645
+ const e = entry ?? {};
646
+ const strings = [e.command, ...Array.isArray(e.args) ? e.args : []].filter(
647
+ (v) => typeof v === "string"
648
+ );
649
+ for (const v of strings) {
650
+ const at = v.indexOf(".skaile/assets/");
651
+ if (at < 0) continue;
652
+ try {
653
+ await fs.stat(path.join(projectDir, v.slice(at)));
654
+ } catch {
655
+ return false;
656
+ }
657
+ }
658
+ return true;
659
+ }
619
660
  async function renderMcpServerConfig(projectDir, harnessTarget, opts) {
620
661
  const warn = opts?.warn ?? ((m) => console.warn(`skaile: ${m}`));
621
662
  const relConfig = TARGET_MCP_CONFIG[harnessTarget];
@@ -839,6 +880,16 @@ function libraryDiskHashes(libPath, cache, registry) {
839
880
  }
840
881
  return hashes;
841
882
  }
883
+ function repoManifestVersion(repoDir) {
884
+ try {
885
+ const { manifest } = decodeSkaileManifest(
886
+ readFileSync(join(repoDir, "skaile.manifest.yaml"), "utf8")
887
+ );
888
+ return manifest.version || void 0;
889
+ } catch {
890
+ return void 0;
891
+ }
892
+ }
842
893
  function manifestFileHasAssetsArray(libPath) {
843
894
  try {
844
895
  const doc = parse(readFileSync(join(libPath, "skaile.manifest.yaml"), "utf8"));
@@ -847,6 +898,14 @@ function manifestFileHasAssetsArray(libPath) {
847
898
  return false;
848
899
  }
849
900
  }
901
+ var libraryAssetCheck;
902
+ function setLibraryAssetCheck(check) {
903
+ libraryAssetCheck = check;
904
+ }
905
+ var renamedEntryLookup;
906
+ function setRenamedEntryLookup(lookup) {
907
+ renamedEntryLookup = lookup;
908
+ }
850
909
  function isUnlistedLibraryAsset(entry, libPath) {
851
910
  const manifest = loadSkPublishManifest(libPath);
852
911
  const listed = manifest?.assets ?? [];
@@ -1045,6 +1104,13 @@ function parseCanonicalLockKey(key) {
1045
1104
  };
1046
1105
  }
1047
1106
  }
1107
+ function labelDiffHeaders(diff, from, to) {
1108
+ const stamp = "\\t(?:\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}(?:\\.\\d+)? [+-]\\d{4}|[A-Z][a-z]{2} [A-Z][a-z]{2} [ \\d]\\d \\d{2}:\\d{2}:\\d{2} \\d{4})";
1109
+ return diff.replace(
1110
+ new RegExp(`^(---|\\+\\+\\+) (.*?)${stamp}$`, "gm"),
1111
+ (_m, mark, file) => `${mark} ${file} (${mark === "---" ? from : to})`
1112
+ );
1113
+ }
1048
1114
  function softDepName(ref) {
1049
1115
  const at = ref.lastIndexOf("@");
1050
1116
  const base = at > 0 ? ref.slice(0, at) : ref;
@@ -1117,7 +1183,9 @@ function relockedRefs(prev, next) {
1117
1183
  e.library?.version ?? "",
1118
1184
  e.library && !e.library.installed ? e.library.name : "",
1119
1185
  e.library?.sha256 ?? e.sha256,
1120
- key.includes("#0.0.0-sha.") ? "" : e.source?.commit ?? ""
1186
+ key.includes("#0.0.0-sha.") ? "" : e.source?.commit ?? "",
1187
+ // The `../` compose folders are part of the lock: `update` moving them is a re-lock.
1188
+ (e.composed ?? []).map((f) => `${f.path}:${f.sha256}`).join("\n")
1121
1189
  ].join("\0");
1122
1190
  const out = [];
1123
1191
  for (const [key, entry] of Object.entries(next.assets)) {
@@ -1133,6 +1201,9 @@ function relockedRefs(prev, next) {
1133
1201
  }
1134
1202
  return out;
1135
1203
  }
1204
+ function planPaths(folders, renders, files) {
1205
+ return renders.length > 0 ? { folders: [], files: [], renders } : { folders, files, renders: [] };
1206
+ }
1136
1207
  function libraryMoves(prev, next) {
1137
1208
  if (!prev) return [];
1138
1209
  const out = [];
@@ -1152,9 +1223,13 @@ function libraryMoves(prev, next) {
1152
1223
  }
1153
1224
  return out;
1154
1225
  }
1226
+ function unregisteredLibraryName(origin) {
1227
+ return lastLibraryName(origin.name) ?? origin.name;
1228
+ }
1155
1229
  function unregisteredLibraryFix(origin, ref) {
1156
- const register = origin.url ? `\`skaile library init ${origin.name} --git ${origin.url}\` (no registered library has the remote ${origin.url} either)` : `\`skaile library init ${origin.name} --path <dir>\``;
1157
- return `register library "${origin.name}" again with ${register}, or drop the dependency with \`skaile remove ${ref}\``;
1230
+ const name = unregisteredLibraryName(origin);
1231
+ const register = origin.url ? `\`skaile library init ${name} --git ${origin.url}\` (no registered library has the remote ${origin.url} either)` : `\`skaile library init ${name} --path <dir>\``;
1232
+ return `register library "${name}" again with ${register}, or drop the dependency with \`skaile remove ${ref}\``;
1158
1233
  }
1159
1234
  function declaredNoHolderReason(ref) {
1160
1235
  return `is declared but no library or source holds it \u2014 register the library that holds it with \`skaile library init <name> --path <dir>\`, or drop it with \`skaile remove ${ref.split("#")[0]}\``;
@@ -1180,7 +1255,7 @@ function libraryCollisionSummary(all) {
1180
1255
  return `${all.length} assets exist in several libraries (${libraries.join(", ")}); using ${usedText.join(", ")}. Highest version wins; an equal version goes to the default library, then by name. ` + (differing.length > 0 ? `Same version, different bytes for: ${differing.join(", ")}. ` : "") + "Pin a version (<ref>#<version>) to choose another; `--verbose` lists each asset.";
1181
1256
  }
1182
1257
  function lockedLibraryProblem(lookup, origin, ref) {
1183
- return lookup.state === "dir-missing" ? missingLibraryDirMessage({ name: origin.name, path: lookup.path }) : `library "${origin.name}" is not registered \u2014 renamed or removed? ${unregisteredLibraryFix(origin, ref)}`;
1258
+ return lookup.state === "dir-missing" ? missingLibraryDirMessage({ name: origin.name, path: lookup.path }) : `library "${unregisteredLibraryName(origin)}" is not registered \u2014 renamed or removed? ${unregisteredLibraryFix(origin, ref)}`;
1184
1259
  }
1185
1260
  function gapIdentity(ref) {
1186
1261
  const p = parseRefTolerant(ref);
@@ -1242,7 +1317,6 @@ function isSymlink(p) {
1242
1317
  return false;
1243
1318
  }
1244
1319
  }
1245
- var FACTORY_SOURCE_URL = "https://github.com/skaile-ai/ai-assets";
1246
1320
  function resolveFactoryAssetsDir() {
1247
1321
  for (const dir of factoryAssetRoots()) {
1248
1322
  if (existsSync(join(dir, "skaile.manifest.yaml"))) return dir;
@@ -1429,6 +1503,43 @@ var AssetManager = class {
1429
1503
  return !recorded || recorded.includes(target);
1430
1504
  });
1431
1505
  }
1506
+ /**
1507
+ * A 5.12.0 global record has no `global_asset_harnesses:` key, yet `add --global --harness`
1508
+ * wrote one copy only. On the first global `install`, an entry with exactly one existing copy
1509
+ * among several default harnesses is that harness's subset (OQ6): record it, so the install
1510
+ * does not fan the asset out to harnesses the user never asked for (p1442). A copy under the
1511
+ * 5.12.0 omp folder (`~/.omp/skills`) counts as an omp copy. Nothing is inferred once the key
1512
+ * exists (an entry without a row then means "every harness"), with one default harness, or
1513
+ * with no copy yet. Global mode only; returns the rows written.
1514
+ */
1515
+ inferLegacyHarnessSubsets(entries) {
1516
+ if (!this.global) return [];
1517
+ const configPath = join(this.stateRoot, "skaile.yaml");
1518
+ if (!existsSync(configPath)) return [];
1519
+ let raw;
1520
+ try {
1521
+ raw = parse(readFileSync(configPath, "utf8"));
1522
+ } catch {
1523
+ return [];
1524
+ }
1525
+ if (!raw || typeof raw !== "object" || ENTRY_HARNESSES_KEY in raw) return [];
1526
+ const all = this.defaultGlobalHarnesses();
1527
+ if (all.length < 2) return [];
1528
+ const rows = [];
1529
+ for (const e of entries) {
1530
+ const component = this.installTarget(e.kind);
1531
+ if (!component || !e.name) continue;
1532
+ const holding = all.filter(
1533
+ (h) => isDeployed(component, e.name, h, true) || h === "omp" && legacyOmpPaths(component, e.name).length > 0
1534
+ );
1535
+ if (holding.length !== 1) continue;
1536
+ const publisher = e.publisher?.replace(/^@/, "");
1537
+ const ref = `${e.kind}:${publisher ? `@${publisher}/` : ""}${e.name}`;
1538
+ if (this.setEntryHarnessRow(raw, ref, holding)) rows.push({ ref, harnesses: holding });
1539
+ }
1540
+ if (rows.length > 0) writeFileSync(configPath, stringify(raw, { lineWidth: 120 }));
1541
+ return rows;
1542
+ }
1432
1543
  /**
1433
1544
  * Set (or, with `harnesses` undefined, clear) the harness row of `ref` in the parsed
1434
1545
  * `~/.skaile/skaile.yaml` `raw`. Returns whether `raw` changed.
@@ -1456,6 +1567,78 @@ var AssetManager = class {
1456
1567
  else raw[ENTRY_HARNESSES_KEY] = rows;
1457
1568
  return true;
1458
1569
  }
1570
+ /** The harness row of `kind:name` (publisher must agree when both sides have one) in the parsed `raw`. */
1571
+ harnessRowIn(raw, id) {
1572
+ const wantPub = id.publisher?.replace(/^@/, "");
1573
+ for (const [key, harnesses] of Object.entries(decodeEntryHarnesses(raw[ENTRY_HARNESSES_KEY]))) {
1574
+ const other = parseAssetRefLoose(key);
1575
+ if (!other || other.kind !== id.kind || other.name !== id.name) continue;
1576
+ const havePub = other.publisher?.replace(/^@/, "");
1577
+ if (wantPub === void 0 || havePub === void 0 || havePub === wantPub) return harnesses;
1578
+ }
1579
+ return void 0;
1580
+ }
1581
+ /**
1582
+ * The harness row `add --global --harness <requested>` leaves for `ref`: the recorded set plus
1583
+ * `requested`. An asset declared with no row goes to every global harness, so its set is that
1584
+ * default; `undefined` (no row) when `requested` is already part of it. A new asset gets
1585
+ * `requested` alone.
1586
+ */
1587
+ widenedHarnessRow(raw, ref, requested, declared) {
1588
+ const id = parseAssetRefLoose(ref);
1589
+ if (!id) return [...requested];
1590
+ const recorded = this.harnessRowIn(raw, id);
1591
+ if (!recorded && !declared) return [...requested];
1592
+ const current = recorded ?? this.defaultGlobalHarnesses();
1593
+ const union = [.../* @__PURE__ */ new Set([...current, ...requested])];
1594
+ if (!recorded && union.length === current.length) return void 0;
1595
+ return union;
1596
+ }
1597
+ /**
1598
+ * After `add --global --harness X`: widen the row of every other entry of the add that has one
1599
+ * (a requirement declared on its own with `--harness`), so the copy now deployed into X stays
1600
+ * managed. Entries with no row already go to every harness.
1601
+ */
1602
+ widenDependencyRows(entries, requested) {
1603
+ const named = this.harnessesOverride;
1604
+ if (!this.global || !named?.length) return;
1605
+ const configPath = join(this.stateRoot, "skaile.yaml");
1606
+ if (!existsSync(configPath)) return;
1607
+ const raw = parse(readFileSync(configPath, "utf8")) ?? {};
1608
+ let changed = false;
1609
+ for (const e of entries) {
1610
+ if (!e.name || e.kind === requested.kind && e.name === requested.name) continue;
1611
+ const recorded = this.harnessRowIn(raw, e);
1612
+ if (!recorded) continue;
1613
+ const union = [.../* @__PURE__ */ new Set([...recorded, ...named])];
1614
+ if (union.length === recorded.length) continue;
1615
+ const publisher = e.publisher?.replace(/^@/, "");
1616
+ const ref = `${e.kind}:${publisher ? `@${publisher}/` : ""}${e.name}`;
1617
+ if (this.setEntryHarnessRow(raw, ref, union)) changed = true;
1618
+ }
1619
+ if (changed) writeFileSync(configPath, stringify(raw, { lineWidth: 120 }));
1620
+ }
1621
+ /**
1622
+ * `add --global --harness X --only`: name each harness that leaves the asset's set but still
1623
+ * holds a copy. The copy stays on disk; `remove --global --harness <h>` deletes it.
1624
+ */
1625
+ reportDroppedCopies(target, onDropped) {
1626
+ const named = this.harnessesOverride;
1627
+ const component = this.installTarget(target.kind);
1628
+ const configPath = join(this.stateRoot, "skaile.yaml");
1629
+ if (!named?.length || !component || !existsSync(configPath)) return;
1630
+ const raw = parse(readFileSync(configPath, "utf8")) ?? {};
1631
+ const publisher = target.publisher?.replace(/^@/, "");
1632
+ const ref = `${target.kind}:${publisher ? `@${publisher}/` : ""}${target.name}`;
1633
+ const declared = Array.isArray(raw.dependencies) && raw.dependencies.some((d) => this.sameDeclaredAsset(d, ref));
1634
+ if (!declared) return;
1635
+ const current = this.harnessRowIn(raw, target) ?? this.defaultGlobalHarnesses();
1636
+ for (const h of current) {
1637
+ if (named.includes(h) || !isDeployed(component, target.name, h, true)) continue;
1638
+ const where = deployedPaths(component, target.name, h, true)[0];
1639
+ onDropped(ref, h, where ?? deployedDir(component, target.name, h, true));
1640
+ }
1641
+ }
1459
1642
  /**
1460
1643
  * Delete the copies a 5.12.0 global install left in `~/.omp/skills`, `~/.omp/agents`,
1461
1644
  * `~/.omp/prompts` and `~/.omp/contracts` (omp reads `~/.omp/agent/<dir>`) for the assets just
@@ -1556,16 +1739,36 @@ var AssetManager = class {
1556
1739
  }
1557
1740
  /** Deploy options for one harness. In global mode, `harness` is the
1558
1741
  * fan-out target (not the constructor's `harness`, which is project-only). */
1559
- deployOptsFor(target, link, onSecretFile) {
1742
+ deployOptsFor(target, link, onSecretFile, composed) {
1743
+ const records = this.global ? void 0 : this.composedRecords();
1560
1744
  return {
1561
1745
  registry: this.registry,
1562
1746
  harness: target,
1563
1747
  global: this.global,
1564
1748
  cwd: this.projectDir,
1565
1749
  link: link === true,
1566
- ...onSecretFile ? { onSecretFile: (entry, p) => onSecretFile(canonicalEntryRef(entry), p) } : {}
1750
+ ...onSecretFile ? { onSecretFile: (entry, p) => onSecretFile(canonicalEntryRef(entry), p) } : {},
1751
+ ...records ? { lockedComposed: (agent) => records.get(agent) } : {},
1752
+ ...composed ? {
1753
+ refreshComposed: composed.refresh,
1754
+ onComposedDrift: (entry, ref) => composed.onDrift(canonicalEntryRef(entry), ref)
1755
+ } : {}
1567
1756
  };
1568
1757
  }
1758
+ /** Agent name → the `../` compose folder files its lock entry records (`LockEntry.composed`). */
1759
+ composedRecords() {
1760
+ const out = /* @__PURE__ */ new Map();
1761
+ for (const [key, entry] of Object.entries(this.tryReadLock()?.assets ?? {})) {
1762
+ if (!entry.composed?.length) continue;
1763
+ const { name } = parseCanonicalLockKey(key);
1764
+ if (name) out.set(name, entry.composed);
1765
+ }
1766
+ return out;
1767
+ }
1768
+ /** `install`'s `Missing:` line for a `../` compose folder the library moved past the lock. */
1769
+ composedDriftLine(agentRef, composeRef) {
1770
+ return `${agentRef} (composes ${composeRef}: the library's copy changed since the lock; \`skaile update\` moves the lock to it)`;
1771
+ }
1569
1772
  /**
1570
1773
  * Names of the `mcp-server` assets the lock holds (the declaration ids, unscoped); a missing
1571
1774
  * `.mcp.json` entry of one counts as drift even before skaile owned it.
@@ -1766,7 +1969,16 @@ var AssetManager = class {
1766
1969
  for (const [slug, decl] of Object.entries(repositories)) {
1767
1970
  const localPath = this.resolveRepoDir(decl, slug);
1768
1971
  if (!localPath || !existsSync(localPath)) continue;
1769
- const commit = getRepoCommit(localPath) ?? "0".repeat(40);
1972
+ if (decl.url && normalizeRepoUrl(decl.url) !== normalizeRepoUrl(FACTORY_SOURCE_URL) && !existsSync(join(localPath, ".git"))) {
1973
+ this.reportSourceError(
1974
+ { localPath, sourceUrl: decl.url, commit: "" },
1975
+ new Error(
1976
+ `${decl.url}: its clone ${localPath} is not a git repository, so git pull fails - run \`skaile source sync\` to clone it again`
1977
+ )
1978
+ );
1979
+ continue;
1980
+ }
1981
+ const commit = decl.factory ? "0".repeat(40) : getRepoCommit(localPath) ?? "0".repeat(40);
1770
1982
  clones.push({
1771
1983
  localPath,
1772
1984
  sourceUrl: decl.url ?? localPath,
@@ -2089,6 +2301,33 @@ var AssetManager = class {
2089
2301
  if (!scan) return [];
2090
2302
  return [...scan.byKey.values()].flat();
2091
2303
  }
2304
+ /**
2305
+ * The version `add` deploys for each asset of a source clone registered as a `reader`
2306
+ * library, when its file declares none: the repo commit pseudo-version (p855). `search`
2307
+ * reads this where no project is in scope, so it lists what `add` locks, not the index
2308
+ * default.
2309
+ *
2310
+ * @returns `<library>\0<kind>:@<publisher>/<name>` -> version; only assets that declare none.
2311
+ */
2312
+ async sourceAssetVersions() {
2313
+ const out = /* @__PURE__ */ new Map();
2314
+ const scan = await this.scanLibraries();
2315
+ if (!scan) return out;
2316
+ const snap = await this.loadLibraryIndex();
2317
+ for (const [key, candidates] of scan.byKey) {
2318
+ for (const c of candidates) {
2319
+ if (c.entry.version) continue;
2320
+ const lib = snap?.libraries.find((l) => l.name === c.library);
2321
+ if (lib?.ownership !== "reader" || !("url" in lib.backendConfig)) continue;
2322
+ const manifestVersion = repoManifestVersion(lib.path);
2323
+ const commit = getRepoCommit(lib.path);
2324
+ if (manifestVersion) out.set(`${c.library}\0${key}`, manifestVersion);
2325
+ else if (commit)
2326
+ out.set(`${c.library}\0${key}`, `${PSEUDO_VERSION_PREFIX}${commit.slice(0, 7)}`);
2327
+ }
2328
+ }
2329
+ return out;
2330
+ }
2092
2331
  /**
2093
2332
  * Every origin that holds `ref` (`kind:@publisher/name`) with the version it holds, in
2094
2333
  * the order `add <ref>` prefers them, so the first row is the one `add` deploys: the
@@ -2598,7 +2837,14 @@ var AssetManager = class {
2598
2837
  if (!existsSync(indexPath)) return null;
2599
2838
  const index = new LocalIndex({ indexPath });
2600
2839
  try {
2601
- return { libraries: await index.listSources(), defs: await index.listAssetDefs() };
2840
+ const libraries = await index.listSources();
2841
+ const defs = await index.listAssetDefs();
2842
+ const hidden = this.undeclaredSourceClones(libraries);
2843
+ if (hidden.size === 0) return { libraries, defs };
2844
+ return {
2845
+ libraries: libraries.filter((l) => !hidden.has(l.id)),
2846
+ defs: defs.filter((d) => !d.libraryId || !hidden.has(d.libraryId))
2847
+ };
2602
2848
  } finally {
2603
2849
  index.close();
2604
2850
  }
@@ -2606,6 +2852,32 @@ var AssetManager = class {
2606
2852
  return null;
2607
2853
  }
2608
2854
  }
2855
+ /**
2856
+ * Ids of the registered libraries that are the clone of a source this project does not
2857
+ * declare. `source add` registers the clone as a machine-wide reader library so `search`
2858
+ * and the indexer can read it, but a `sources:` clone belongs to the projects that declare
2859
+ * it (OQ12, p321): another project neither resolves it, nor ties with it by name, nor has
2860
+ * `update` re-pin its locked assets to it. A reader library the user registered by hand
2861
+ * (`library init --reader --git`) lives outside the sources cache and is never hidden.
2862
+ */
2863
+ undeclaredSourceClones(libraries) {
2864
+ const hidden = /* @__PURE__ */ new Set();
2865
+ if (!this.global && !findWorkspaceRoot(this.projectDir)) return hidden;
2866
+ let declared;
2867
+ try {
2868
+ declared = new Set(this.loadConfig().sources.map((s) => sourceCacheKey(s.url)));
2869
+ } catch {
2870
+ return hidden;
2871
+ }
2872
+ const cloneRoots = [join(globalCacheRoot(), "sources"), this.reposDir].map((d) => resolve(d));
2873
+ for (const l of libraries) {
2874
+ if (l.ownership !== "reader" || !("url" in l.backendConfig)) continue;
2875
+ const at = resolve(l.path);
2876
+ if (!cloneRoots.some((root) => at.startsWith(root + sep))) continue;
2877
+ if (!declared.has(sourceCacheKey(l.backendConfig.url))) hidden.add(l.id);
2878
+ }
2879
+ return hidden;
2880
+ }
2609
2881
  /**
2610
2882
  * Library-origin lock entries for the assets {@link resolveLibraryEntries}
2611
2883
  * matched. The locked version is the one actually deployed: the on-disk
@@ -2749,14 +3021,19 @@ var AssetManager = class {
2749
3021
  * `install --locked`. An entry is restored only when its library is still
2750
3022
  * registered/on disk, still holds the asset, and still offers the locked
2751
3023
  * version; anything else is reported in `missing` with the reason.
3024
+ *
3025
+ * @param opts.storeFallback - `install --locked` only: an entry whose library is not registered
3026
+ * here is restored from a store that serves the locked sha256 (OQ15, p1490). Reads such as
3027
+ * the drift check never ask a store.
2752
3028
  */
2753
- async restoreLibraryLockEntries(lock) {
3029
+ async restoreLibraryLockEntries(lock, opts) {
2754
3030
  const entries = [];
2755
3031
  const missing = [];
3032
+ const viaStore = [];
2756
3033
  const locked = Object.entries(lock.assets).filter(
2757
3034
  ([, e]) => e.library !== void 0 && !e.library.installed
2758
3035
  );
2759
- if (locked.length === 0) return { entries, missing };
3036
+ if (locked.length === 0) return { entries, missing, viaStore };
2760
3037
  const snap = await this.loadLibraryIndex();
2761
3038
  const diskHashes = /* @__PURE__ */ new Map();
2762
3039
  const fileSets = /* @__PURE__ */ new Map();
@@ -2769,6 +3046,12 @@ var AssetManager = class {
2769
3046
  }
2770
3047
  const lookup = this.lockedLibrary(origin, snap, { kind, publisher, name });
2771
3048
  if (lookup.state !== "ok") {
3049
+ const fallback = lookup.state === "unregistered" && opts?.storeFallback ? await this.storeEntryForLockedLibraryAsset(publisher, kind, name, origin) : null;
3050
+ if (fallback) {
3051
+ entries.push(fallback);
3052
+ viaStore.push(ref);
3053
+ continue;
3054
+ }
2772
3055
  missing.push(
2773
3056
  `${ref} (${lockedLibraryProblem(lookup, origin, `${kind}:@${publisher}/${name}`)})`
2774
3057
  );
@@ -2811,22 +3094,94 @@ var AssetManager = class {
2811
3094
  found.version = sameVersion ? origin.version : current;
2812
3095
  entries.push(withLibraryFiles(found, libPath, fileSets, this.registry));
2813
3096
  }
2814
- return { entries, missing };
3097
+ return { entries, missing, viaStore };
3098
+ }
3099
+ /**
3100
+ * Whether a library-origin lock entry was restored from its lock url: its library is not
3101
+ * registered here, and the project's store cache holds the locked version (`install --locked`
3102
+ * fetched and SHA-verified it there against the sha256 the lock recorded). Such an entry is
3103
+ * as restorable as a source- or store-origin one: `check` and `doctor` read its requirements
3104
+ * from the deployed copy and do not ask for the library (OQ8, p1655). An unregistered library
3105
+ * that never restored anything here stays a gap.
3106
+ */
3107
+ async restoredFromLockUrl(key, entry, snap) {
3108
+ const origin = entry.library;
3109
+ if (!origin || origin.installed || !origin.sha256 || !origin.version) return false;
3110
+ if (isPseudoVersion(origin.version)) return false;
3111
+ const { publisher, kind, name } = parseCanonicalLockKey(key);
3112
+ if (!publisher || !kind || !name) return false;
3113
+ if (this.lockedLibrary(origin, snap, { kind, publisher, name }).state !== "unregistered") {
3114
+ return false;
3115
+ }
3116
+ try {
3117
+ return readdirSync(join(this.storeCacheDir, publisher, kind, name, origin.version)).length > 0;
3118
+ } catch {
3119
+ return false;
3120
+ }
3121
+ }
3122
+ /**
3123
+ * The store's copy of a library-origin lock entry whose library is not registered here: a
3124
+ * store (project `stores:` or the global catalog) that serves `kind:@publisher/name#version`
3125
+ * under the very sha256 the lock recorded holds the same bytes. The entry is fetched and
3126
+ * verified like any store asset; the lock stays as it is. `null` when the lock has no hash,
3127
+ * no store answers, or the hash differs (OQ15, p1490).
3128
+ */
3129
+ async storeEntryForLockedLibraryAsset(publisher, kind, name, origin) {
3130
+ if (!origin.sha256 || !origin.version || isPseudoVersion(origin.version)) return null;
3131
+ const { repositories, stores: projectStores } = this.loadConfig();
3132
+ const stores = [...projectStores, ...await this.globalStoreFallback(projectStores)];
3133
+ const fetcher = this.storeFetcherFor(stores);
3134
+ if (!fetcher) return null;
3135
+ for (const store of stores) {
3136
+ try {
3137
+ const m = await fetcher.getInstallManifest(
3138
+ store.url,
3139
+ `${kind}:@${publisher}/${name}#${origin.version}`
3140
+ );
3141
+ if (!m || m.sha256 !== origin.sha256) continue;
3142
+ const [entry] = await this.buildCatalogEntries(
3143
+ [
3144
+ {
3145
+ publisher,
3146
+ kind,
3147
+ name,
3148
+ version: origin.version,
3149
+ sourceUrl: m.sourceUrl,
3150
+ commit: m.commit,
3151
+ files: m.files,
3152
+ sha256: m.sha256,
3153
+ ...m.deps?.length ? { deps: m.deps, storeRequires: m.deps } : {}
3154
+ }
3155
+ ],
3156
+ repositories
3157
+ );
3158
+ if (entry) return entry;
3159
+ } catch (err) {
3160
+ this.noteStoreFailure(store.url, err);
3161
+ }
3162
+ }
3163
+ return null;
2815
3164
  }
2816
3165
  /**
2817
3166
  * Whether a deployed copy exists for the lock entries `install` kept for the unresolved
2818
- * `ref`. A kind with no install target has no deployed copy.
3167
+ * `ref`. A kind with no install target has none, except an `mcp-server`: its copy is the
3168
+ * `.mcp.json` entry skaile wrote and the payload that entry points into (p792).
2819
3169
  */
2820
- keptCopyDeployed(kept, ref) {
3170
+ async keptCopyDeployed(kept, ref) {
2821
3171
  const want = parseRefTolerant(ref.split(" ")[0] ?? ref);
2822
3172
  if (!want) return false;
2823
- return Object.keys(kept.assets).some((key) => {
3173
+ for (const key of Object.keys(kept.assets)) {
2824
3174
  const id = parseCanonicalLockKey(key);
2825
- if (id.kind !== want.kind || id.name !== want.name) return false;
2826
- if (want.publisher && id.publisher !== want.publisher) return false;
3175
+ if (id.kind !== want.kind || id.name !== want.name) continue;
3176
+ if (want.publisher && id.publisher !== want.publisher) continue;
2827
3177
  const component = this.installTarget(id.kind);
2828
- return component !== void 0 && isDeployed(component, id.name, this.harness, this.global, this.projectDir);
2829
- });
3178
+ if (component !== void 0) {
3179
+ if (isDeployed(component, id.name, this.harness, this.global, this.projectDir)) return true;
3180
+ } else if (id.kind === "mcp-server" && !this.global && await mcpCommittedRenderIntact(this.projectDir, this.harness, id.name)) {
3181
+ return true;
3182
+ }
3183
+ }
3184
+ return false;
2830
3185
  }
2831
3186
  /** Whether the deployed copy of a library asset is on disk; a kind with no install target has none to check. */
2832
3187
  lockedCopyDeployed(kind, name) {
@@ -2949,6 +3304,24 @@ var AssetManager = class {
2949
3304
  lockedRefsNamed(kind, name) {
2950
3305
  return Object.keys(this.tryReadLock()?.assets ?? {}).map(parseCanonicalLockKey).filter((id) => id.kind === kind && id.name === name).map((id) => id.publisher ? `${kind}:@${id.publisher}/${name}` : `${kind}:${name}`);
2951
3306
  }
3307
+ /**
3308
+ * The library a library-origin lock entry of `kind` and `name` (and `publisher`, when given)
3309
+ * resolves to on this machine, or `undefined` when the lock holds no such entry, it is not
3310
+ * from a library, or its library is not here. `info` prints it next to `source:`.
3311
+ */
3312
+ async lockedLibraryOf(kind, name, publisher) {
3313
+ const bare = publisher?.replace(/^@/, "");
3314
+ const names = await this.lockedLibraryNames();
3315
+ for (const [key, entry] of Object.entries(this.tryReadLock()?.assets ?? {})) {
3316
+ if (!entry.library || entry.library.installed) continue;
3317
+ const id = parseCanonicalLockKey(key);
3318
+ if (id.kind !== kind || id.name !== name || !id.publisher) continue;
3319
+ if (bare && id.publisher !== bare) continue;
3320
+ const lib = names.get(`${kind}:${id.publisher}/${name}`);
3321
+ if (lib) return lib;
3322
+ }
3323
+ return void 0;
3324
+ }
2952
3325
  /**
2953
3326
  * Where each library-origin lock entry of this project resolves on this machine: identity
2954
3327
  * `kind:publisher/name` (no scope sigil, no version) → the name the locked library is
@@ -2991,6 +3364,88 @@ var AssetManager = class {
2991
3364
  }
2992
3365
  return { libraries: await this.lockedLibraryNames(), shas };
2993
3366
  }
3367
+ /**
3368
+ * Canonical refs of the locked source assets that share a source URL with `resolved` but
3369
+ * are locked at another commit than the one `resolved` carries for it. Assets in `resolved`
3370
+ * and library-origin entries are skipped.
3371
+ */
3372
+ staleSourceSiblingRefs(resolved, declared = []) {
3373
+ const pinOf = /* @__PURE__ */ new Map();
3374
+ for (const d of declared) {
3375
+ const id = refIdentity(d);
3376
+ const pin = parseAssetRefLoose(d)?.pin;
3377
+ if (id !== null && pin) pinOf.set(id.replace(/^@/, ""), pin);
3378
+ }
3379
+ const commitOf = /* @__PURE__ */ new Map();
3380
+ const held = /* @__PURE__ */ new Set();
3381
+ for (const c of resolved) {
3382
+ if (!c.sourceUrl || !c.commit) continue;
3383
+ commitOf.set(normalizeRepoUrl(c.sourceUrl), c.commit);
3384
+ held.add(`${c.publisher.replace(/^@/, "")}/${c.kind}:${c.name}`);
3385
+ }
3386
+ const out = [];
3387
+ for (const [key, entry] of Object.entries(this.tryReadLock()?.assets ?? {})) {
3388
+ const src = entry.source;
3389
+ if (!src?.url || entry.library) continue;
3390
+ const commit = commitOf.get(normalizeRepoUrl(src.url));
3391
+ if (!commit || src.commit === commit) continue;
3392
+ const { publisher, kind, name } = parseCanonicalLockKey(key);
3393
+ if (!publisher || !kind || !name || held.has(`${publisher}/${kind}:${name}`)) continue;
3394
+ const pin = pinOf.get(`${publisher}/${kind}:${name}`);
3395
+ out.push(`${kind}:@${publisher}/${name}${pin ? `#${pin}` : ""}`);
3396
+ }
3397
+ return out;
3398
+ }
3399
+ /**
3400
+ * The lock entries that came from a source or the store (a `source:`, no `library:`), by
3401
+ * identity `<publisher>/<kind>:<name>` -> locked version. The lock's origin
3402
+ * wins: a library registered later that holds the same ref does not take such an asset
3403
+ * (`update`, `install`, `add`; OQ15, p1490).
3404
+ */
3405
+ lockedOriginVersions() {
3406
+ const out = /* @__PURE__ */ new Map();
3407
+ for (const [key, e] of Object.entries(this.tryReadLock()?.assets ?? {})) {
3408
+ if (!e.source || e.library) continue;
3409
+ const { publisher, kind, name, version } = parseCanonicalLockKey(key);
3410
+ if (publisher && kind && name) out.set(`${publisher}/${kind}:${name}`, version ?? "");
3411
+ }
3412
+ return out;
3413
+ }
3414
+ /**
3415
+ * Say which registered library holds a ref the lock keeps on its source or the store: the
3416
+ * candidate that lost, with the way to switch (p1490). Printed once per `add`.
3417
+ */
3418
+ async noteLosingLibraries(refs) {
3419
+ const lock = this.tryReadLock();
3420
+ for (const ref of refs) {
3421
+ const plain = ref.split("#")[0] ?? ref;
3422
+ let hit;
3423
+ try {
3424
+ hit = await this.resolveLibraryEntries([plain], { quiet: true });
3425
+ } catch {
3426
+ continue;
3427
+ }
3428
+ const entry = hit.entries[0];
3429
+ const library = entry ? hit.libraryOf.get(entry) : void 0;
3430
+ if (!entry || !library) continue;
3431
+ const canonical = canonicalEntryRef(entry);
3432
+ const plainCanonical = canonical.split("#")[0] ?? canonical;
3433
+ const lockedUrl = Object.entries(lock?.assets ?? {}).find(([k, e]) => {
3434
+ const id = parseCanonicalLockKey(k);
3435
+ return e.source && !e.library && `${id.kind}:@${id.publisher}/${id.name}` === plainCanonical;
3436
+ })?.[1].source?.url;
3437
+ if (lockedUrl) {
3438
+ const ownClone = (await this.loadLibraryIndex())?.libraries.some(
3439
+ (l) => l.name === library && l.ownership === "reader" && "url" in l.backendConfig && sourceCacheKey(l.backendConfig.url) === sourceCacheKey(lockedUrl)
3440
+ );
3441
+ if (ownClone) continue;
3442
+ }
3443
+ this.emitLibraryCollision(
3444
+ `${plainCanonical} is locked from ${lockedUrl ?? "its source"}, which wins; library "${library}" also holds it${entry.version ? ` (${entry.version})` : ""} and is not used. To switch, run \`skaile remove ${plainCanonical}\`, then \`skaile add ${plainCanonical}\`.`,
3445
+ [plainCanonical]
3446
+ );
3447
+ }
3448
+ }
2994
3449
  // ── Store fetch-then-deploy ────────────────────────────────────────────
2995
3450
  /** A candidate's stable identity key (`<publisher>/<kind>:<name>@<version>`). */
2996
3451
  candidateKey(c) {
@@ -3150,10 +3605,19 @@ var AssetManager = class {
3150
3605
  fresh.push(ref);
3151
3606
  }
3152
3607
  if (fresh.length === 0) break;
3608
+ const keepsOrigin = (ref) => {
3609
+ const id = refIdentity(ref);
3610
+ const locked = id !== null ? opts.lockedOrigin?.get(id) : void 0;
3611
+ if (locked === void 0) return false;
3612
+ const pin = ref.includes("#") ? ref.slice(ref.indexOf("#") + 1).trim() : void 0;
3613
+ return !pin || locked === "" || matchPin(pin, locked);
3614
+ };
3615
+ const originKept = fresh.filter(keepsOrigin);
3616
+ if (opts.warnCollisions && originKept.length > 0) await this.noteLosingLibraries(originKept);
3153
3617
  const libraryTried = new Set(
3154
3618
  fresh.filter((ref) => {
3155
3619
  const id = refIdentity(ref);
3156
- return id !== null && (opts.provenanceIndex.get(id)?.length ?? 0) === 0;
3620
+ return id !== null && (opts.provenanceIndex.get(id)?.length ?? 0) === 0 && !keepsOrigin(ref);
3157
3621
  })
3158
3622
  );
3159
3623
  const hit = await this.resolveLibraryEntries([...libraryTried], {
@@ -3181,7 +3645,7 @@ var AssetManager = class {
3181
3645
  }
3182
3646
  presets.push(...r.presets);
3183
3647
  const hit2 = await this.resolveLibraryEntries(
3184
- r.missing.filter((m) => !libraryTried.has(m)),
3648
+ r.missing.filter((m) => !libraryTried.has(m) && !keepsOrigin(m)),
3185
3649
  {
3186
3650
  quiet: !opts.warnCollisions,
3187
3651
  lockedLibraries: opts.lockedLibraries,
@@ -3324,6 +3788,13 @@ var AssetManager = class {
3324
3788
  }
3325
3789
  });
3326
3790
  if (intact) {
3791
+ const { restoreScriptModes } = await import('./library/index.js');
3792
+ restoreScriptModes(
3793
+ targetDir,
3794
+ c.files.map(
3795
+ (f) => root && f.path.startsWith(`${root}/`) ? f.path.slice(root.length + 1) : f.path
3796
+ )
3797
+ );
3327
3798
  staged.set(this.candidateKey(c), join(targetDir, this.stagedManifestRel(c)));
3328
3799
  continue;
3329
3800
  }
@@ -3484,7 +3955,8 @@ var AssetManager = class {
3484
3955
  };
3485
3956
  }
3486
3957
  const orphans = await this.orphanedSourceGaps(
3487
- await this.declaredLockKeys(lockForLibraries, dependencies)
3958
+ await this.declaredLockKeys(lockForLibraries, dependencies),
3959
+ true
3488
3960
  );
3489
3961
  if (orphans.size > 0) {
3490
3962
  return {
@@ -3527,6 +3999,7 @@ var AssetManager = class {
3527
3999
  // library holding the same ref does not move a locked asset. `update` re-resolves.
3528
4000
  ...opts?.moveLibraries ? { preferLocked: await this.lockedPreference() } : { lockedLibraries: await this.lockedLibraryNames() },
3529
4001
  ...keepLocked ? { keepLocked } : {},
4002
+ lockedOrigin: this.lockedOriginVersions(),
3530
4003
  readStoreRequires: true
3531
4004
  });
3532
4005
  this.assertNoFolderCollision([...resolved, ...lib.entries.map((e) => this.entryIdentity(e))]);
@@ -3570,7 +4043,7 @@ var AssetManager = class {
3570
4043
  const missingHints = await this.publisherChangeHints(stillMissing);
3571
4044
  const prevLock = this.tryReadLock();
3572
4045
  const kept = keptLockEntries(prevLock, stillMissing);
3573
- const storeNotChecked = this.storeNotCheckedRefs(
4046
+ const storeNotChecked = await this.storeNotCheckedRefs(
3574
4047
  kept,
3575
4048
  sources.map((s) => s.url)
3576
4049
  );
@@ -3578,7 +4051,7 @@ var AssetManager = class {
3578
4051
  for (const ref of /* @__PURE__ */ new Set([...kept.refs, ...fixes.keys()])) {
3579
4052
  const fix = fixes.get(ref);
3580
4053
  const notes = [
3581
- kept.refs.has(ref) ? this.keptCopyDeployed(kept, ref) ? "the deployed copy and its lock entry are kept" : "its lock entry is kept; no deployed copy exists" : void 0,
4054
+ kept.refs.has(ref) ? await this.keptCopyDeployed(kept, ref) ? "the deployed copy and its lock entry are kept" : "its lock entry is kept; no deployed copy exists" : void 0,
3582
4055
  fix
3583
4056
  ].filter((n) => n !== void 0);
3584
4057
  missingHints[ref] = `${fix ? "" : missingHints[ref] ?? ""} (${notes.join("; ")})`;
@@ -3597,12 +4070,32 @@ var AssetManager = class {
3597
4070
  const entries = [...await this.buildCatalogEntries(resolved, repositories), ...lib.entries];
3598
4071
  assertFlowsLoad(entries, "installed");
3599
4072
  assertNoLibrarySymlinks(entries, this.registry, repositories, opts?.link === true);
4073
+ const forceCommand = opts?.moveLibraries === true ? "skaile update --force" : "skaile install --force";
3600
4074
  if (!opts?.force) {
3601
4075
  await this.assertNoHandEditsPruned(
3602
4076
  resolved,
3603
- /* @__PURE__ */ new Set([...Object.keys(libraryLock), ...Object.keys(kept.assets)])
4077
+ /* @__PURE__ */ new Set([...Object.keys(libraryLock), ...Object.keys(kept.assets)]),
4078
+ forceCommand
4079
+ );
4080
+ }
4081
+ const relockRemoved = await this.relockDeletions(
4082
+ entries,
4083
+ prevLock,
4084
+ withKeptLockEntries(freshLock, kept),
4085
+ opts?.force === true,
4086
+ forceCommand,
4087
+ lib.libraryOf
4088
+ );
4089
+ if (!opts?.force) this.assertNoUnrecordedAgentOverwrite(entries, prevLock, forceCommand);
4090
+ if (!opts?.force) {
4091
+ await this.assertNoHandEditsOverwritten(
4092
+ entries,
4093
+ prevLock,
4094
+ withKeptLockEntries(freshLock, kept),
4095
+ forceCommand
3604
4096
  );
3605
4097
  }
4098
+ const inferredHarnesses = this.inferLegacyHarnessSubsets(entries);
3606
4099
  const { removed, replaced } = this.reconcileRemovedAssets(
3607
4100
  resolved,
3608
4101
  /* @__PURE__ */ new Set([...Object.keys(libraryLock), ...Object.keys(kept.assets)])
@@ -3610,6 +4103,7 @@ var AssetManager = class {
3610
4103
  const copyDrift = await this.lockedCopyDrift();
3611
4104
  const missingCopies = await this.incompleteDeploys();
3612
4105
  const unrecordedAgents = this.unrecordedAgentFiles(prevLock);
4106
+ const composedDrift = /* @__PURE__ */ new Set();
3613
4107
  const deployed = [
3614
4108
  ...new Set(
3615
4109
  this.targetHarnesses().flatMap(
@@ -3617,7 +4111,10 @@ var AssetManager = class {
3617
4111
  this.entriesForHarness(entries, t),
3618
4112
  repositories,
3619
4113
  patches,
3620
- this.deployOptsFor(t, opts?.link)
4114
+ this.deployOptsFor(t, opts?.link, void 0, {
4115
+ refresh: opts?.moveLibraries === true,
4116
+ onDrift: (ref, composeRef) => composedDrift.add(this.composedDriftLine(ref, composeRef))
4117
+ })
3621
4118
  )
3622
4119
  )
3623
4120
  )
@@ -3625,14 +4122,24 @@ var AssetManager = class {
3625
4122
  this.sweepLegacyOmpCopies(entries);
3626
4123
  const mcpRestored = await this.renderHarnessMcpConfig();
3627
4124
  const agentRender = await this.renderAgentInstances();
3628
- const restored = [...this.restoredCopies(copyDrift, deployed, missingCopies), ...mcpRestored];
4125
+ const restored = [
4126
+ ...this.restoredCopies(copyDrift, deployed, missingCopies),
4127
+ ...relockRemoved,
4128
+ ...mcpRestored
4129
+ ];
3629
4130
  const rewritten = this.rewrittenAgentFiles(unrecordedAgents);
3630
4131
  const lockWanted = resolved.length > 0 || lib.entries.length > 0 || existsSync(this.lockPath);
3631
4132
  let lockWritten = false;
3632
4133
  let relocked = [];
3633
4134
  let moved = [];
3634
4135
  if (lockWanted) {
3635
- const lockData = this.withRenderedAgents(withKeptLockEntries(freshLock, kept));
4136
+ const lockData = this.withCopiedFiles(
4137
+ this.withRenderedAgents(
4138
+ withKeptLockEntries(freshLock, kept),
4139
+ opts?.moveLibraries ? void 0 : prevLock
4140
+ ),
4141
+ prevLock
4142
+ );
3636
4143
  moved = libraryMoves(prevLock, libraryLock);
3637
4144
  const movedRefs = new Set(moved.map((m) => m.ref));
3638
4145
  relocked = relockedRefs(prevLock, lockData).filter((k) => !movedRefs.has(k.split("#")[0]));
@@ -3648,12 +4155,17 @@ var AssetManager = class {
3648
4155
  return {
3649
4156
  deployed,
3650
4157
  removed,
3651
- missing: [...reportedMissing, ...versionBlocked.filter((b) => !reportedMissing.includes(b))],
4158
+ missing: [
4159
+ ...reportedMissing,
4160
+ ...versionBlocked.filter((b) => !reportedMissing.includes(b)),
4161
+ ...composedDrift
4162
+ ],
3652
4163
  ...placeholderBodies.length > 0 ? { placeholderBodies } : {},
3653
4164
  ...placeholderDescriptions.length > 0 ? { placeholderDescriptions } : {},
3654
4165
  ...storeNotChecked.length > 0 ? { storeNotChecked } : {},
3655
4166
  ...Object.keys(missingHints).length > 0 ? { missingHints } : {},
3656
4167
  ...relocked.length > 0 ? { relocked } : {},
4168
+ ...inferredHarnesses.length > 0 ? { inferredHarnesses } : {},
3657
4169
  ...moved.length > 0 ? { moved } : {},
3658
4170
  ...replaced.length > 0 ? { replaced } : {},
3659
4171
  ...restored.length > 0 ? { restored } : {},
@@ -3674,7 +4186,7 @@ var AssetManager = class {
3674
4186
  * @param declaredSourceUrls - The `sources:` URLs of `skaile.yaml`; a lock entry from one of
3675
4187
  * them is that source's, not the store's.
3676
4188
  */
3677
- storeNotCheckedRefs(kept, declaredSourceUrls) {
4189
+ async storeNotCheckedRefs(kept, declaredSourceUrls) {
3678
4190
  const skipped = this.catalogUrlOverride === "";
3679
4191
  if (!skipped && this.storeFailures.size === 0) return [];
3680
4192
  const declared = new Set(declaredSourceUrls.map(normalizeRepoUrl));
@@ -3685,7 +4197,7 @@ var AssetManager = class {
3685
4197
  const entry = kept.assets[key];
3686
4198
  return entry !== void 0 && !entry.library && entry.source !== void 0 && !declared.has(normalizeRepoUrl(entry.source.url));
3687
4199
  });
3688
- if (!fromStore || !this.keptCopyDeployed(kept, ref)) continue;
4200
+ if (!fromStore || !await this.keptCopyDeployed(kept, ref)) continue;
3689
4201
  if (skipped && this.storeFailures.size === 0) {
3690
4202
  out.push({ ref, reason: "skipped", detail: "--no-catalog: the store was not asked" });
3691
4203
  continue;
@@ -3831,7 +4343,7 @@ var AssetManager = class {
3831
4343
  * other version taking its place) whose deployed folder holds hand edits: the files that differ
3832
4344
  * from the lock are named, nothing is removed, and `--force` prunes anyway (OQ6).
3833
4345
  */
3834
- async assertNoHandEditsPruned(resolved, keep) {
4346
+ async assertNoHandEditsPruned(resolved, keep, forceCommand) {
3835
4347
  const oldLock = this.tryReadLock();
3836
4348
  if (!oldLock) return;
3837
4349
  const resolvedRefs = [
@@ -3863,7 +4375,202 @@ var AssetManager = class {
3863
4375
  throw new Error(
3864
4376
  `Not pruned: skaile.yaml no longer declares ${lines.length === 1 ? "an asset" : "assets"} whose deployed copy holds your edits:
3865
4377
  ${lines.join("\n ")}
3866
- Nothing was changed. Declare it again to keep it, or run \`skaile install --force\` to prune it with its edits.`
4378
+ Nothing was changed. Declare it again to keep it, or run \`${forceCommand}\` to prune it with its edits.`
4379
+ );
4380
+ }
4381
+ /**
4382
+ * Report (and, without `force`, guard) the files a re-lock deletes from a deployed copy
4383
+ * (OQ5, p1441): the asset is locked already and its library or source now holds other content
4384
+ * (`next` re-locks it), and the deployed folder holds files the new content does not ship. The
4385
+ * deploy clears them. Two cases:
4386
+ * - the deployed copy still equals what `prevLock` recorded (per-file sha256 for a source
4387
+ * or store asset, the library hash for a library asset): the new version dropped those
4388
+ * files, nobody edited them, so they are deleted and come back as `removed` rows (an
4389
+ * `install`/`update` prints `Removed <file> (not part of <ref>)`), without a refusal;
4390
+ * - the copy diverged (a file the previous content did not ship, an edited file, or no hash
4391
+ * to compare): without `force` this throws one error naming the files and changes nothing.
4392
+ * A source or store asset names only the files the previous content did not ship (or that
4393
+ * were edited); a library asset records no file list, so it names every file to delete.
4394
+ *
4395
+ * @param entries - The catalog entries about to be deployed.
4396
+ * @param next - The lock the run is about to write (or merge).
4397
+ * @param libraryOf - The library each library-origin entry came from.
4398
+ */
4399
+ async relockDeletions(entries, prevLock, next, force, forceCommand, libraryOf) {
4400
+ if (!prevLock) return [];
4401
+ const relocked = relockedRefs(prevLock, next).map((key) => ({
4402
+ key,
4403
+ id: parseCanonicalLockKey(key)
4404
+ }));
4405
+ if (relocked.length === 0) return [];
4406
+ const shown = (p) => (this.global ? p : relative(this.projectDir, p)).split(sep).join("/");
4407
+ let snap;
4408
+ const rows = [];
4409
+ const refused = [];
4410
+ for (const e of entries) {
4411
+ const hit = relocked.find(({ id }) => id.kind === e.kind && id.name === e.name);
4412
+ const component = hit ? this.installTarget(e.kind) : void 0;
4413
+ if (!hit || !component) continue;
4414
+ const prevEntry = Object.entries(prevLock.assets).find(([k]) => {
4415
+ const o = parseCanonicalLockKey(k);
4416
+ return o.kind === hit.id.kind && o.name === hit.id.name && o.publisher === hit.id.publisher;
4417
+ })?.[1];
4418
+ const removed = [];
4419
+ const blocked = [];
4420
+ for (const t of this.targetHarnesses()) {
4421
+ const drift = deployedAssetDrift(e, this.deployOptsFor(t));
4422
+ if (!drift || drift.extra.length === 0) continue;
4423
+ const dir = deployedDir(component, e.name, t, this.global, this.projectDir);
4424
+ const files = drift.extra;
4425
+ let untouched;
4426
+ if (prevEntry && prevEntry.files.length > 0) {
4427
+ const locked = new Map(
4428
+ lockedAssetFiles(e.kind, prevEntry.files).map((f) => [f.rel, f.sha256])
4429
+ );
4430
+ untouched = files.filter((rel) => {
4431
+ const want = locked.get(rel);
4432
+ if (want === void 0) return false;
4433
+ try {
4434
+ return computeHash(join(dir, rel)) === want;
4435
+ } catch {
4436
+ return false;
4437
+ }
4438
+ });
4439
+ } else if (prevEntry?.library?.sha256 && !prevEntry.library.installed) {
4440
+ const libName = libraryOf.get(e);
4441
+ snap ??= await this.loadLibraryIndex();
4442
+ const libPath = libName ? this.libraryPath({ name: libName }, snap) : null;
4443
+ if (libPath) {
4444
+ const assetRoot = statSync(e.source).isDirectory() ? e.source : dirname(e.source);
4445
+ const hash = computeDeterministicHash(
4446
+ dir,
4447
+ listAssetFiles(dir),
4448
+ posix.normalize(relative(libPath, assetRoot).split(sep).join("/"))
4449
+ );
4450
+ if (hash === prevEntry.library.sha256) untouched = [...files];
4451
+ }
4452
+ }
4453
+ const own = new Set(untouched ?? []);
4454
+ for (const f of files) (own.has(f) ? removed : blocked).push(shown(join(dir, f)));
4455
+ }
4456
+ if (removed.length > 0) rows.push({ ref: hit.key, restored: [], removed });
4457
+ if (blocked.length > 0) refused.push({ ref: hit.key, restored: [], removed: blocked });
4458
+ }
4459
+ if (refused.length === 0 || force) return [...rows, ...refused];
4460
+ const lines = refused.map((r) => `${r.ref} (${r.removed.join(", ")})`);
4461
+ throw new Error(
4462
+ `Not re-locked: the deployed copy of ${refused.length === 1 ? "an asset" : "assets"} holds files its locked content did not ship or that were edited, and the deploy would delete them:
4463
+ ${lines.join("\n ")}
4464
+ Nothing was changed. Copy what you need, or run \`${forceCommand}\` to delete them.`
4465
+ );
4466
+ }
4467
+ /**
4468
+ * Stop `install` from overwriting a hand-edited rendered agent file that no lock entry
4469
+ * records (OQ5, p1440): the lock is gone (or never held the agent), so nothing tells an edit
4470
+ * from a stale render, and the deploy would replace the file without a word. Checks the file
4471
+ * named after each agent about to be deployed and the file of each agent instance whose bound
4472
+ * agent has no lock entry. Throws one error naming each file that exists and differs from its
4473
+ * render and changes nothing; `--force` (the caller skips this check) overwrites. A file that
4474
+ * equals its render is not an edit. Project mode only.
4475
+ */
4476
+ assertNoUnrecordedAgentOverwrite(entries, prevLock, forceCommand) {
4477
+ if (this.global) return;
4478
+ const lockedAgents = new Set(
4479
+ Object.keys(prevLock?.assets ?? {}).map(parseCanonicalLockKey).filter((k) => k.kind && this.installTarget(k.kind) === "agent").map((k) => k.name)
4480
+ );
4481
+ const opts = this.deployOptsFor(this.harness);
4482
+ const lines = /* @__PURE__ */ new Set();
4483
+ const collect = (diff, what) => {
4484
+ if (!diff) return;
4485
+ for (const m of diff.matchAll(/^--- (.+?)\t/gm)) {
4486
+ const abs = m[1];
4487
+ if (!existsSync(abs)) continue;
4488
+ lines.add(
4489
+ `${relative(this.projectDir, abs).split(sep).join("/")} (${what} differs from its render)`
4490
+ );
4491
+ }
4492
+ };
4493
+ for (const e of entries) {
4494
+ if (this.installTarget(e.kind) !== "agent" || lockedAgents.has(e.name)) continue;
4495
+ try {
4496
+ collect(diffDeployedAgent(e, opts), `agent ${e.name}`);
4497
+ } catch {
4498
+ }
4499
+ }
4500
+ let instances = {};
4501
+ try {
4502
+ instances = effectiveAgentInstances(resolveSkWorkspaceConfig(this.projectDir));
4503
+ } catch {
4504
+ }
4505
+ for (const [id, config] of Object.entries(instances)) {
4506
+ if (!config.ref.startsWith("agent:")) continue;
4507
+ const bound = parseRefTolerant(config.ref);
4508
+ if (bound?.kind === "agent" && lockedAgents.has(bound.name)) continue;
4509
+ try {
4510
+ const srcDir = resolveAgentDefinitionDir(this.projectDir, config.ref);
4511
+ if (srcDir === void 0) continue;
4512
+ const entry = {
4513
+ kind: "agent",
4514
+ name: id,
4515
+ description: "",
4516
+ source: srcDir,
4517
+ version: "",
4518
+ requires: [],
4519
+ dependencies: []
4520
+ };
4521
+ collect(diffDeployedAgent(entry, opts, { id, srcDir, config }), `agent instance ${id}`);
4522
+ } catch {
4523
+ }
4524
+ }
4525
+ if (lines.size === 0) return;
4526
+ throw new Error(
4527
+ `Not deployed: the deploy would overwrite ${lines.size === 1 ? "a rendered agent file" : "rendered agent files"} that no lock entry records:
4528
+ ${[...lines].join("\n ")}
4529
+ Nothing was changed. Move your edits away, or run \`${forceCommand}\` to overwrite the file.`
4530
+ );
4531
+ }
4532
+ /**
4533
+ * Stop a re-lock from replacing a hand edit of a deployed copy (OQ6, p1641): the asset is locked
4534
+ * already, its library or source now holds other content (`next` re-locks it), and a deployed
4535
+ * file differs from what the lock covers and from the new content. The deploy would replace
4536
+ * the file without a word. Throws one error naming each file and changes nothing; `--force`
4537
+ * (the caller skips this check) replaces them. An agent's rendered files are compared with the
4538
+ * hashes the lock records. A file the new content brings back byte for byte loses nothing.
4539
+ * Project mode only.
4540
+ */
4541
+ async assertNoHandEditsOverwritten(entries, prevLock, next, forceCommand) {
4542
+ if (this.global || !prevLock) return;
4543
+ const relocked = relockedRefs(prevLock, next).map((k) => parseCanonicalLockKey(k));
4544
+ if (relocked.length === 0) return;
4545
+ const opts = this.deployOptsFor(this.harness);
4546
+ const lines = [];
4547
+ for (const d of await this.lockedCopyDrift()) {
4548
+ const id = parseCanonicalLockKey(d.key);
4549
+ if (!relocked.some(
4550
+ (r) => r.kind === id.kind && r.name === id.name && r.publisher === id.publisher
4551
+ )) {
4552
+ continue;
4553
+ }
4554
+ let files = d.modified;
4555
+ if (!d.rendered) {
4556
+ const component = id.kind ? this.installTarget(id.kind) : void 0;
4557
+ if (!id.kind || !id.name || !component) continue;
4558
+ const entry = entries.find((e) => e.kind === id.kind && e.name === id.name);
4559
+ const differs = entry ? deployedAssetDrift(entry, opts)?.modified : void 0;
4560
+ if (differs) files = files.filter((f) => differs.includes(f));
4561
+ const dir = relative(
4562
+ this.projectDir,
4563
+ deployedDir(component, id.name, this.harness, this.global, this.projectDir)
4564
+ ).split(sep).join("/");
4565
+ files = files.map((f) => `${dir}/${f}`);
4566
+ }
4567
+ if (files.length > 0) lines.push(`${d.ref} (${files.join(", ")} was edited)`);
4568
+ }
4569
+ if (lines.length === 0) return;
4570
+ throw new Error(
4571
+ `Not re-locked: the new content would replace ${lines.length === 1 ? "a file" : "files"} you edited in the deployed copy:
4572
+ ${lines.join("\n ")}
4573
+ Nothing was changed. Copy your edits away, or run \`${forceCommand}\` to replace them.`
3867
4574
  );
3868
4575
  }
3869
4576
  /**
@@ -4091,7 +4798,7 @@ Nothing was changed. Declare it again to keep it, or run \`skaile install --forc
4091
4798
  }
4092
4799
  assertLockHashes(lock);
4093
4800
  this.assertNoFolderCollision(Object.keys(lock.assets).map(parseCanonicalLockKey));
4094
- const orphaned = [...(await this.orphanedSourceGaps()).values()];
4801
+ const orphaned = [...(await this.orphanedSourceGaps(void 0, true)).values()];
4095
4802
  if (orphaned.length > 0) {
4096
4803
  throw new Error(
4097
4804
  `Lock file verification failed:
@@ -4166,20 +4873,47 @@ Nothing was changed. Declare it again to keep it, or run \`skaile install --forc
4166
4873
  throw new Error(`Lock file verification failed:
4167
4874
  ${reason}`);
4168
4875
  }
4169
- const library = await this.restoreLibraryLockEntries(lock);
4876
+ const library = await this.restoreLibraryLockEntries(lock, { storeFallback: true });
4170
4877
  assertFlowsLoad([...resolved, ...library.entries], "installed");
4171
4878
  assertNoLibrarySymlinks([...resolved, ...library.entries], this.registry, repositories);
4172
- const copyDrift = await this.lockedCopyDrift();
4173
- const missingCopies = await this.incompleteDeploys();
4174
- const unrecordedAgents = this.unrecordedAgentFiles(lock);
4175
- const deployed = [
4879
+ const inferredHarnesses = this.inferLegacyHarnessSubsets([...resolved, ...library.entries]);
4880
+ const composedPre = /* @__PURE__ */ new Set();
4881
+ if (!this.global) {
4882
+ for (const e of [...resolved, ...library.entries]) {
4883
+ if (this.installTarget(e.kind) !== "agent") continue;
4884
+ try {
4885
+ for (const composeRef of composedDriftRefs(e, this.deployOptsFor(this.harness))) {
4886
+ composedPre.add(this.composedDriftLine(canonicalEntryRef(e), composeRef));
4887
+ }
4888
+ } catch {
4889
+ }
4890
+ }
4891
+ }
4892
+ if (library.missing.length > 0 || composedPre.size > 0) {
4893
+ return {
4894
+ deployed: [],
4895
+ removed: [],
4896
+ missing: [...library.missing, ...composedPre],
4897
+ lockWritten: false,
4898
+ npmInstalled: [],
4899
+ npmFailed: []
4900
+ };
4901
+ }
4902
+ const copyDrift = await this.lockedCopyDrift();
4903
+ const missingCopies = await this.incompleteDeploys();
4904
+ const unrecordedAgents = this.unrecordedAgentFiles(lock);
4905
+ const composedDrift = /* @__PURE__ */ new Set();
4906
+ const deployed = [
4176
4907
  ...new Set(
4177
4908
  this.targetHarnesses().flatMap(
4178
4909
  (t) => deployAll(
4179
4910
  this.entriesForHarness([...resolved, ...library.entries], t),
4180
4911
  repositories,
4181
4912
  patches,
4182
- this.deployOptsFor(t)
4913
+ this.deployOptsFor(t, void 0, void 0, {
4914
+ refresh: false,
4915
+ onDrift: (ref, composeRef) => composedDrift.add(this.composedDriftLine(ref, composeRef))
4916
+ })
4183
4917
  )
4184
4918
  )
4185
4919
  )
@@ -4195,25 +4929,53 @@ Nothing was changed. Declare it again to keep it, or run \`skaile install --forc
4195
4929
  return p ? `${p.kind}:${p.name}` : m;
4196
4930
  })
4197
4931
  );
4932
+ const viaStoreIds = new Set(library.viaStore.map(gapIdentity));
4198
4933
  const gaps = (await this.projectState()).gaps.filter((g) => {
4199
4934
  if (g.type === "agent") return false;
4935
+ if (g.unreadableSource) return false;
4936
+ if ((g.type === "library" || g.type === "unresolved") && viaStoreIds.has(gapIdentity(g.ref))) {
4937
+ return false;
4938
+ }
4200
4939
  const p = parseRefTolerant(g.ref);
4201
4940
  return !(p && alreadyMissing.has(`${p.kind}:${p.name}`));
4202
4941
  }).map((g) => g.message);
4942
+ const libraryNotices = this.lockUrlRestoreNotices(lock, library.viaStore);
4203
4943
  const placeholderBodies = scaffoldBodyRefs(library.entries, deployed);
4204
4944
  const placeholderDescriptions = scaffoldDescriptionRefs(library.entries, deployed);
4205
4945
  return {
4206
4946
  deployed,
4207
4947
  removed: [],
4208
- missing: [...library.missing, ...gaps],
4948
+ missing: [...library.missing, ...gaps, ...composedDrift],
4209
4949
  ...placeholderBodies.length > 0 ? { placeholderBodies } : {},
4210
4950
  ...placeholderDescriptions.length > 0 ? { placeholderDescriptions } : {},
4211
4951
  lockWritten: false,
4952
+ ...inferredHarnesses.length > 0 ? { inferredHarnesses } : {},
4212
4953
  ...restored.length > 0 ? { restored } : {},
4213
4954
  ...rewritten.length > 0 ? { rewritten } : {},
4955
+ ...libraryNotices.length > 0 ? { libraryNotices } : {},
4214
4956
  ...agentRender
4215
4957
  };
4216
4958
  }
4959
+ /**
4960
+ * One notice line per locked library that is not registered here and whose assets
4961
+ * `install --locked` restored from the lock url: names the library, how many assets came from
4962
+ * the url, and how to register the library to move the lock later (OQ8, p1655).
4963
+ */
4964
+ lockUrlRestoreNotices(lock, viaStore) {
4965
+ const byLibrary = /* @__PURE__ */ new Map();
4966
+ for (const key of viaStore) {
4967
+ const origin = lock.assets[key]?.library;
4968
+ if (!origin) continue;
4969
+ const name = unregisteredLibraryName(origin);
4970
+ const row = byLibrary.get(name) ?? { ...origin.url ? { url: origin.url } : {}, refs: [] };
4971
+ row.refs.push(key);
4972
+ byLibrary.set(name, row);
4973
+ }
4974
+ return [...byLibrary].map(([name, { url, refs }]) => {
4975
+ const register = url ? `skaile library init ${name} --git ${url}` : `skaile library init ${name} --path <dir>`;
4976
+ return `Library "${name}" is not registered here: restored ${refs.length} asset${refs.length === 1 ? "" : "s"} from the lock url${url ? ` ${url}` : ""}. Register it with \`${register}\` to move the lock later.`;
4977
+ });
4978
+ }
4217
4979
  /**
4218
4980
  * The versions of `parsed`'s name that a source, a library or a store holds, newest first,
4219
4981
  * for the `Asset not found` of a pinned ref. Empty for an unpinned ref (nothing was ruled
@@ -4236,6 +4998,120 @@ Nothing was changed. Declare it again to keep it, or run \`skaile install --forc
4236
4998
  }
4237
4999
  return [...found].sort((a, b) => gt(coerceVersion(a), coerceVersion(b)) ? -1 : 1);
4238
5000
  }
5001
+ /**
5002
+ * The refs of the other kinds that a source or a library holds under the same publisher and
5003
+ * name as `parsed` (`agent:@pub/captain` for a `@pub/captain` that is no skill), for the
5004
+ * `Did you mean` of an `add` that missed.
5005
+ */
5006
+ async otherKindRefs(parsed) {
5007
+ if (!parsed.publisher) return [];
5008
+ const publisher = parsed.publisher.replace(/^@/, "");
5009
+ const found = [];
5010
+ for (const kind of [...this.knownKinds].sort()) {
5011
+ if (kind === parsed.kind) continue;
5012
+ const candidate = `${kind}:@${publisher}/${parsed.name}`;
5013
+ try {
5014
+ if (await this.info(candidate) !== null) found.push(candidate);
5015
+ } catch {
5016
+ }
5017
+ }
5018
+ return found;
5019
+ }
5020
+ /**
5021
+ * The `Did you mean agent:@pub/name?` sentence for a ref written without a kind (`@pub/name`,
5022
+ * which means `skill`) that names no asset: the refs of other kinds under the same name that
5023
+ * a source, a library, the lock or `skaile.yaml` holds (p1425). `undefined` when the ref has a
5024
+ * kind or nothing of another kind answers to it. `add`, `diff`, `update`, `info`, `remove` and
5025
+ * `why` end their not-found line with it.
5026
+ */
5027
+ async otherKindHint(ref) {
5028
+ if (/^[^@/#\s]+:/.test(ref.trim())) return void 0;
5029
+ const parsed = parseRefTolerant(ref);
5030
+ if (!parsed) return void 0;
5031
+ const wantPublisher = parsed.publisher?.replace(/^@/, "");
5032
+ const found = new Set(await this.otherKindRefs(parsed));
5033
+ const note = (id) => {
5034
+ if (!id.kind || !id.name || id.kind === parsed.kind || id.name !== parsed.name) return;
5035
+ const publisher = id.publisher?.replace(/^@/, "");
5036
+ if (wantPublisher && publisher !== wantPublisher) return;
5037
+ found.add(
5038
+ assetRefToStr({ kind: id.kind, name: id.name, ...publisher ? { publisher } : {} })
5039
+ );
5040
+ };
5041
+ for (const key of Object.keys(this.tryReadLock()?.assets ?? {})) {
5042
+ note(parseCanonicalLockKey(key));
5043
+ }
5044
+ try {
5045
+ for (const dep of resolveSkWorkspaceConfig(this.projectDir).dependencies ?? []) {
5046
+ if (typeof dep === "string") note(parseRefTolerant(dep) ?? {});
5047
+ }
5048
+ } catch {
5049
+ }
5050
+ const refs = [...found].sort();
5051
+ if (refs.length === 0) return void 0;
5052
+ return refs.length === 1 ? `Did you mean ${refs[0]}?` : `Did you mean one of: ${refs.join(", ")}?`;
5053
+ }
5054
+ /**
5055
+ * What `add` would deploy for the resolved candidates and library entries, without touching
5056
+ * the project: one item per asset with its origin, version, deploy folders and files.
5057
+ */
5058
+ planAddItems(resolved, lib, stores, parsed) {
5059
+ const storeUrls = new Set(stores.map((s) => normalizeRepoUrl(s.url)));
5060
+ const factoryUrl = normalizeRepoUrl(FACTORY_SOURCE_URL);
5061
+ const shownPath = (dir) => {
5062
+ const rel = relative(this.projectDir, dir);
5063
+ return this.global || rel.startsWith("..") ? dir.replace(homedir(), "~") : `${rel}${dir.endsWith(sep) ? sep : ""}`;
5064
+ };
5065
+ const folders = (kind, name) => {
5066
+ const component = this.installTarget(kind);
5067
+ if (!component) return [];
5068
+ return this.targetHarnesses().map(
5069
+ (t) => shownPath(deployedDir(component, name, t, this.global, this.projectDir))
5070
+ );
5071
+ };
5072
+ const renders = (kind, name) => {
5073
+ if (this.installTarget(kind) !== "agent") return [];
5074
+ return this.targetHarnesses().flatMap(
5075
+ (t) => agentRenderFiles(
5076
+ t,
5077
+ dirname(deployedDir("agent", name, t, this.global, this.projectDir)),
5078
+ name
5079
+ ).map(shownPath)
5080
+ );
5081
+ };
5082
+ const asked = (kind, name, publisher) => kind === parsed.kind && name === parsed.name && (!parsed.publisher || (publisher ?? "").replace(/^@/, "") === parsed.publisher.replace(/^@/, ""));
5083
+ const items = [];
5084
+ for (const c of resolved) {
5085
+ const url = normalizeRepoUrl(c.sourceUrl);
5086
+ items.push({
5087
+ ref: assetRefToStr({
5088
+ kind: c.kind,
5089
+ name: c.name,
5090
+ publisher: c.publisher.replace(/^@/, "")
5091
+ }),
5092
+ version: c.version,
5093
+ origin: storeUrls.has(url) ? `store ${c.sourceUrl}` : url === factoryUrl ? "factory" : `source ${sourceShortName(c.sourceUrl)}`,
5094
+ requested: asked(c.kind, c.name, c.publisher),
5095
+ ...planPaths(
5096
+ folders(c.kind, c.name),
5097
+ renders(c.kind, c.name),
5098
+ c.files.map((f) => f.path)
5099
+ )
5100
+ });
5101
+ }
5102
+ for (const e of lib.entries) {
5103
+ const id = this.entryIdentity(e);
5104
+ items.push({
5105
+ // The ref is printed once: `canonicalEntryRef` carries the version as a pin, `version` has it.
5106
+ ref: canonicalEntryRef(e).replace(/#.*$/, ""),
5107
+ version: e.version ?? "",
5108
+ origin: `library ${lib.libraryOf.get(e) ?? ""}`.trim(),
5109
+ requested: asked(e.kind, e.name, id.publisher),
5110
+ ...planPaths(folders(e.kind, e.name), renders(e.kind, e.name), [...e.files ?? []])
5111
+ });
5112
+ }
5113
+ return items.sort((a, b) => Number(b.requested) - Number(a.requested));
5114
+ }
4239
5115
  /**
4240
5116
  * Parse the ref `add` was given. A ref the grammar rejects fails with the parser's message
4241
5117
  * minus its manifest-migration advice (it points at a skill that is not for `add`), and:
@@ -4253,7 +5129,7 @@ Nothing was changed. Declare it again to keep it, or run \`skaile install --forc
4253
5129
  const plain = err.message.endsWith(advice) ? err.message.slice(0, -advice.length) : err.message;
4254
5130
  if (plain.startsWith("publisher required")) {
4255
5131
  const matches = await this.resolvableMatches(ref, repositories, sources);
4256
- const hint = matches.length === 0 ? "" : matches.length === 1 ? ` Did you mean ${matches[0]}?` : ` Did you mean one of: ${matches.join(", ")}?`;
5132
+ const hint = matches.length === 0 ? ` Find the publisher with \`skaile store search ${ref.trim().replace(/^[^:@/#\s]+:/, "").replace(/#.*$/, "")}\`.` : matches.length === 1 ? ` Did you mean ${matches[0]}?` : ` Did you mean one of: ${matches.join(", ")}?`;
4257
5133
  throw new Error(`${plain}${hint}`);
4258
5134
  }
4259
5135
  const pinned = /^(.*\/[^@#/]+)@([^@#/]+)$/.exec(ref.trim());
@@ -4334,6 +5210,7 @@ Nothing was changed. Declare it again to keep it, or run \`skaile install --forc
4334
5210
  stores,
4335
5211
  declaredRefs: dependencies,
4336
5212
  warnCollisions: true,
5213
+ lockedOrigin: this.lockedOriginVersions(),
4337
5214
  readStoreRequires: true,
4338
5215
  ...opts?.preferLockedLibrary ? { preferLocked: await this.lockedPreference() } : {}
4339
5216
  });
@@ -4370,6 +5247,25 @@ Nothing was changed. Declare it again to keep it, or run \`skaile install --forc
4370
5247
  );
4371
5248
  }
4372
5249
  }
5250
+ if (renamedEntryLookup) {
5251
+ for (const l of snap?.libraries ?? []) {
5252
+ const libPath = this.libraryPath({ name: l.name }, snap);
5253
+ if (!libPath) continue;
5254
+ const renamed = renamedEntryLookup(libPath).find(
5255
+ (r) => r.kind === parsed.kind && r.name === parsed.name && (!parsed.publisher || bare(r.publisher) === bare(parsed.publisher))
5256
+ );
5257
+ if (renamed) {
5258
+ const shown = renamed.publisher ? assetRefToStr({
5259
+ kind: renamed.kind,
5260
+ name: renamed.name,
5261
+ publisher: bare(renamed.publisher)
5262
+ }) : `${renamed.kind}:${renamed.name}`;
5263
+ throw new Error(
5264
+ `${shown}: ${renamed.file} now names "${renamed.now}", so the library entry is a ref \`add\` cannot resolve. Run \`skaile library index ${l.name}\` to rename the entry, or restore the name.`
5265
+ );
5266
+ }
5267
+ }
5268
+ }
4373
5269
  const unreachable = [...this.storeFailures.values()];
4374
5270
  if (unreachable.length > 0) {
4375
5271
  throw new Error(
@@ -4377,8 +5273,13 @@ Nothing was changed. Declare it again to keep it, or run \`skaile install --forc
4377
5273
  );
4378
5274
  }
4379
5275
  const available = await this.availableVersions(parsed, stores);
5276
+ const otherKind = await this.otherKindHint(ref);
5277
+ const others = missing.filter((m) => {
5278
+ const id = parseRefTolerant(m);
5279
+ return !(id && id.kind === parsed.kind && id.name === parsed.name && (id.publisher ?? "") === (parsed.publisher ?? ""));
5280
+ });
4380
5281
  throw new Error(
4381
- `Asset not found: ${ref}${(await this.publisherChangeHints([ref]))[ref] ?? ""}${missing.length ? ` (missing: ${missing.join(", ")})` : ""}${available.length ? `; ${parsed.name} has version(s): ${available.join(", ")}` : ""}`
5282
+ `Asset not found: ${ref}${(await this.publisherChangeHints([ref]))[ref] ?? ""}${others.length ? ` (missing: ${others.join(", ")})` : ""}${available.length ? `; ${parsed.name} has version(s): ${available.join(", ")}` : ""}` + (otherKind ? `. ${otherKind}` : "")
4382
5283
  );
4383
5284
  }
4384
5285
  if (parsed.kind === "preset" && missing.length > 0) {
@@ -4401,6 +5302,62 @@ Nothing was changed. Declare it again to keep it, or run \`skaile install --forc
4401
5302
  )
4402
5303
  );
4403
5304
  }
5305
+ const problems = origin && libPath && !(e.kind === "flow" && flowLoadError(e) !== void 0) ? libraryAssetCheck?.({ kind: e.kind, name: e.name, source: e.source }, libPath) ?? [] : [];
5306
+ if (problems.length > 0) {
5307
+ throw new Error(
5308
+ `${canonicalEntryRef(e)} in library "${origin}" fails the checks of \`skaile validate\` - nothing was added:
5309
+ ${problems.map((l) => ` ${l}`).join("\n")}
5310
+ Fix the asset, then run \`skaile library index ${origin}\` and add it again.`
5311
+ );
5312
+ }
5313
+ }
5314
+ }
5315
+ if (libraryAssetCheck) {
5316
+ for (const c of resolved) {
5317
+ const decl = Object.entries(repositories).find(([, d]) => d.url === c.sourceUrl);
5318
+ const cloneDir = decl ? this.resolveRepoDir(decl[1], decl[0]) : null;
5319
+ const entry = cloneDir ? this.candidateToCatalogEntry(c, repositories) : null;
5320
+ if (!cloneDir || !entry) continue;
5321
+ if (entry.kind === "flow" && flowLoadError(entry) !== void 0) continue;
5322
+ const problems = libraryAssetCheck(
5323
+ { kind: entry.kind, name: entry.name, source: entry.source },
5324
+ cloneDir
5325
+ );
5326
+ if (problems.length > 0) {
5327
+ throw new Error(
5328
+ `${canonicalEntryRef(entry)} from source ${c.sourceUrl} fails the checks of \`skaile validate\` - nothing was added:
5329
+ ${problems.map((l) => ` ${l}`).join("\n")}
5330
+ Fix the asset in the source repo, then run \`skaile source sync\` and add it again.`
5331
+ );
5332
+ }
5333
+ }
5334
+ }
5335
+ const staleSiblings = this.staleSourceSiblingRefs(resolved, dependencies);
5336
+ if (staleSiblings.length > 0) {
5337
+ const siblings = await this.resolveLibraryFirst(staleSiblings, {
5338
+ provenanceIndex,
5339
+ overrides,
5340
+ stores,
5341
+ declaredRefs: dependencies,
5342
+ lockedOrigin: this.lockedOriginVersions(),
5343
+ readStoreRequires: true
5344
+ });
5345
+ for (const sib of staleSiblings) {
5346
+ const pin = parseAssetRefLoose(sib)?.pin;
5347
+ if (!pin) continue;
5348
+ const id = refIdentity(sib.replace(/#.*$/, ""));
5349
+ const kept = siblings.resolved.some(
5350
+ (c) => `${c.publisher.replace(/^@/, "")}/${c.kind}:${c.name}` === id?.replace(/^@/, "") && matchPin(pin, c.version ?? "")
5351
+ );
5352
+ if (!kept) {
5353
+ throw new Error(
5354
+ `${sib.replace(/#.*$/, "")} is pinned to ${pin} in skaile.yaml, but its source moved on and no longer holds ${pin}, so adding ${ref} would break the lock - nothing was added. Change the pin in skaile.yaml, or drop the pinned asset, then add again.`
5355
+ );
5356
+ }
5357
+ }
5358
+ const held = new Set(resolved.map((c) => this.candidateKey(c)));
5359
+ for (const c of siblings.resolved) {
5360
+ if (!held.has(this.candidateKey(c))) resolved.push(c);
4404
5361
  }
4405
5362
  }
4406
5363
  this.assertSourceCommitsPublished(resolved, repositories);
@@ -4437,9 +5394,22 @@ Nothing was changed. Declare it again to keep it, or run \`skaile install --forc
4437
5394
  `${ref.split("#")[0]} requires ${missing.join(", ")} (not found \u2014 ${REGISTER_HOLDER}, or remove the requirement from the asset's manifest) \u2014 nothing was added`
4438
5395
  );
4439
5396
  }
5397
+ if (opts?.dryRun) {
5398
+ const plan = this.planAddItems(resolved, lib, stores, parsed);
5399
+ opts.onPlan?.(plan);
5400
+ return [];
5401
+ }
4440
5402
  const entries = [...await this.buildCatalogEntries(resolved, repositories), ...lib.entries];
4441
5403
  assertFlowsLoad(entries, "added");
4442
5404
  assertNoLibrarySymlinks(entries, this.registry, repositories, opts?.link === true);
5405
+ if (!opts?.force && !this.global && (resolved.length > 0 || lib.entries.length > 0)) {
5406
+ await this.assertNoHandEditsOverwritten(
5407
+ entries,
5408
+ this.tryReadLock(),
5409
+ buildLockFile(resolved, overridesApplied, await this.libraryLockEntries(lib)),
5410
+ opts?.preferLockedLibrary ? "skaile update --force" : `skaile add ${ref} --force`
5411
+ );
5412
+ }
4443
5413
  if (parsed.kind === "mcp-server" && !this.global) {
4444
5414
  const decls = await loadMcpServerDeclarations(this.projectDir, { alsoDeclared: [ref] });
4445
5415
  const mine = decls.find(
@@ -4452,14 +5422,21 @@ Nothing was changed. Declare it again to keep it, or run \`skaile install --forc
4452
5422
  );
4453
5423
  }
4454
5424
  }
5425
+ const harnessNamed = this.global && (this.harnessesOverride?.length ?? 0) > 0;
5426
+ if (harnessNamed && opts?.only && opts.onDroppedCopy)
5427
+ this.reportDroppedCopies(parsed, opts.onDroppedCopy);
4455
5428
  const deployed = [
4456
5429
  ...new Set(
4457
5430
  this.targetHarnesses().flatMap(
4458
5431
  (t) => deployAll(
4459
- this.entriesForHarness(entries, t),
5432
+ harnessNamed ? entries : this.entriesForHarness(entries, t),
4460
5433
  repositories,
4461
5434
  patches,
4462
- this.deployOptsFor(t, opts?.link, opts?.onSecretFile)
5435
+ this.deployOptsFor(t, opts?.link, opts?.onSecretFile, {
5436
+ refresh: true,
5437
+ onDrift: () => {
5438
+ }
5439
+ })
4463
5440
  )
4464
5441
  )
4465
5442
  )
@@ -4479,7 +5456,8 @@ Nothing was changed. Declare it again to keep it, or run \`skaile install --forc
4479
5456
  for (const placeholder of scaffoldDescriptionRefs(lib.entries, deployed)) {
4480
5457
  opts?.onPlaceholderDescription?.(placeholder);
4481
5458
  }
4482
- this.addDependencyToConfig(ref);
5459
+ this.addDependencyToConfig(ref, opts?.only === true);
5460
+ this.widenDependencyRows(entries, parsed);
4483
5461
  await this.renderHarnessMcpConfig();
4484
5462
  await this.renderAgentInstances();
4485
5463
  if (resolved.length > 0 || lib.entries.length > 0) {
@@ -4494,7 +5472,9 @@ Nothing was changed. Declare it again to keep it, or run \`skaile install --forc
4494
5472
  }
4495
5473
  writeLock(
4496
5474
  this.lockPath,
4497
- this.withInstanceRenderings(mergeLockFile(prevLock, this.withRenderedAgents(fresh)))
5475
+ this.withInstanceRenderings(
5476
+ mergeLockFile(prevLock, this.withCopiedFiles(this.withRenderedAgents(fresh), prevLock))
5477
+ )
4498
5478
  );
4499
5479
  }
4500
5480
  for (const d of deployed) {
@@ -4816,10 +5796,18 @@ Nothing was changed. Declare it again to keep it, or run \`skaile install --forc
4816
5796
  }
4817
5797
  const kept = [];
4818
5798
  const ownedBy = [];
5799
+ const mcpPublishers = target.kind === "mcp-server" && !this.global ? [
5800
+ .../* @__PURE__ */ new Set([
5801
+ ...this.lockHolders("mcp-server", target.name).map((h) => h.publisher),
5802
+ target.publisher?.replace(/^@/, "")
5803
+ ])
5804
+ ].filter((p) => !!p) : [];
4819
5805
  const changed = this.removeResolved(target, { force: opts?.force, kept, ownedBy });
5806
+ const droppedSettings = changed && mcpPublishers.length > 0 ? this.dropRemovedMcpServerSettings(target.name, mcpPublishers) : [];
4820
5807
  const keptNote = {
4821
5808
  ...kept.length > 0 ? { kept } : {},
4822
- ...ownedBy.length > 0 ? { ownedBy } : {}
5809
+ ...ownedBy.length > 0 ? { ownedBy } : {},
5810
+ ...droppedSettings.length > 0 ? { droppedSettings } : {}
4823
5811
  };
4824
5812
  const pruned = changed && requiredRefs.length > 0 ? await this.pruneOrphans(requiredRefs) : [];
4825
5813
  if (changed && (target.kind === "mcp-server" || pruned.some((r) => r.startsWith("mcp-server:")))) {
@@ -4876,6 +5864,25 @@ Nothing was changed. Declare it again to keep it, or run \`skaile install --forc
4876
5864
  return kind ? { kind, name } : null;
4877
5865
  }
4878
5866
  }
5867
+ /**
5868
+ * The refs among `refs` (`skill:@publisher/name`, `skill:name`, with or without `#version`)
5869
+ * that are not deployed for this manager's harnesses and scope, judged as `check` judges a
5870
+ * requirement. A ref that does not parse or names a kind this build does not install is
5871
+ * never reported. `run <flow> --dry-run` names the node skills a flow still lacks (p1757).
5872
+ */
5873
+ undeployedSkills(refs) {
5874
+ const out = [];
5875
+ for (const ref of refs) {
5876
+ const parsed = parseRefTolerant(ref);
5877
+ if (!parsed) continue;
5878
+ const component = this.installTarget(parsed.kind);
5879
+ if (!component) continue;
5880
+ if (!isDeployed(component, parsed.name, this.harness, this.global, this.projectDir)) {
5881
+ out.push(ref);
5882
+ }
5883
+ }
5884
+ return out;
5885
+ }
4879
5886
  /**
4880
5887
  * The reverse guard of {@link removeWithDependencies}: which other locked assets (and
4881
5888
  * declared presets) require `target`. Their requirements are read the way {@link lockedGraph}
@@ -4987,6 +5994,40 @@ Nothing was changed. Declare it again to keep it, or run \`skaile install --forc
4987
5994
  }
4988
5995
  return changed;
4989
5996
  }
5997
+ /**
5998
+ * After an `mcp-server` asset left the project: drop what would keep it rendered. Its staged
5999
+ * store bytes under `.skaile/cache/store/<publisher>/mcp-server/<name>/` go (an `mcp_servers:`
6000
+ * entry of the same id would otherwise keep loading them), and so does an `mcp_servers:` entry
6001
+ * that only sets values for it: `env` and the like, with no `command`, `url` or recipe of its
6002
+ * own. An entry that can run alone is the user's own server and stays.
6003
+ *
6004
+ * @returns The dropped `mcp_servers` entries, as `mcp_servers entry "<id>"`.
6005
+ */
6006
+ dropRemovedMcpServerSettings(name, publishers) {
6007
+ for (const publisher of publishers) {
6008
+ rmSync(join(this.storeCacheDir, publisher, "mcp-server", name), {
6009
+ recursive: true,
6010
+ force: true
6011
+ });
6012
+ }
6013
+ const configPath = join(this.stateRoot, "skaile.yaml");
6014
+ if (!existsSync(configPath)) return [];
6015
+ const raw = parse(readFileSync(configPath, "utf8")) ?? {};
6016
+ if (!Array.isArray(raw.mcp_servers)) return [];
6017
+ const dropped = [];
6018
+ const standalone = ["command", "url", "recipe", "launcher"];
6019
+ raw.mcp_servers = raw.mcp_servers.filter((entry) => {
6020
+ const e = entry;
6021
+ if (!e || typeof e.id !== "string" || unscopedName(e.id) !== name) return true;
6022
+ if (standalone.some((k) => e[k] !== void 0 && e[k] !== null)) return true;
6023
+ dropped.push(`mcp_servers entry "${e.id}"`);
6024
+ return false;
6025
+ });
6026
+ if (dropped.length === 0) return [];
6027
+ if (raw.mcp_servers.length === 0) delete raw.mcp_servers;
6028
+ writeFileSync(configPath, stringify(raw, { lineWidth: 120 }));
6029
+ return dropped;
6030
+ }
4990
6031
  /**
4991
6032
  * After `remove --global --harness X`: the harnesses outside `X` that still hold a copy of the
4992
6033
  * asset, or `undefined` when none does (or this is not a harness-limited global remove).
@@ -5158,7 +6199,11 @@ Nothing was changed. Declare it again to keep it, or run \`skaile install --forc
5158
6199
  }
5159
6200
  }
5160
6201
  if (best) return best;
5161
- const lib = await this.resolveLibraryEntries([ref], { quiet: true });
6202
+ const lockedLibraries = await this.lockedLibraryNames();
6203
+ const lib = await this.resolveLibraryEntries([ref], {
6204
+ quiet: true,
6205
+ ...lockedLibraries.size > 0 ? { lockedLibraries } : {}
6206
+ });
5162
6207
  return lib.entries[0] ?? null;
5163
6208
  }
5164
6209
  /**
@@ -5211,18 +6256,18 @@ Nothing was changed. Declare it again to keep it, or run \`skaile install --forc
5211
6256
  }
5212
6257
  /**
5213
6258
  * Tracked assets (`skaile.lock.yaml` or declared in `skaile.yaml`) whose copy
5214
- * still sits in the project's `.claude/…` dirs although this manager deploys
5215
- * to another harness. Before the default followed the configured harness every
5216
- * project deployed to `.claude/`, so an omp/codex project upgraded from then
5217
- * holds orphans there that `remove` / `clean` / `list` no longer look at.
6259
+ * still sits in the project dirs of another harness (`.claude/…`, `.omp/…`,
6260
+ * `.codex/…`) although this manager deploys to {@link AssetManager.harness}.
6261
+ * A project that moved with `skaile harness set` holds orphans there that
6262
+ * `remove` / `clean` / `list` no longer look at.
5218
6263
  *
5219
- * Read-only: nothing is deleted. Empty for `claude-code`, for global mode, and
5220
- * for kinds whose dir is shared with the active target (flows).
6264
+ * Read-only: nothing is deleted. Empty for global mode and for kinds whose
6265
+ * dir is shared with the active target (flows).
5221
6266
  *
5222
- * @returns One {@link LegacyDeployedAsset} per stale copy, sorted by kind then name.
6267
+ * @returns One {@link StaleHarnessCopy} per stale copy, sorted by harness, kind, then name.
5223
6268
  */
5224
- listLegacyClaudeDeployed() {
5225
- if (this.global || this.harness === "claude-code") return [];
6269
+ listStaleHarnessCopies() {
6270
+ if (this.global) return [];
5226
6271
  const tracked = /* @__PURE__ */ new Map();
5227
6272
  const track = (kind, name) => {
5228
6273
  const component = kind ? this.installTarget(kind) : void 0;
@@ -5248,20 +6293,34 @@ Nothing was changed. Declare it again to keep it, or run \`skaile install --forc
5248
6293
  } catch {
5249
6294
  }
5250
6295
  const out = [];
5251
- for (const { kind, name, component: k } of tracked.values()) {
5252
- const legacy = deployedDir(k, name, "claude-code", false, this.projectDir);
5253
- if (legacy === deployedDir(k, name, this.harness, false, this.projectDir)) continue;
5254
- const found = deployedPaths(k, name, "claude-code", false, this.projectDir).find((p) => {
5255
- try {
5256
- lstatSync(p);
5257
- return true;
5258
- } catch {
5259
- return false;
6296
+ for (const other of SUPPORTED_HARNESSES) {
6297
+ if (other === this.harness) continue;
6298
+ for (const { kind, name, component: k } of tracked.values()) {
6299
+ if (deployedDir(k, name, other, false, this.projectDir) === deployedDir(k, name, this.harness, false, this.projectDir)) {
6300
+ continue;
5260
6301
  }
5261
- });
5262
- if (found) out.push({ kind, name, path: found });
6302
+ const found = deployedPaths(k, name, other, false, this.projectDir).find((p) => {
6303
+ try {
6304
+ lstatSync(p);
6305
+ return true;
6306
+ } catch {
6307
+ return false;
6308
+ }
6309
+ });
6310
+ if (found) out.push({ harness: other, kind, name, path: found });
6311
+ }
5263
6312
  }
5264
- return out.sort((a, b) => a.kind.localeCompare(b.kind) || a.name.localeCompare(b.name));
6313
+ return out.sort(
6314
+ (a, b) => a.harness.localeCompare(b.harness) || a.kind.localeCompare(b.kind) || a.name.localeCompare(b.name)
6315
+ );
6316
+ }
6317
+ /**
6318
+ * @deprecated Use {@link AssetManager.listStaleHarnessCopies}, which covers every
6319
+ * other harness folder. This returns only its `.claude/` rows (the old default
6320
+ * deploy dir), in the old shape.
6321
+ */
6322
+ listLegacyClaudeDeployed() {
6323
+ return this.listStaleHarnessCopies().filter((c) => c.harness === "claude-code").map(({ kind, name, path: path2 }) => ({ kind, name, path: path2 }));
5265
6324
  }
5266
6325
  /**
5267
6326
  * List the **declared** top-level dependencies from `skaile.yaml`, each tagged
@@ -5325,7 +6384,11 @@ Nothing was changed. Declare it again to keep it, or run \`skaile install --forc
5325
6384
  state = "declared";
5326
6385
  } else if (parsed.kind === "mcp-server") {
5327
6386
  const wired = await wiredMcpNames();
5328
- const isWired = wired.has(parsed.name) || wired.has(unscopedName(parsed.name));
6387
+ const isWired = wired.has(parsed.name) || wired.has(unscopedName(parsed.name)) || !this.global && await mcpCommittedRenderIntact(
6388
+ this.projectDir,
6389
+ this.harness,
6390
+ unscopedName(parsed.name)
6391
+ );
5329
6392
  const skippedReason = Object.entries(
5330
6393
  await readSkippedMcpServers(this.projectDir, this.harness)
5331
6394
  ).find(([id]) => id === parsed.name || unscopedName(id) === parsed.name)?.[1];
@@ -5670,11 +6733,12 @@ Nothing was changed. Declare it again to keep it, or run \`skaile install --forc
5670
6733
  if (!lock) return [];
5671
6734
  const bare = (s) => s.includes("#") ? s.slice(0, s.indexOf("#")) : s;
5672
6735
  const asked = parseRefTolerant(ref);
6736
+ const askedPublisher = asked?.publisher?.replace(/^@/, "");
5673
6737
  const matchesIdentity = (k) => {
5674
6738
  if (bare(k) === bare(ref)) return true;
5675
- if (!asked || asked.publisher) return false;
6739
+ if (!asked) return false;
5676
6740
  const locked = parseCanonicalLockKey(k);
5677
- return locked.kind === asked.kind && locked.name === asked.name;
6741
+ return locked.kind === asked.kind && locked.name === asked.name && (!askedPublisher || locked.publisher === askedPublisher);
5678
6742
  };
5679
6743
  const key = lock.assets[ref] ? ref : Object.keys(lock.assets).find(matchesIdentity);
5680
6744
  if (!key) return asked?.kind === "preset" ? this.whyPreset(lock, asked) : [];
@@ -5810,6 +6874,7 @@ Nothing was changed. Declare it again to keep it, or run \`skaile install --forc
5810
6874
  const libraryAssets = Object.entries(lock.assets).filter(
5811
6875
  ([, e]) => e.library && !e.library.installed
5812
6876
  );
6877
+ const declaredPins = this.declaredPinsByIdentity();
5813
6878
  if (libraryAssets.length > 0) {
5814
6879
  const snap = await this.loadLibraryIndex();
5815
6880
  const diskHashes = /* @__PURE__ */ new Map();
@@ -5835,6 +6900,8 @@ Nothing was changed. Declare it again to keep it, or run \`skaile install --forc
5835
6900
  { kind, publisher, name },
5836
6901
  found.version
5837
6902
  );
6903
+ const declaredPin = declaredPins.get(`${kind}:${publisher}/${name}`);
6904
+ if (declaredPin && diskVersion && !matchPin(declaredPin, diskVersion)) continue;
5838
6905
  if (diskVersion && !isPseudoVersion(diskVersion) && gt(coerceVersion(diskVersion), coerceVersion(origin.version))) {
5839
6906
  result.push({
5840
6907
  kind,
@@ -5873,6 +6940,26 @@ Nothing was changed. Declare it again to keep it, or run \`skaile install --forc
5873
6940
  library: lookup.name,
5874
6941
  contentChanged: true
5875
6942
  });
6943
+ continue;
6944
+ }
6945
+ }
6946
+ if (entry.composed && kind === "agent") {
6947
+ const srcDir = statSync(found.source).isDirectory() ? found.source : dirname(found.source);
6948
+ const now = new Map(
6949
+ sourceComposedFiles(srcDir, name, this.projectDir).map((f) => [f.path, f.sha256])
6950
+ );
6951
+ const locked = new Map(entry.composed.map((f) => [f.path, f.sha256]));
6952
+ const same = now.size === locked.size && [...now].every(([f, h]) => locked.get(f) === h);
6953
+ if (!same) {
6954
+ result.push({
6955
+ kind,
6956
+ name,
6957
+ publisher,
6958
+ currentVersion: origin.version,
6959
+ behind: 0,
6960
+ library: lookup.name,
6961
+ contentChanged: true
6962
+ });
5876
6963
  }
5877
6964
  }
5878
6965
  }
@@ -5882,6 +6969,23 @@ Nothing was changed. Declare it again to keep it, or run \`skaile install --forc
5882
6969
  await this.addPinnedRows(lock, result);
5883
6970
  return this.addLockGapRows(result);
5884
6971
  }
6972
+ /**
6973
+ * The `#pin` of every publisher-qualified `skaile.yaml` dependency, keyed `kind:publisher/name`
6974
+ * (no scope sigil). `update` honours it, so `outdated` must not list a move it keeps.
6975
+ */
6976
+ declaredPinsByIdentity() {
6977
+ const pins = /* @__PURE__ */ new Map();
6978
+ for (const dep of this.loadConfig().dependencies) {
6979
+ try {
6980
+ const p = parseAssetRef(dep);
6981
+ if (p.pin && p.publisher) {
6982
+ pins.set(`${p.kind}:${p.publisher.replace(/^@/, "")}/${p.name}`, p.pin);
6983
+ }
6984
+ } catch {
6985
+ }
6986
+ }
6987
+ return pins;
6988
+ }
5885
6989
  /**
5886
6990
  * List a locked asset whose `skaile.yaml` ref carries a `#version` pin while a source, a
5887
6991
  * library or a store holds a newer one: `locked 0.7.0 -> 0.9.0 (pinned)`. The pin is the
@@ -5986,20 +7090,20 @@ Nothing was changed. Declare it again to keep it, or run \`skaile install --forc
5986
7090
  async addLibraryMoves(libraryAssets, rows) {
5987
7091
  const refs = [];
5988
7092
  const lockedIn = /* @__PURE__ */ new Map();
7093
+ const declaredPins = this.declaredPinsByIdentity();
5989
7094
  for (const [key, entry] of libraryAssets) {
5990
7095
  const { publisher, kind, name } = parseCanonicalLockKey(key);
5991
7096
  if (!publisher || !kind || !name || !entry.library) continue;
5992
7097
  const ref = `${kind}:@${publisher}/${name}`;
5993
- refs.push(ref);
7098
+ const pin = declaredPins.get(`${kind}:${publisher}/${name}`);
7099
+ refs.push(pin ? `${ref}#${pin}` : ref);
5994
7100
  lockedIn.set(ref, { name: entry.library.name, key });
5995
7101
  }
5996
7102
  if (refs.length === 0) return;
5997
7103
  let res;
7104
+ const preferLocked = await this.lockedPreference();
5998
7105
  try {
5999
- res = await this.resolveLibraryEntries(refs, {
6000
- quiet: true,
6001
- preferLocked: await this.lockedPreference()
6002
- });
7106
+ res = await this.resolveLibraryEntries(refs, { quiet: true, preferLocked });
6003
7107
  } catch {
6004
7108
  return;
6005
7109
  }
@@ -6009,9 +7113,11 @@ Nothing was changed. Declare it again to keep it, or run \`skaile install --forc
6009
7113
  const ref = `${e.kind}:@${publisher}/${e.name}`;
6010
7114
  const from = lockedIn.get(ref);
6011
7115
  if (!to || !from || from.name === to) continue;
7116
+ const renamedOnly = preferLocked.libraries.get(`${e.kind}:${publisher}/${e.name}`) === to;
6012
7117
  const row = rows.find(
6013
7118
  (r) => r.kind === e.kind && r.name === e.name && r.publisher.replace(/^@/, "") === publisher
6014
7119
  );
7120
+ if (renamedOnly && !row) continue;
6015
7121
  if (row) {
6016
7122
  row.library = from.name;
6017
7123
  row.movesTo = to;
@@ -6143,7 +7249,7 @@ Nothing was changed. Declare it again to keep it, or run \`skaile install --forc
6143
7249
  (r) => unscopedName(r.drift.id) === unscopedName(parsed.name)
6144
7250
  );
6145
7251
  if (!row) return { status: "identical" };
6146
- const fileSetDiffers = row.drift.extra.length > 0 || row.drift.missing.some((f) => !f.includes(" entry "));
7252
+ const fileSetDiffers = row.drift.extra.length > 0 || row.drift.missing.length > 0;
6147
7253
  return {
6148
7254
  status: "differs",
6149
7255
  diff: mcpDriftDiff(row.drift),
@@ -6151,8 +7257,10 @@ Nothing was changed. Declare it again to keep it, or run \`skaile install --forc
6151
7257
  };
6152
7258
  }
6153
7259
  }
6154
- const entry = await this.lockedLibraryEntry(parsed) ?? this.lockedSourceEntry(parsed) ?? await this.info(ref);
7260
+ const libraryEntry = await this.lockedLibraryEntry(parsed);
7261
+ const entry = libraryEntry ?? this.lockedSourceEntry(parsed) ?? await this.info(ref);
6155
7262
  if (!entry || !existsSync(entry.source)) return { status: "not-found" };
7263
+ const toLabel = libraryEntry ? "library" : "source";
6156
7264
  const wantPublisher = parsed.publisher?.replace(/^@/, "");
6157
7265
  if (wantPublisher) {
6158
7266
  const lockedHere = Object.keys(this.tryReadLock()?.assets ?? {}).map(parseCanonicalLockKey).filter((id) => id.kind === parsed.kind && id.name === parsed.name);
@@ -6170,7 +7278,11 @@ Nothing was changed. Declare it again to keep it, or run \`skaile install --forc
6170
7278
  parts.push(diffDeployedAgent(entry, opts, inst));
6171
7279
  }
6172
7280
  if (parts.every((d) => d === null)) return { status: "not-deployed" };
6173
- const diff = parts.filter((d) => d !== null).join("");
7281
+ const diff = labelDiffHeaders(
7282
+ parts.filter((d) => d !== null).join(""),
7283
+ "deployed",
7284
+ "rendered"
7285
+ );
6174
7286
  return diff === "" ? { status: "identical" } : { status: "differs", diff };
6175
7287
  }
6176
7288
  const deployedPath = join(
@@ -6191,7 +7303,7 @@ Nothing was changed. Declare it again to keep it, or run \`skaile install --forc
6191
7303
  );
6192
7304
  }
6193
7305
  }
6194
- return diffs.length > 0 ? { status: "differs", diff: diffs.join("") } : { status: "identical" };
7306
+ return diffs.length > 0 ? { status: "differs", diff: labelDiffHeaders(diffs.join(""), "deployed", toLabel) } : { status: "identical" };
6195
7307
  }
6196
7308
  /**
6197
7309
  * The agent instances of `skaile.yaml` whose `ref` names `want` (an `agent:` ref, matched on
@@ -6241,7 +7353,8 @@ Nothing was changed. Declare it again to keep it, or run \`skaile install --forc
6241
7353
  provenanceIndex,
6242
7354
  overrides,
6243
7355
  stores,
6244
- declaredRefs: dependencies
7356
+ declaredRefs: dependencies,
7357
+ lockedOrigin: this.lockedOriginVersions()
6245
7358
  });
6246
7359
  this.assertSourceCommitsPublished(resolved, repositories);
6247
7360
  const libraryLock = await this.libraryLockEntries(lib);
@@ -6250,6 +7363,12 @@ Nothing was changed. Declare it again to keep it, or run \`skaile install --forc
6250
7363
  for (const [key, entry] of Object.entries(lockData.assets)) {
6251
7364
  const rendered = prior?.assets[key]?.rendered;
6252
7365
  if (rendered) entry.rendered = rendered;
7366
+ const copied = prior?.assets[key]?.copied;
7367
+ if (copied && prior?.assets[key]?.library?.sha256 === entry.library?.sha256) {
7368
+ entry.copied = copied;
7369
+ }
7370
+ const composed = prior?.assets[key]?.composed;
7371
+ if (composed) entry.composed = composed;
6253
7372
  }
6254
7373
  writeLock(this.lockPath, lockData);
6255
7374
  return lockData;
@@ -6363,15 +7482,30 @@ Nothing was changed. Declare it again to keep it, or run \`skaile install --forc
6363
7482
  const { kind, name, publisher } = parseCanonicalLockKey(key);
6364
7483
  if (kind && name && publisher) lockedPublisher[`${kind}:${name}`] = publisher;
6365
7484
  }
7485
+ let librarySnap;
6366
7486
  for (const entry of deployed) {
6367
7487
  const publisher = lockedPublisher[`${entry.kind}:${entry.name}`];
6368
7488
  const full = await this.info(
6369
7489
  publisher ? `${entry.kind}:@${publisher}/${entry.name}` : `${entry.kind}:${entry.name}`
6370
7490
  );
6371
- const restorable = !full && publisher !== void 0 && Object.entries(lock?.assets ?? {}).some(([key, e]) => {
6372
- const id = parseCanonicalLockKey(key);
6373
- return e.source !== void 0 && id.kind === entry.kind && id.name === entry.name && id.publisher === publisher;
6374
- });
7491
+ let restorable = false;
7492
+ if (!full && publisher !== void 0) {
7493
+ for (const [key, e] of Object.entries(lock?.assets ?? {})) {
7494
+ const id = parseCanonicalLockKey(key);
7495
+ if (id.kind !== entry.kind || id.name !== entry.name || id.publisher !== publisher) {
7496
+ continue;
7497
+ }
7498
+ if (e.source !== void 0) {
7499
+ restorable = true;
7500
+ break;
7501
+ }
7502
+ librarySnap ??= { snap: await this.loadLibraryIndex() };
7503
+ if (await this.restoredFromLockUrl(key, e, librarySnap.snap)) {
7504
+ restorable = true;
7505
+ break;
7506
+ }
7507
+ }
7508
+ }
6375
7509
  if (!full && !restorable) {
6376
7510
  if (publisher) {
6377
7511
  const canonical = `${entry.kind}:@${publisher}/${entry.name}`;
@@ -6554,7 +7688,7 @@ Nothing was changed. Declare it again to keep it, or run \`skaile install --forc
6554
7688
  return want !== null && gaps.some((g) => g.type === "library" && sameAsset(g.ref, want));
6555
7689
  };
6556
7690
  gaps = gaps.filter((g) => !(g.type === "requires" && libraryHeld(g.ref)));
6557
- for (const gap of this.undeployedLockGaps(lock, named)) {
7691
+ for (const gap of await this.undeployedLockGaps(lock, named)) {
6558
7692
  gaps.push(gap);
6559
7693
  named.add(gapIdentity(gap.ref));
6560
7694
  }
@@ -6576,7 +7710,7 @@ Nothing was changed. Declare it again to keep it, or run \`skaile install --forc
6576
7710
  }),
6577
7711
  ...d.extra.map((f) => `${f} is not part of the asset`)
6578
7712
  ];
6579
- const reason = `is locked, but its deployed copy differs from the lock (${parts.join("; ")}) \u2014 run \`skaile install --locked\` to restore it`;
7713
+ const reason = d.libraryChanged ? `is locked, but its deployed copy differs from the lock (${parts.join("; ")}) \u2014 the library holds other content now: copy your edit away, then run \`skaile update --force\` to replace it` : `is locked, but its deployed copy differs from the lock (${parts.join("; ")}) \u2014 run \`skaile install --locked\` to restore it`;
6580
7714
  gaps.push({ type: "modified", ref: d.ref, reason, message: `${d.ref} ${reason}` });
6581
7715
  named.add(gapIdentity(d.ref));
6582
7716
  }
@@ -6609,7 +7743,8 @@ Nothing was changed. Declare it again to keep it, or run \`skaile install --forc
6609
7743
  ref: s.url,
6610
7744
  reason,
6611
7745
  message: `source ${s.url} ${reason}`,
6612
- manualFix: true
7746
+ manualFix: true,
7747
+ unreadableSource: true
6613
7748
  });
6614
7749
  }
6615
7750
  for (const u of unboundAgentInstances(this.projectDir, this.harness)) {
@@ -6629,6 +7764,22 @@ Nothing was changed. Declare it again to keep it, or run \`skaile install --forc
6629
7764
  )
6630
7765
  };
6631
7766
  }
7767
+ /**
7768
+ * The first `../` compose folder of the locked library agent `kind:name` whose library copy
7769
+ * changed since the lock, or `undefined` (project scope only). `install --locked` cannot
7770
+ * restore such an agent; `update` moves the lock to the library's copy.
7771
+ */
7772
+ async lockedComposeDrift(lock, kind, name) {
7773
+ if (this.global) return void 0;
7774
+ const { entries } = await this.restoreLibraryLockEntries(lock);
7775
+ const entry = entries.find((x) => x.kind === kind && x.name === name);
7776
+ if (!entry) return void 0;
7777
+ try {
7778
+ return composedDriftRefs(entry, this.deployOptsFor(this.harness))[0];
7779
+ } catch {
7780
+ return void 0;
7781
+ }
7782
+ }
6632
7783
  /**
6633
7784
  * Why the `agent:` ref of agent instance `id` binds to nothing, as the phrase that follows
6634
7785
  * the ref in a {@link LockGap}; see {@link AssetManager.unboundAgentAdvice}.
@@ -6652,6 +7803,15 @@ Nothing was changed. Declare it again to keep it, or run \`skaile install --forc
6652
7803
  return k.kind === "agent" && k.name === parsedRef?.name && (!publisher || !k.publisher || k.publisher === publisher);
6653
7804
  });
6654
7805
  if (lockedAgentKey !== void 0) {
7806
+ const lock = this.tryReadLock();
7807
+ const locked = parseCanonicalLockKey(lockedAgentKey);
7808
+ const composeRef = lock && locked.kind && locked.name ? await this.lockedComposeDrift(lock, locked.kind, locked.name) : void 0;
7809
+ if (composeRef) {
7810
+ return {
7811
+ lead: `its agent composes ${composeRef}, whose library copy changed since the lock`,
7812
+ advice: `run \`skaile update\` (it moves the lock to the library's copy), ${drop}`
7813
+ };
7814
+ }
6655
7815
  const libraryFix = await this.lockedAgentLibraryHolds(lockedAgentKey) ? void 0 : (await this.unresolvedFixes([ref])).get(ref);
6656
7816
  if (libraryFix) {
6657
7817
  const register = libraryFix.replace(
@@ -6835,7 +7995,8 @@ Nothing was changed. Declare it again to keep it, or run \`skaile install --forc
6835
7995
  continue;
6836
7996
  }
6837
7997
  if (lookup.state === "unregistered") {
6838
- const reason2 = `is locked, but library "${origin.name}" is not registered (renamed or removed?) \u2014 ${unregisteredLibraryFix(origin, ref)}`;
7998
+ if (await this.restoredFromLockUrl(key, entry, snap)) continue;
7999
+ const reason2 = `is locked, but library "${unregisteredLibraryName(origin)}" is not registered (renamed or removed?) \u2014 ${unregisteredLibraryFix(origin, ref)}`;
6839
8000
  gaps.push({ type: "library", ref, reason: reason2, message: `${ref} ${reason2}`, manualFix: true });
6840
8001
  continue;
6841
8002
  }
@@ -6855,12 +8016,15 @@ Nothing was changed. Declare it again to keep it, or run \`skaile install --forc
6855
8016
  * The locked source assets whose source is no longer in `skaile.yaml` `sources:` (`source
6856
8017
  * remove`, a hand edit), keyed by lock key. ONE sentence for it: `check`, `doctor`, `outdated`,
6857
8018
  * `install`, `install --locked` and `update` all say it (p1062), and none re-clones the source.
6858
- * A source of a store-resolved lock entry is not told apart from one, so no GitHub source is
6859
- * called missing while a `stores:` entry exists.
8019
+ * A GitHub lock entry that no source declares counts as store-supplied only when a store
8020
+ * lists the ref (OQ3): a configured store alone proves nothing, as it may never have held
8021
+ * the asset. `check`, `doctor` and `outdated` read the store's cached catalog and never touch
8022
+ * the network; `install` and `update` (`online`) ask the store itself.
6860
8023
  *
6861
8024
  * @param inScope - Only these lock keys count (`install` prunes an undeclared entry as usual).
8025
+ * @param online - Ask the stores when the cached catalog does not list the ref.
6862
8026
  */
6863
- async orphanedSourceGaps(inScope) {
8027
+ async orphanedSourceGaps(inScope, online = false) {
6864
8028
  const out = /* @__PURE__ */ new Map();
6865
8029
  let config;
6866
8030
  try {
@@ -6868,23 +8032,53 @@ Nothing was changed. Declare it again to keep it, or run \`skaile install --forc
6868
8032
  } catch {
6869
8033
  return out;
6870
8034
  }
6871
- const storeBacked = config.stores.length > 0 || (await this.globalStoreFallback(config.stores, false)).length > 0;
8035
+ const stores = [...config.stores, ...await this.globalStoreFallback(config.stores, false)];
6872
8036
  const registered = new Set(config.sources.map((s) => sourceCacheKey(s.url)));
6873
8037
  for (const [key, entry] of Object.entries(this.tryReadLock()?.assets ?? {})) {
6874
8038
  const url = entry.source?.url;
6875
8039
  if (!url || entry.library || !GIT_URL.test(url)) continue;
6876
8040
  if (inScope && !inScope.has(key)) continue;
6877
8041
  if (normalizeRepoUrl(url) === normalizeRepoUrl(FACTORY_SOURCE_URL)) continue;
6878
- const { publisher, kind, name } = parseCanonicalLockKey(key);
8042
+ const { publisher, kind, name, version } = parseCanonicalLockKey(key);
6879
8043
  if (!publisher || !kind || !name) continue;
6880
8044
  if (registered.has(sourceCacheKey(url))) continue;
6881
- if (storeBacked && GITHUB_REPO_URL.test(url)) continue;
6882
8045
  const ref = `${kind}:@${publisher}/${name}`;
8046
+ const locked = version ? `${ref}#${version}` : void 0;
8047
+ if (GITHUB_REPO_URL.test(url) && await this.storeListsRef(stores, ref, online, locked))
8048
+ continue;
6883
8049
  const reason = `is locked, but its source ${url} is not in skaile.yaml sources: any more \u2014 register it again with \`skaile source add ${url}\`, or drop the dependency with \`skaile remove ${ref}\``;
6884
8050
  out.set(key, { type: "source", ref, reason, message: `${ref} ${reason}`, manualFix: true });
6885
8051
  }
6886
8052
  return out;
6887
8053
  }
8054
+ /**
8055
+ * `true` when one of `stores` lists `ref` (a canonical lock key): its cached catalog holds it
8056
+ * or, with `online`, the store answers for it. An injected store fetcher is the store client
8057
+ * itself, so it is asked either way. A store an online install cannot ask (unreachable,
8058
+ * air-gapped, `--no-catalog`) may hold the ref; `check` never asks, so a store lists only what
8059
+ * its cached catalog holds.
8060
+ */
8061
+ async storeListsRef(stores, ref, online, lockedRef) {
8062
+ for (const store of stores) {
8063
+ try {
8064
+ if (this.injectedStoreFetcher) {
8065
+ if (await this.injectedStoreFetcher.getInstallManifest(store.url, ref)) return true;
8066
+ continue;
8067
+ }
8068
+ const remote = this.remoteStore(store.url);
8069
+ if (remote.holdsCached(ref)) return true;
8070
+ if (!online) {
8071
+ if (lockedRef && remote.holdsCached(lockedRef)) return true;
8072
+ continue;
8073
+ }
8074
+ if (this.catalogUrlOverride === "") return true;
8075
+ if (await (lockedRef ? remote.revalidate(lockedRef) : remote.resolve(ref))) return true;
8076
+ } catch {
8077
+ if (online) return true;
8078
+ }
8079
+ }
8080
+ return false;
8081
+ }
6888
8082
  /**
6889
8083
  * Locked source assets whose source cannot supply them any more: the source is no longer in
6890
8084
  * `skaile.yaml` `sources:` (`source remove`, a hand edit), or it is there but now publishes
@@ -6936,13 +8130,55 @@ Nothing was changed. Declare it again to keep it, or run \`skaile install --forc
6936
8130
  }
6937
8131
  return out;
6938
8132
  }
8133
+ /**
8134
+ * Record, on every library-origin entry of `lock` that is copied into a harness folder, the
8135
+ * files of the deployed copy and their sha256 (`LockEntry.copied`), so a hand edit is named
8136
+ * even after the library moved past the lock (p1641). Call it after the deploy. An entry that
8137
+ * is unchanged since `prev` keeps its record, or stays without one: the deployed bytes may
8138
+ * carry an edit the deploy did not touch. Project mode only; a `--link` deploy and a patched
8139
+ * asset are not recorded.
8140
+ */
8141
+ withCopiedFiles(lock, prev) {
8142
+ if (this.global) return lock;
8143
+ const { patches } = this.loadConfig();
8144
+ for (const [key, entry] of Object.entries(lock.assets)) {
8145
+ if (!entry.library || entry.library.installed) continue;
8146
+ const before = prev?.assets[key];
8147
+ if (before && before.library?.sha256 === entry.library.sha256) {
8148
+ if (before.copied) entry.copied = before.copied;
8149
+ else delete entry.copied;
8150
+ continue;
8151
+ }
8152
+ delete entry.copied;
8153
+ const { kind, name } = parseCanonicalLockKey(key);
8154
+ const component = kind ? this.installTarget(kind) : void 0;
8155
+ if (!kind || !name || !component || component === "agent") continue;
8156
+ if (patches[`${kind}:${name}`]) continue;
8157
+ const dest = deployedDir(component, name, this.harness, this.global, this.projectDir);
8158
+ try {
8159
+ const st = lstatSync(dest);
8160
+ if (st.isSymbolicLink() || !st.isDirectory()) continue;
8161
+ const rows = listAssetFiles(dest).map((path2) => ({
8162
+ path: path2,
8163
+ sha256: computeHash(join(dest, path2))
8164
+ }));
8165
+ if (rows.length > 0) entry.copied = rows.sort((a, b) => a.path.localeCompare(b.path));
8166
+ } catch {
8167
+ }
8168
+ }
8169
+ return lock;
8170
+ }
6939
8171
  /**
6940
8172
  * Record, on every agent entry of `lock` that has none, the files the agent's harness
6941
8173
  * rendering occupies and their sha256. Call it after the deploy, so the bytes hashed are the
6942
8174
  * ones `install` just wrote; {@link lockedCopyDrift} compares later edits with them.
6943
8175
  * Entries that carry a record (kept from an earlier lock) are left as they are.
8176
+ *
8177
+ * @param keepComposedOf - The lock an `install` read: an agent whose `../` compose folder is
8178
+ * not in the cache keeps that lock's `composed:` record. Omitted by `update` and `add`,
8179
+ * which copy the library's current bytes.
6944
8180
  */
6945
- withRenderedAgents(lock) {
8181
+ withRenderedAgents(lock, keepComposedOf) {
6946
8182
  if (this.global) return lock;
6947
8183
  for (const [key, entry] of Object.entries(lock.assets)) {
6948
8184
  if (entry.rendered || entry.library?.installed) continue;
@@ -6951,6 +8187,18 @@ Nothing was changed. Declare it again to keep it, or run \`skaile install --forc
6951
8187
  const rendered = this.renderedAgentFiles(name);
6952
8188
  if (rendered.length > 0) entry.rendered = rendered;
6953
8189
  }
8190
+ for (const [key, entry] of Object.entries(lock.assets)) {
8191
+ if (entry.library?.installed) continue;
8192
+ const { kind, name } = parseCanonicalLockKey(key);
8193
+ if (!kind || !name || this.installTarget(kind) !== "agent") continue;
8194
+ const composed = cachedComposedFiles(name, this.projectDir);
8195
+ if (composed.length > 0) entry.composed = composed;
8196
+ else {
8197
+ const earlier = keepComposedOf?.assets[key]?.composed;
8198
+ if (earlier?.length) entry.composed = earlier;
8199
+ else delete entry.composed;
8200
+ }
8201
+ }
6954
8202
  return this.withInstanceRenderings(lock);
6955
8203
  }
6956
8204
  /**
@@ -7129,6 +8377,73 @@ Nothing was changed. Declare it again to keep it, or run \`skaile install --forc
7129
8377
  }
7130
8378
  return out;
7131
8379
  }
8380
+ /**
8381
+ * Hand edits in the deployed copy of a library asset whose library moved past the lock (a
8382
+ * version bump, or an in-place edit of the same version): {@link restoreLibraryLockEntries}
8383
+ * drops such an entry, so {@link lockedCopyDrift} would not see the edit and `update` would
8384
+ * replace the file without a word (p1641). `covered` holds the `kind:@publisher/name` of the
8385
+ * entries that were compared already. The lock's per-file record (`copied:`) names the edited
8386
+ * files; a lock without one (written before it existed) compares the deployed folder with the
8387
+ * one library hash and names the files that also differ from the library's new content.
8388
+ */
8389
+ async changedLibraryCopyEdits(lock, covered, patches) {
8390
+ const out = [];
8391
+ let snap;
8392
+ const fileSets = /* @__PURE__ */ new Map();
8393
+ for (const [key, entry] of Object.entries(lock.assets)) {
8394
+ const origin = entry.library;
8395
+ if (!origin || origin.installed) continue;
8396
+ const { kind, name, publisher } = parseCanonicalLockKey(key);
8397
+ if (!kind || !name || !publisher) continue;
8398
+ const ref = `${kind}:@${publisher}/${name}`;
8399
+ if (covered.has(ref) || patches[`${kind}:${name}`]) continue;
8400
+ const component = this.installTarget(kind);
8401
+ if (!component || component === "agent") continue;
8402
+ const dest = deployedDir(component, name, this.harness, this.global, this.projectDir);
8403
+ try {
8404
+ const st = lstatSync(dest);
8405
+ if (st.isSymbolicLink() || !st.isDirectory()) continue;
8406
+ } catch {
8407
+ continue;
8408
+ }
8409
+ let modified = [];
8410
+ if (entry.copied && entry.copied.length > 0) {
8411
+ for (const f of entry.copied) {
8412
+ const file = join(dest, f.path);
8413
+ if (!existsSync(file)) continue;
8414
+ try {
8415
+ if (computeHash(file) !== f.sha256) modified.push(f.path);
8416
+ } catch {
8417
+ modified.push(f.path);
8418
+ }
8419
+ }
8420
+ } else if (origin.sha256) {
8421
+ snap ??= await this.loadLibraryIndex();
8422
+ const lookup = this.lockedLibrary(origin, snap, { kind, publisher, name });
8423
+ if (lookup.state !== "ok") continue;
8424
+ const found = (await scanDirectory(lookup.path, publisher, { knownKinds: this.knownKinds })).find((x) => x.kind === kind && x.name === name);
8425
+ if (!found) continue;
8426
+ const current = withLibraryFiles(found, lookup.path, fileSets, this.registry);
8427
+ const assetRoot = statSync(current.source).isDirectory() ? current.source : dirname(current.source);
8428
+ const hash = computeDeterministicHash(
8429
+ dest,
8430
+ listAssetFiles(dest),
8431
+ posix.normalize(relative(lookup.path, assetRoot).split(sep).join("/"))
8432
+ );
8433
+ if (hash === origin.sha256) continue;
8434
+ modified = deployedAssetDrift(current, {
8435
+ registry: this.registry,
8436
+ harness: this.harness,
8437
+ global: this.global,
8438
+ cwd: this.projectDir
8439
+ })?.modified ?? [];
8440
+ }
8441
+ if (modified.length > 0) {
8442
+ out.push({ ref, key, modified, extra: [], library: true, libraryChanged: true });
8443
+ }
8444
+ }
8445
+ return out;
8446
+ }
7132
8447
  /**
7133
8448
  * Locked assets whose deployed copy differs from the bytes the lock covers: a file edited by
7134
8449
  * hand, or a file the asset does not ship. One row per asset, `ref` being `kind:@publisher/name`
@@ -7172,6 +8487,15 @@ Nothing was changed. Declare it again to keep it, or run \`skaile install --forc
7172
8487
  const ref = `${e.kind}:@${(e.publisher ?? "").replace(/^@/, "")}/${e.name}`;
7173
8488
  out.push({ ref, key: keyOf.get(ref) ?? ref, ...drift, library: true });
7174
8489
  }
8490
+ out.push(
8491
+ ...await this.changedLibraryCopyEdits(
8492
+ lock,
8493
+ new Set(
8494
+ entries.map((e) => `${e.kind}:@${(e.publisher ?? "").replace(/^@/, "")}/${e.name}`)
8495
+ ),
8496
+ patches
8497
+ )
8498
+ );
7175
8499
  for (const [key, entry] of Object.entries(lock.assets)) {
7176
8500
  if (!entry.source || entry.files.length === 0) continue;
7177
8501
  const { kind, name } = parseCanonicalLockKey(key);
@@ -7206,12 +8530,13 @@ Nothing was changed. Declare it again to keep it, or run \`skaile install --forc
7206
8530
  out.push({ ref: refOf(key), key, modified, extra, library: false });
7207
8531
  }
7208
8532
  for (const [key, entry] of Object.entries(lock.assets)) {
7209
- if (!entry.rendered || entry.rendered.length === 0 || entry.library?.installed) continue;
8533
+ const recorded = [...entry.rendered ?? [], ...entry.composed ?? []];
8534
+ if (recorded.length === 0 || entry.library?.installed) continue;
7210
8535
  const { kind, name } = parseCanonicalLockKey(key);
7211
8536
  if (!kind || !name || this.installTarget(kind) !== "agent") continue;
7212
8537
  if (patches[`${kind}:${name}`]) continue;
7213
8538
  const modified = [];
7214
- for (const f of entry.rendered) {
8539
+ for (const f of recorded) {
7215
8540
  const file = join(this.projectDir, f.path);
7216
8541
  if (!existsSync(file)) continue;
7217
8542
  try {
@@ -7232,7 +8557,7 @@ Nothing was changed. Declare it again to keep it, or run \`skaile install --forc
7232
8557
  extra: [],
7233
8558
  library: false,
7234
8559
  rendered: true,
7235
- lockedSha: new Map(entry.rendered.map((f) => [f.path, f.sha256])),
8560
+ lockedSha: new Map(recorded.map((f) => [f.path, f.sha256])),
7236
8561
  instanceOf
7237
8562
  });
7238
8563
  }
@@ -7245,7 +8570,7 @@ Nothing was changed. Declare it again to keep it, or run \`skaile install --forc
7245
8570
  *
7246
8571
  * @param named - `kind:name` identities already reported; they are skipped.
7247
8572
  */
7248
- undeployedLockGaps(lock, named) {
8573
+ async undeployedLockGaps(lock, named) {
7249
8574
  const gaps = [];
7250
8575
  for (const [key, entry] of Object.entries(lock?.assets ?? {})) {
7251
8576
  if (entry.library?.installed) continue;
@@ -7264,7 +8589,8 @@ Nothing was changed. Declare it again to keep it, or run \`skaile install --forc
7264
8589
  this.global,
7265
8590
  this.projectDir
7266
8591
  ).find((p) => isSymlink(p) && !existsSync(p));
7267
- const reason = dangling ? `is locked but its deployed copy is a symlink to nothing (${relative(this.projectDir, dangling)}) \u2014 run \`skaile install --locked\`` : "is locked but its deployed copy is missing \u2014 run `skaile install --locked`";
8592
+ const composeDrift = component === "agent" && !dangling && lock ? await this.lockedComposeDrift(lock, kind, name) : void 0;
8593
+ const reason = composeDrift ? `(composes ${composeDrift}: the library's copy changed since the lock; \`skaile update\` moves the lock to it)` : dangling ? `is locked but its deployed copy is a symlink to nothing (${relative(this.projectDir, dangling)}) \u2014 run \`skaile install --locked\`` : "is locked but its deployed copy is missing \u2014 run `skaile install --locked`";
7268
8594
  gaps.push({ type: "drift", ref, reason, message: `${ref} ${reason}` });
7269
8595
  }
7270
8596
  return gaps;
@@ -7345,10 +8671,23 @@ Nothing was changed. Declare it again to keep it, or run \`skaile install --forc
7345
8671
  ({ id }) => id.kind === want.kind && id.name === want.name && (!want.publisher || id.publisher === want.publisher)
7346
8672
  );
7347
8673
  if (held.length > 0 && !held.some(({ id }) => !id.version || matchPin(want.pin, id.version))) {
7348
- gap = {
7349
- type: "pin",
7350
- reason: `is pinned in skaile.yaml but the lock holds ${held.map((h) => h.key).join(", ")} \u2014 run \`skaile install\``
7351
- };
8674
+ const lockHolds = held.map((h) => h.key).join(", ");
8675
+ const pinned = await this.info(d.ref);
8676
+ const pinHeld = pinned !== null && matchPin(want.pin, pinned.version ?? "");
8677
+ if (!pinHeld && this.loadConfig().stores.length === 0) {
8678
+ const plainRef = d.ref.split("#")[0] ?? d.ref;
8679
+ const other = pinned ?? await this.info(plainRef);
8680
+ gap = {
8681
+ type: "pin",
8682
+ reason: `is pinned in skaile.yaml but the lock holds ${lockHolds}, and no library or source holds ${want.pin}${other?.version ? ` (${want.name} has version(s): ${other.version})` : ""} \u2014 change the pin in skaile.yaml`,
8683
+ manualFix: true
8684
+ };
8685
+ } else {
8686
+ gap = {
8687
+ type: "pin",
8688
+ reason: `is pinned in skaile.yaml but the lock holds ${lockHolds} \u2014 run \`skaile install\``
8689
+ };
8690
+ }
7352
8691
  }
7353
8692
  }
7354
8693
  if (gap) gaps.push({ ...gap, ref: d.ref, message: `${d.ref} ${gap.reason}` });
@@ -7641,20 +8980,26 @@ Nothing was changed. Declare it again to keep it, or run \`skaile install --forc
7641
8980
  * user-scope SSOT `~/.skaile/skaile.yaml`, **bootstrapping** the file (and its
7642
8981
  * parent dir) when absent so `skaile add --global` works on a fresh machine.
7643
8982
  */
7644
- addDependencyToConfig(ref) {
8983
+ addDependencyToConfig(ref, only = false) {
7645
8984
  const configPath = join(this.stateRoot, "skaile.yaml");
7646
- const limit = this.global && this.harnessesOverride?.length ? this.harnessesOverride : void 0;
8985
+ const requested = this.global && this.harnessesOverride?.length ? this.harnessesOverride : void 0;
7647
8986
  if (!existsSync(configPath)) {
7648
8987
  if (!this.global) return;
7649
8988
  mkdirSync(this.stateRoot, { recursive: true });
7650
8989
  const fresh = { dependencies: [ref] };
7651
- if (limit) this.setEntryHarnessRow(fresh, ref, limit);
8990
+ if (requested) this.setEntryHarnessRow(fresh, ref, requested);
7652
8991
  writeFileSync(configPath, stringify(fresh, { lineWidth: 120 }));
7653
8992
  return;
7654
8993
  }
7655
8994
  const raw = parse(readFileSync(configPath, "utf8")) ?? {};
7656
8995
  if (!Array.isArray(raw.dependencies)) raw.dependencies = [];
7657
8996
  const deps = raw.dependencies;
8997
+ const limit = requested ? only ? requested : this.widenedHarnessRow(
8998
+ raw,
8999
+ ref,
9000
+ requested,
9001
+ deps.some((d) => this.sameDeclaredAsset(d, ref))
9002
+ ) : void 0;
7658
9003
  const limitChanged = this.global ? this.setEntryHarnessRow(raw, ref, limit) : false;
7659
9004
  if (deps.includes(ref) && deps.filter((d) => this.sameDeclaredAsset(d, ref)).length === 1) {
7660
9005
  if (limitChanged) writeFileSync(configPath, stringify(raw, { lineWidth: 120 }));
@@ -7821,6 +9166,6 @@ Nothing was changed. Declare it again to keep it, or run \`skaile install --forc
7821
9166
  }
7822
9167
  };
7823
9168
 
7824
- export { AssetManager, FACTORY_SOURCE_URL, LibraryEntryError, MissingManifestError, installAgentInstances, normalizeRepoUrl, parseCanonicalLockKey, parseRefTolerant, renderMcpServerConfig, softDepName };
7825
- //# sourceMappingURL=chunk-OAHVKSQJ.js.map
7826
- //# sourceMappingURL=chunk-OAHVKSQJ.js.map
9169
+ export { AssetManager, LibraryEntryError, MissingManifestError, installAgentInstances, labelDiffHeaders, mcpNotRunnableReason, normalizeRepoUrl, parseCanonicalLockKey, parseRefTolerant, renderMcpServerConfig, setLibraryAssetCheck, setRenamedEntryLookup, softDepName };
9170
+ //# sourceMappingURL=chunk-PKBLJI23.js.map
9171
+ //# sourceMappingURL=chunk-PKBLJI23.js.map