@canonical/pragma-cli 0.33.0 → 0.35.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 (892) hide show
  1. package/README.md +153 -30
  2. package/dist/.build-ok +0 -0
  3. package/dist/package.json +83 -0
  4. package/dist/pragma.conf.js +814 -0
  5. package/dist/pragma.conf.js.map +1 -0
  6. package/dist/src/bin.js +356 -0
  7. package/dist/src/bin.js.map +1 -0
  8. package/dist/src/capabilities/capabilities/capabilities.render.js +70 -0
  9. package/dist/src/capabilities/capabilities/capabilities.render.js.map +1 -0
  10. package/dist/src/capabilities/capabilities/capabilities.verb.js +59 -0
  11. package/dist/src/capabilities/capabilities/capabilities.verb.js.map +1 -0
  12. package/dist/src/capabilities/capabilities/catalog.js +130 -0
  13. package/dist/src/capabilities/capabilities/catalog.js.map +1 -0
  14. package/dist/src/capabilities/capabilities/hints.js +195 -0
  15. package/dist/src/capabilities/capabilities/hints.js.map +1 -0
  16. package/dist/src/capabilities/capabilities/index.js +11 -0
  17. package/dist/src/capabilities/capabilities/index.js.map +1 -0
  18. package/dist/src/capabilities/capabilities/types.js +11 -0
  19. package/dist/src/capabilities/capabilities/types.js.map +1 -0
  20. package/dist/src/capabilities/colophon/collectColophon.js +83 -0
  21. package/dist/src/capabilities/colophon/collectColophon.js.map +1 -0
  22. package/dist/src/capabilities/colophon/colophon.render.js +59 -0
  23. package/dist/src/capabilities/colophon/colophon.render.js.map +1 -0
  24. package/dist/src/capabilities/colophon/colophon.verb.js +43 -0
  25. package/dist/src/capabilities/colophon/colophon.verb.js.map +1 -0
  26. package/dist/src/capabilities/colophon/index.js +5 -0
  27. package/dist/src/capabilities/colophon/index.js.map +1 -0
  28. package/dist/src/capabilities/colophon/markdownTerminal.js +90 -0
  29. package/dist/src/capabilities/colophon/markdownTerminal.js.map +1 -0
  30. package/dist/src/capabilities/colophon/types.js +10 -0
  31. package/dist/src/capabilities/colophon/types.js.map +1 -0
  32. package/dist/src/capabilities/config/collectConfigShow.js +59 -0
  33. package/dist/src/capabilities/config/collectConfigShow.js.map +1 -0
  34. package/dist/src/capabilities/config/field.render.js +22 -0
  35. package/dist/src/capabilities/config/field.render.js.map +1 -0
  36. package/dist/src/capabilities/config/fields.js +46 -0
  37. package/dist/src/capabilities/config/fields.js.map +1 -0
  38. package/dist/src/capabilities/config/get.verb.js +61 -0
  39. package/dist/src/capabilities/config/get.verb.js.map +1 -0
  40. package/dist/src/capabilities/config/index.js +5 -0
  41. package/dist/src/capabilities/config/index.js.map +1 -0
  42. package/dist/src/capabilities/config/runField.js +59 -0
  43. package/dist/src/capabilities/config/runField.js.map +1 -0
  44. package/dist/src/capabilities/config/runGet.js +28 -0
  45. package/dist/src/capabilities/config/runGet.js.map +1 -0
  46. package/dist/src/capabilities/config/runSet.js +53 -0
  47. package/dist/src/capabilities/config/runSet.js.map +1 -0
  48. package/dist/src/capabilities/config/set.verb.js +66 -0
  49. package/dist/src/capabilities/config/set.verb.js.map +1 -0
  50. package/dist/src/capabilities/config/show.render.js +84 -0
  51. package/dist/src/capabilities/config/show.render.js.map +1 -0
  52. package/dist/src/capabilities/config/show.verb.js +44 -0
  53. package/dist/src/capabilities/config/show.verb.js.map +1 -0
  54. package/dist/src/capabilities/config/types.js +5 -0
  55. package/dist/src/capabilities/config/types.js.map +1 -0
  56. package/dist/src/capabilities/config/unset.verb.js +49 -0
  57. package/dist/src/capabilities/config/unset.verb.js.map +1 -0
  58. package/dist/src/capabilities/create/cliProjection.js +195 -0
  59. package/dist/src/capabilities/create/cliProjection.js.map +1 -0
  60. package/dist/src/capabilities/create/constants.js +47 -0
  61. package/dist/src/capabilities/create/constants.js.map +1 -0
  62. package/dist/src/capabilities/create/create.render.js +100 -0
  63. package/dist/src/capabilities/create/create.render.js.map +1 -0
  64. package/dist/src/capabilities/create/create.verb.js +438 -0
  65. package/dist/src/capabilities/create/create.verb.js.map +1 -0
  66. package/dist/src/capabilities/create/createHelp.js +47 -0
  67. package/dist/src/capabilities/create/createHelp.js.map +1 -0
  68. package/dist/src/capabilities/create/createSurface.generated.js +394 -0
  69. package/dist/src/capabilities/create/createSurface.generated.js.map +1 -0
  70. package/dist/src/capabilities/create/generatorToVerbSpec.js +106 -0
  71. package/dist/src/capabilities/create/generatorToVerbSpec.js.map +1 -0
  72. package/dist/src/capabilities/create/index.js +18 -0
  73. package/dist/src/capabilities/create/index.js.map +1 -0
  74. package/dist/src/capabilities/create/messages.js +16 -0
  75. package/dist/src/capabilities/create/messages.js.map +1 -0
  76. package/dist/src/capabilities/create/mount.js +293 -0
  77. package/dist/src/capabilities/create/mount.js.map +1 -0
  78. package/dist/src/capabilities/create/pathJail.js +65 -0
  79. package/dist/src/capabilities/create/pathJail.js.map +1 -0
  80. package/dist/src/capabilities/create/pickGenerator.js +53 -0
  81. package/dist/src/capabilities/create/pickGenerator.js.map +1 -0
  82. package/dist/src/capabilities/create/types.js +2 -0
  83. package/dist/src/capabilities/create/types.js.map +1 -0
  84. package/dist/src/capabilities/distribution.js +40 -0
  85. package/dist/src/capabilities/distribution.js.map +1 -0
  86. package/dist/src/capabilities/doctor/checks/checkKeStore.js +40 -0
  87. package/dist/src/capabilities/doctor/checks/checkKeStore.js.map +1 -0
  88. package/dist/src/capabilities/doctor/checks/checkNodeVersion.js +21 -0
  89. package/dist/src/capabilities/doctor/checks/checkNodeVersion.js.map +1 -0
  90. package/dist/src/capabilities/doctor/checks/checkPackageRefs.js +92 -0
  91. package/dist/src/capabilities/doctor/checks/checkPackageRefs.js.map +1 -0
  92. package/dist/src/capabilities/doctor/checks/checkPragmaVersion.js +19 -0
  93. package/dist/src/capabilities/doctor/checks/checkPragmaVersion.js.map +1 -0
  94. package/dist/src/capabilities/doctor/checks/checkShellCompletions.js +199 -0
  95. package/dist/src/capabilities/doctor/checks/checkShellCompletions.js.map +1 -0
  96. package/dist/src/capabilities/doctor/checks/index.js +27 -0
  97. package/dist/src/capabilities/doctor/checks/index.js.map +1 -0
  98. package/dist/src/capabilities/doctor/checks/mcpCommand.js +57 -0
  99. package/dist/src/capabilities/doctor/checks/mcpCommand.js.map +1 -0
  100. package/dist/src/capabilities/doctor/checks/targetHealth.js +205 -0
  101. package/dist/src/capabilities/doctor/checks/targetHealth.js.map +1 -0
  102. package/dist/src/capabilities/doctor/doctor.render.js +185 -0
  103. package/dist/src/capabilities/doctor/doctor.render.js.map +1 -0
  104. package/dist/src/capabilities/doctor/doctor.verb.js +45 -0
  105. package/dist/src/capabilities/doctor/doctor.verb.js.map +1 -0
  106. package/dist/src/capabilities/doctor/index.js +5 -0
  107. package/dist/src/capabilities/doctor/index.js.map +1 -0
  108. package/dist/src/capabilities/doctor/runChecks.js +83 -0
  109. package/dist/src/capabilities/doctor/runChecks.js.map +1 -0
  110. package/dist/src/capabilities/doctor/types.js +5 -0
  111. package/dist/src/capabilities/doctor/types.js.map +1 -0
  112. package/dist/src/capabilities/graph/index.js +19 -0
  113. package/dist/src/capabilities/graph/index.js.map +1 -0
  114. package/dist/src/capabilities/graph/inspect.render.js +76 -0
  115. package/dist/src/capabilities/graph/inspect.render.js.map +1 -0
  116. package/dist/src/capabilities/graph/inspect.verb.js +53 -0
  117. package/dist/src/capabilities/graph/inspect.verb.js.map +1 -0
  118. package/dist/src/capabilities/graph/query.render.js +73 -0
  119. package/dist/src/capabilities/graph/query.render.js.map +1 -0
  120. package/dist/src/capabilities/graph/query.verb.js +49 -0
  121. package/dist/src/capabilities/graph/query.verb.js.map +1 -0
  122. package/dist/src/capabilities/graph/runQuery.js +41 -0
  123. package/dist/src/capabilities/graph/runQuery.js.map +1 -0
  124. package/dist/src/capabilities/index.js +56 -0
  125. package/dist/src/capabilities/index.js.map +1 -0
  126. package/dist/src/capabilities/info/collectInfo.js +66 -0
  127. package/dist/src/capabilities/info/collectInfo.js.map +1 -0
  128. package/dist/src/capabilities/info/index.js +5 -0
  129. package/dist/src/capabilities/info/index.js.map +1 -0
  130. package/dist/src/capabilities/info/info.render.js +53 -0
  131. package/dist/src/capabilities/info/info.render.js.map +1 -0
  132. package/dist/src/capabilities/info/info.verb.js +42 -0
  133. package/dist/src/capabilities/info/info.verb.js.map +1 -0
  134. package/dist/src/capabilities/info/types.js +5 -0
  135. package/dist/src/capabilities/info/types.js.map +1 -0
  136. package/dist/src/capabilities/meta/complete.verb.js +43 -0
  137. package/dist/src/capabilities/meta/complete.verb.js.map +1 -0
  138. package/dist/src/capabilities/meta/index.js +13 -0
  139. package/dist/src/capabilities/meta/index.js.map +1 -0
  140. package/dist/src/capabilities/meta/mcp.verb.js +34 -0
  141. package/dist/src/capabilities/meta/mcp.verb.js.map +1 -0
  142. package/dist/src/capabilities/ontology/index.js +21 -0
  143. package/dist/src/capabilities/ontology/index.js.map +1 -0
  144. package/dist/src/capabilities/ontology/queries.js +123 -0
  145. package/dist/src/capabilities/ontology/queries.js.map +1 -0
  146. package/dist/src/capabilities/ontology/render.js +66 -0
  147. package/dist/src/capabilities/ontology/render.js.map +1 -0
  148. package/dist/src/capabilities/ontology/verbs.js +181 -0
  149. package/dist/src/capabilities/ontology/verbs.js.map +1 -0
  150. package/dist/src/capabilities/prompt/index.js +16 -0
  151. package/dist/src/capabilities/prompt/index.js.map +1 -0
  152. package/dist/src/capabilities/prompt/prompt.render.js +78 -0
  153. package/dist/src/capabilities/prompt/prompt.render.js.map +1 -0
  154. package/dist/src/capabilities/prompt/types.js +10 -0
  155. package/dist/src/capabilities/prompt/types.js.map +1 -0
  156. package/dist/src/capabilities/prompt/verbs.js +93 -0
  157. package/dist/src/capabilities/prompt/verbs.js.map +1 -0
  158. package/dist/src/capabilities/resources/index.js +10 -0
  159. package/dist/src/capabilities/resources/index.js.map +1 -0
  160. package/dist/src/capabilities/resources/provider.js +423 -0
  161. package/dist/src/capabilities/resources/provider.js.map +1 -0
  162. package/dist/src/capabilities/setup/buildPlan.js +168 -0
  163. package/dist/src/capabilities/setup/buildPlan.js.map +1 -0
  164. package/dist/src/capabilities/setup/index.js +5 -0
  165. package/dist/src/capabilities/setup/index.js.map +1 -0
  166. package/dist/src/capabilities/setup/mode.js +53 -0
  167. package/dist/src/capabilities/setup/mode.js.map +1 -0
  168. package/dist/src/capabilities/setup/operations/index.js +39 -0
  169. package/dist/src/capabilities/setup/operations/index.js.map +1 -0
  170. package/dist/src/capabilities/setup/operations/setupCompletions.js +160 -0
  171. package/dist/src/capabilities/setup/operations/setupCompletions.js.map +1 -0
  172. package/dist/src/capabilities/setup/operations/setupConfig.js +101 -0
  173. package/dist/src/capabilities/setup/operations/setupConfig.js.map +1 -0
  174. package/dist/src/capabilities/setup/operations/setupGenerator.js +343 -0
  175. package/dist/src/capabilities/setup/operations/setupGenerator.js.map +1 -0
  176. package/dist/src/capabilities/setup/operations/setupLsp.js +277 -0
  177. package/dist/src/capabilities/setup/operations/setupLsp.js.map +1 -0
  178. package/dist/src/capabilities/setup/operations/setupMcp.js +207 -0
  179. package/dist/src/capabilities/setup/operations/setupMcp.js.map +1 -0
  180. package/dist/src/capabilities/setup/operations/setupSkills.js +316 -0
  181. package/dist/src/capabilities/setup/operations/setupSkills.js.map +1 -0
  182. package/dist/src/capabilities/setup/plan.js +128 -0
  183. package/dist/src/capabilities/setup/plan.js.map +1 -0
  184. package/dist/src/capabilities/setup/plan.render.js +193 -0
  185. package/dist/src/capabilities/setup/plan.render.js.map +1 -0
  186. package/dist/src/capabilities/setup/setup.render.js +54 -0
  187. package/dist/src/capabilities/setup/setup.render.js.map +1 -0
  188. package/dist/src/capabilities/setup/setup.verb.js +334 -0
  189. package/dist/src/capabilities/setup/setup.verb.js.map +1 -0
  190. package/dist/src/capabilities/setup/shell.js +196 -0
  191. package/dist/src/capabilities/setup/shell.js.map +1 -0
  192. package/dist/src/capabilities/setup/targets.js +263 -0
  193. package/dist/src/capabilities/setup/targets.js.map +1 -0
  194. package/dist/src/capabilities/setup/types.js +5 -0
  195. package/dist/src/capabilities/setup/types.js.map +1 -0
  196. package/dist/src/capabilities/shared/assertExecOk.js +137 -0
  197. package/dist/src/capabilities/shared/assertExecOk.js.map +1 -0
  198. package/dist/src/capabilities/shared/bands.js +16 -0
  199. package/dist/src/capabilities/shared/bands.js.map +1 -0
  200. package/dist/src/capabilities/shared/index.js +26 -0
  201. package/dist/src/capabilities/shared/index.js.map +1 -0
  202. package/dist/src/capabilities/shared/packageManager.js +103 -0
  203. package/dist/src/capabilities/shared/packageManager.js.map +1 -0
  204. package/dist/src/capabilities/shared/registry.js +55 -0
  205. package/dist/src/capabilities/shared/registry.js.map +1 -0
  206. package/dist/src/capabilities/skill/discover.js +195 -0
  207. package/dist/src/capabilities/skill/discover.js.map +1 -0
  208. package/dist/src/capabilities/skill/index.js +10 -0
  209. package/dist/src/capabilities/skill/index.js.map +1 -0
  210. package/dist/src/capabilities/skill/render.js +49 -0
  211. package/dist/src/capabilities/skill/render.js.map +1 -0
  212. package/dist/src/capabilities/skill/verbs.js +83 -0
  213. package/dist/src/capabilities/skill/verbs.js.map +1 -0
  214. package/dist/src/capabilities/sources/collectStatus.js +103 -0
  215. package/dist/src/capabilities/sources/collectStatus.js.map +1 -0
  216. package/dist/src/capabilities/sources/index.js +13 -0
  217. package/dist/src/capabilities/sources/index.js.map +1 -0
  218. package/dist/src/capabilities/sources/installSkills.js +167 -0
  219. package/dist/src/capabilities/sources/installSkills.js.map +1 -0
  220. package/dist/src/capabilities/sources/runUpdate.js +394 -0
  221. package/dist/src/capabilities/sources/runUpdate.js.map +1 -0
  222. package/dist/src/capabilities/sources/status.render.js +64 -0
  223. package/dist/src/capabilities/sources/status.render.js.map +1 -0
  224. package/dist/src/capabilities/sources/status.verb.js +40 -0
  225. package/dist/src/capabilities/sources/status.verb.js.map +1 -0
  226. package/dist/src/capabilities/sources/types.js +6 -0
  227. package/dist/src/capabilities/sources/types.js.map +1 -0
  228. package/dist/src/capabilities/sources/update.render.js +34 -0
  229. package/dist/src/capabilities/sources/update.render.js.map +1 -0
  230. package/dist/src/capabilities/sources/update.verb.js +55 -0
  231. package/dist/src/capabilities/sources/update.verb.js.map +1 -0
  232. package/dist/src/capabilities/upgrade/index.js +5 -0
  233. package/dist/src/capabilities/upgrade/index.js.map +1 -0
  234. package/dist/src/capabilities/upgrade/runUpgrade.js +99 -0
  235. package/dist/src/capabilities/upgrade/runUpgrade.js.map +1 -0
  236. package/dist/src/capabilities/upgrade/types.js +5 -0
  237. package/dist/src/capabilities/upgrade/types.js.map +1 -0
  238. package/dist/src/capabilities/upgrade/upgrade.render.js +44 -0
  239. package/dist/src/capabilities/upgrade/upgrade.render.js.map +1 -0
  240. package/dist/src/capabilities/upgrade/upgrade.verb.js +52 -0
  241. package/dist/src/capabilities/upgrade/upgrade.verb.js.map +1 -0
  242. package/dist/src/capabilities/version/index.js +10 -0
  243. package/dist/src/capabilities/version/index.js.map +1 -0
  244. package/dist/src/capabilities/version/version.verb.js +36 -0
  245. package/dist/src/capabilities/version/version.verb.js.map +1 -0
  246. package/dist/src/constants.js +82 -0
  247. package/dist/src/constants.js.map +1 -0
  248. package/dist/src/index.js +12 -0
  249. package/dist/src/index.js.map +1 -0
  250. package/dist/src/kernel/completion/complete.js +66 -0
  251. package/dist/src/kernel/completion/complete.js.map +1 -0
  252. package/dist/src/kernel/completion/emitScripts.js +92 -0
  253. package/dist/src/kernel/completion/emitScripts.js.map +1 -0
  254. package/dist/src/kernel/completion/entitySource.js +285 -0
  255. package/dist/src/kernel/completion/entitySource.js.map +1 -0
  256. package/dist/src/kernel/completion/index.js +14 -0
  257. package/dist/src/kernel/completion/index.js.map +1 -0
  258. package/dist/src/kernel/completion/model.js +301 -0
  259. package/dist/src/kernel/completion/model.js.map +1 -0
  260. package/dist/src/kernel/completion/parse.js +253 -0
  261. package/dist/src/kernel/completion/parse.js.map +1 -0
  262. package/dist/src/kernel/completion/rank.js +101 -0
  263. package/dist/src/kernel/completion/rank.js.map +1 -0
  264. package/dist/src/kernel/completion/resolve.js +100 -0
  265. package/dist/src/kernel/completion/resolve.js.map +1 -0
  266. package/dist/src/kernel/completion/templates/bash.js +209 -0
  267. package/dist/src/kernel/completion/templates/bash.js.map +1 -0
  268. package/dist/src/kernel/completion/templates/fish.js +214 -0
  269. package/dist/src/kernel/completion/templates/fish.js.map +1 -0
  270. package/dist/src/kernel/completion/templates/index.js +23 -0
  271. package/dist/src/kernel/completion/templates/index.js.map +1 -0
  272. package/dist/src/kernel/completion/templates/shared.js +98 -0
  273. package/dist/src/kernel/completion/templates/shared.js.map +1 -0
  274. package/dist/src/kernel/completion/templates/zsh.js +201 -0
  275. package/dist/src/kernel/completion/templates/zsh.js.map +1 -0
  276. package/dist/src/kernel/completion/types.js +13 -0
  277. package/dist/src/kernel/completion/types.js.map +1 -0
  278. package/dist/src/kernel/config/defaults.js +17 -0
  279. package/dist/src/kernel/config/defaults.js.map +1 -0
  280. package/dist/src/kernel/config/evaluateProjectConfig.js +126 -0
  281. package/dist/src/kernel/config/evaluateProjectConfig.js.map +1 -0
  282. package/dist/src/kernel/config/findProjectConfig.js +43 -0
  283. package/dist/src/kernel/config/findProjectConfig.js.map +1 -0
  284. package/dist/src/kernel/config/firstRun.js +34 -0
  285. package/dist/src/kernel/config/firstRun.js.map +1 -0
  286. package/dist/src/kernel/config/globalConfig.js +79 -0
  287. package/dist/src/kernel/config/globalConfig.js.map +1 -0
  288. package/dist/src/kernel/config/index.js +16 -0
  289. package/dist/src/kernel/config/index.js.map +1 -0
  290. package/dist/src/kernel/config/paths.js +35 -0
  291. package/dist/src/kernel/config/paths.js.map +1 -0
  292. package/dist/src/kernel/config/readConfig.js +78 -0
  293. package/dist/src/kernel/config/readConfig.js.map +1 -0
  294. package/dist/src/kernel/config/schema.js +130 -0
  295. package/dist/src/kernel/config/schema.js.map +1 -0
  296. package/dist/src/kernel/config/types.js +19 -0
  297. package/dist/src/kernel/config/types.js.map +1 -0
  298. package/dist/src/kernel/config/writeConfigField.js +59 -0
  299. package/dist/src/kernel/config/writeConfigField.js.map +1 -0
  300. package/dist/src/kernel/copy.js +43 -0
  301. package/dist/src/kernel/copy.js.map +1 -0
  302. package/dist/src/kernel/error/PragmaError.js +149 -0
  303. package/dist/src/kernel/error/PragmaError.js.map +1 -0
  304. package/dist/src/kernel/error/constants.js +40 -0
  305. package/dist/src/kernel/error/constants.js.map +1 -0
  306. package/dist/src/kernel/error/fromTaskError.js +96 -0
  307. package/dist/src/kernel/error/fromTaskError.js.map +1 -0
  308. package/dist/src/kernel/error/index.js +13 -0
  309. package/dist/src/kernel/error/index.js.map +1 -0
  310. package/dist/src/kernel/error/recovery.js +52 -0
  311. package/dist/src/kernel/error/recovery.js.map +1 -0
  312. package/dist/src/kernel/error/renderError.js +115 -0
  313. package/dist/src/kernel/error/renderError.js.map +1 -0
  314. package/dist/src/kernel/error/serialize.js +25 -0
  315. package/dist/src/kernel/error/serialize.js.map +1 -0
  316. package/dist/src/kernel/error/types.js +9 -0
  317. package/dist/src/kernel/error/types.js.map +1 -0
  318. package/dist/src/kernel/index.js +25 -0
  319. package/dist/src/kernel/index.js.map +1 -0
  320. package/dist/src/kernel/packs/collect.js +187 -0
  321. package/dist/src/kernel/packs/collect.js.map +1 -0
  322. package/dist/src/kernel/packs/compile.js +304 -0
  323. package/dist/src/kernel/packs/compile.js.map +1 -0
  324. package/dist/src/kernel/packs/disclosure.js +76 -0
  325. package/dist/src/kernel/packs/disclosure.js.map +1 -0
  326. package/dist/src/kernel/packs/glob.js +38 -0
  327. package/dist/src/kernel/packs/glob.js.map +1 -0
  328. package/dist/src/kernel/packs/graphql/buildLookupDocument.js +258 -0
  329. package/dist/src/kernel/packs/graphql/buildLookupDocument.js.map +1 -0
  330. package/dist/src/kernel/packs/graphql/fetchGraphqlLookup.js +157 -0
  331. package/dist/src/kernel/packs/graphql/fetchGraphqlLookup.js.map +1 -0
  332. package/dist/src/kernel/packs/graphql/index.js +20 -0
  333. package/dist/src/kernel/packs/graphql/index.js.map +1 -0
  334. package/dist/src/kernel/packs/graphql/nameMap.js +63 -0
  335. package/dist/src/kernel/packs/graphql/nameMap.js.map +1 -0
  336. package/dist/src/kernel/packs/index.js +34 -0
  337. package/dist/src/kernel/packs/index.js.map +1 -0
  338. package/dist/src/kernel/packs/iri.js +110 -0
  339. package/dist/src/kernel/packs/iri.js.map +1 -0
  340. package/dist/src/kernel/packs/renderPack.js +129 -0
  341. package/dist/src/kernel/packs/renderPack.js.map +1 -0
  342. package/dist/src/kernel/packs/resolveEntity.js +223 -0
  343. package/dist/src/kernel/packs/resolveEntity.js.map +1 -0
  344. package/dist/src/kernel/packs/runBodies.js +87 -0
  345. package/dist/src/kernel/packs/runBodies.js.map +1 -0
  346. package/dist/src/kernel/packs/sample.js +56 -0
  347. package/dist/src/kernel/packs/sample.js.map +1 -0
  348. package/dist/src/kernel/packs/schema.js +369 -0
  349. package/dist/src/kernel/packs/schema.js.map +1 -0
  350. package/dist/src/kernel/packs/sparql/applyFilters.js +68 -0
  351. package/dist/src/kernel/packs/sparql/applyFilters.js.map +1 -0
  352. package/dist/src/kernel/packs/sparql/applySearch.js +25 -0
  353. package/dist/src/kernel/packs/sparql/applySearch.js.map +1 -0
  354. package/dist/src/kernel/packs/sparql/buildLookupQuery.js +217 -0
  355. package/dist/src/kernel/packs/sparql/buildLookupQuery.js.map +1 -0
  356. package/dist/src/kernel/packs/sparql/escape.js +21 -0
  357. package/dist/src/kernel/packs/sparql/escape.js.map +1 -0
  358. package/dist/src/kernel/packs/sparql/index.js +31 -0
  359. package/dist/src/kernel/packs/sparql/index.js.map +1 -0
  360. package/dist/src/kernel/packs/sparql/runSelect.js +101 -0
  361. package/dist/src/kernel/packs/sparql/runSelect.js.map +1 -0
  362. package/dist/src/kernel/packs/types.js +30 -0
  363. package/dist/src/kernel/packs/types.js.map +1 -0
  364. package/dist/src/kernel/packs/uniqueness.js +30 -0
  365. package/dist/src/kernel/packs/uniqueness.js.map +1 -0
  366. package/dist/src/kernel/project/cli/buildProgram.js +263 -0
  367. package/dist/src/kernel/project/cli/buildProgram.js.map +1 -0
  368. package/dist/src/kernel/project/cli/constants.js +27 -0
  369. package/dist/src/kernel/project/cli/constants.js.map +1 -0
  370. package/dist/src/kernel/project/cli/dispatch.js +420 -0
  371. package/dist/src/kernel/project/cli/dispatch.js.map +1 -0
  372. package/dist/src/kernel/project/cli/exitCodes.js +66 -0
  373. package/dist/src/kernel/project/cli/exitCodes.js.map +1 -0
  374. package/dist/src/kernel/project/cli/globalFlags.js +216 -0
  375. package/dist/src/kernel/project/cli/globalFlags.js.map +1 -0
  376. package/dist/src/kernel/project/cli/helpFormat.js +59 -0
  377. package/dist/src/kernel/project/cli/helpFormat.js.map +1 -0
  378. package/dist/src/kernel/project/cli/index.js +14 -0
  379. package/dist/src/kernel/project/cli/index.js.map +1 -0
  380. package/dist/src/kernel/project/cli/rootHelp.js +197 -0
  381. package/dist/src/kernel/project/cli/rootHelp.js.map +1 -0
  382. package/dist/src/kernel/project/cli/suggest.js +75 -0
  383. package/dist/src/kernel/project/cli/suggest.js.map +1 -0
  384. package/dist/src/kernel/project/cli/suggestNames.js +96 -0
  385. package/dist/src/kernel/project/cli/suggestNames.js.map +1 -0
  386. package/dist/src/kernel/project/cli/verbHelp.js +131 -0
  387. package/dist/src/kernel/project/cli/verbHelp.js.map +1 -0
  388. package/dist/src/kernel/project/mcp/buildServer.js +67 -0
  389. package/dist/src/kernel/project/mcp/buildServer.js.map +1 -0
  390. package/dist/src/kernel/project/mcp/envelope.js +37 -0
  391. package/dist/src/kernel/project/mcp/envelope.js.map +1 -0
  392. package/dist/src/kernel/project/mcp/index.js +8 -0
  393. package/dist/src/kernel/project/mcp/index.js.map +1 -0
  394. package/dist/src/kernel/project/mcp/instructions.js +44 -0
  395. package/dist/src/kernel/project/mcp/instructions.js.map +1 -0
  396. package/dist/src/kernel/project/mcp/mcpError.js +42 -0
  397. package/dist/src/kernel/project/mcp/mcpError.js.map +1 -0
  398. package/dist/src/kernel/project/mcp/prompts/index.js +36 -0
  399. package/dist/src/kernel/project/mcp/prompts/index.js.map +1 -0
  400. package/dist/src/kernel/project/mcp/prompts/provider.js +98 -0
  401. package/dist/src/kernel/project/mcp/prompts/provider.js.map +1 -0
  402. package/dist/src/kernel/project/mcp/prompts/source.js +154 -0
  403. package/dist/src/kernel/project/mcp/prompts/source.js.map +1 -0
  404. package/dist/src/kernel/project/mcp/registerVerb.js +300 -0
  405. package/dist/src/kernel/project/mcp/registerVerb.js.map +1 -0
  406. package/dist/src/kernel/project/mcp/serve.js +35 -0
  407. package/dist/src/kernel/project/mcp/serve.js.map +1 -0
  408. package/dist/src/kernel/render/compactUri.js +24 -0
  409. package/dist/src/kernel/render/compactUri.js.map +1 -0
  410. package/dist/src/kernel/render/contracts.js +10 -0
  411. package/dist/src/kernel/render/contracts.js.map +1 -0
  412. package/dist/src/kernel/render/disclosure.js +34 -0
  413. package/dist/src/kernel/render/disclosure.js.map +1 -0
  414. package/dist/src/kernel/render/envelope.js +30 -0
  415. package/dist/src/kernel/render/envelope.js.map +1 -0
  416. package/dist/src/kernel/render/formatters.js +27 -0
  417. package/dist/src/kernel/render/formatters.js.map +1 -0
  418. package/dist/src/kernel/render/index.js +15 -0
  419. package/dist/src/kernel/render/index.js.map +1 -0
  420. package/dist/src/kernel/render/prefixes.js +46 -0
  421. package/dist/src/kernel/render/prefixes.js.map +1 -0
  422. package/dist/src/kernel/render/renderers.js +468 -0
  423. package/dist/src/kernel/render/renderers.js.map +1 -0
  424. package/dist/src/kernel/render/style.js +85 -0
  425. package/dist/src/kernel/render/style.js.map +1 -0
  426. package/dist/src/kernel/render/turtle.js +194 -0
  427. package/dist/src/kernel/render/turtle.js.map +1 -0
  428. package/dist/src/kernel/render/writeStdout.js +43 -0
  429. package/dist/src/kernel/render/writeStdout.js.map +1 -0
  430. package/dist/src/kernel/runtime/boot.js +49 -0
  431. package/dist/src/kernel/runtime/boot.js.map +1 -0
  432. package/dist/src/kernel/runtime/facade.js +39 -0
  433. package/dist/src/kernel/runtime/facade.js.map +1 -0
  434. package/dist/src/kernel/runtime/graphpack/build.js +122 -0
  435. package/dist/src/kernel/runtime/graphpack/build.js.map +1 -0
  436. package/dist/src/kernel/runtime/graphpack/buildIndex.js +303 -0
  437. package/dist/src/kernel/runtime/graphpack/buildIndex.js.map +1 -0
  438. package/dist/src/kernel/runtime/graphpack/embedded/pack.generated.js +6 -0
  439. package/dist/src/kernel/runtime/graphpack/embedded/pack.generated.js.map +1 -0
  440. package/dist/src/kernel/runtime/graphpack/embedded/pack.index.generated.js +4 -0
  441. package/dist/src/kernel/runtime/graphpack/embedded/pack.index.generated.js.map +1 -0
  442. package/dist/src/kernel/runtime/graphpack/embedded/pack.stories.generated.js +4 -0
  443. package/dist/src/kernel/runtime/graphpack/embedded/pack.stories.generated.js.map +1 -0
  444. package/dist/src/kernel/runtime/graphpack/embedded.js +82 -0
  445. package/dist/src/kernel/runtime/graphpack/embedded.js.map +1 -0
  446. package/dist/src/kernel/runtime/graphpack/hash.js +38 -0
  447. package/dist/src/kernel/runtime/graphpack/hash.js.map +1 -0
  448. package/dist/src/kernel/runtime/graphpack/index.js +11 -0
  449. package/dist/src/kernel/runtime/graphpack/index.js.map +1 -0
  450. package/dist/src/kernel/runtime/graphpack/manifest.js +145 -0
  451. package/dist/src/kernel/runtime/graphpack/manifest.js.map +1 -0
  452. package/dist/src/kernel/runtime/graphpack/read.js +92 -0
  453. package/dist/src/kernel/runtime/graphpack/read.js.map +1 -0
  454. package/dist/src/kernel/runtime/graphpack/schemas.js +70 -0
  455. package/dist/src/kernel/runtime/graphpack/schemas.js.map +1 -0
  456. package/dist/src/kernel/runtime/graphpack/stories.js +68 -0
  457. package/dist/src/kernel/runtime/graphpack/stories.js.map +1 -0
  458. package/dist/src/kernel/runtime/graphpack/types.js +55 -0
  459. package/dist/src/kernel/runtime/graphpack/types.js.map +1 -0
  460. package/dist/src/kernel/runtime/index.js +8 -0
  461. package/dist/src/kernel/runtime/index.js.map +1 -0
  462. package/dist/src/kernel/runtime/loadSession.js +34 -0
  463. package/dist/src/kernel/runtime/loadSession.js.map +1 -0
  464. package/dist/src/kernel/runtime/paths.js +78 -0
  465. package/dist/src/kernel/runtime/paths.js.map +1 -0
  466. package/dist/src/kernel/runtime/probe.js +37 -0
  467. package/dist/src/kernel/runtime/probe.js.map +1 -0
  468. package/dist/src/kernel/runtime/readEntity.js +363 -0
  469. package/dist/src/kernel/runtime/readEntity.js.map +1 -0
  470. package/dist/src/kernel/runtime/refs/gitOps.js +65 -0
  471. package/dist/src/kernel/runtime/refs/gitOps.js.map +1 -0
  472. package/dist/src/kernel/runtime/refs/index.js +29 -0
  473. package/dist/src/kernel/runtime/refs/index.js.map +1 -0
  474. package/dist/src/kernel/runtime/refs/parseRef.js +70 -0
  475. package/dist/src/kernel/runtime/refs/parseRef.js.map +1 -0
  476. package/dist/src/kernel/runtime/refs/resolve.js +332 -0
  477. package/dist/src/kernel/runtime/refs/resolve.js.map +1 -0
  478. package/dist/src/kernel/runtime/resolveSources.js +62 -0
  479. package/dist/src/kernel/runtime/resolveSources.js.map +1 -0
  480. package/dist/src/kernel/runtime/store.js +75 -0
  481. package/dist/src/kernel/runtime/store.js.map +1 -0
  482. package/dist/src/kernel/runtime/storeReadiness.js +51 -0
  483. package/dist/src/kernel/runtime/storeReadiness.js.map +1 -0
  484. package/dist/src/kernel/runtime/types.js +11 -0
  485. package/dist/src/kernel/runtime/types.js.map +1 -0
  486. package/dist/src/kernel/spec/asVerb.js +21 -0
  487. package/dist/src/kernel/spec/asVerb.js.map +1 -0
  488. package/dist/src/kernel/spec/emitReference.js +578 -0
  489. package/dist/src/kernel/spec/emitReference.js.map +1 -0
  490. package/dist/src/kernel/spec/emitSurface.js +202 -0
  491. package/dist/src/kernel/spec/emitSurface.js.map +1 -0
  492. package/dist/src/kernel/spec/index.js +11 -0
  493. package/dist/src/kernel/spec/index.js.map +1 -0
  494. package/dist/src/kernel/spec/surfaceConformance.js +103 -0
  495. package/dist/src/kernel/spec/surfaceConformance.js.map +1 -0
  496. package/dist/src/kernel/spec/types.js +14 -0
  497. package/dist/src/kernel/spec/types.js.map +1 -0
  498. package/dist/src/kernel/spec/validate.js +110 -0
  499. package/dist/src/kernel/spec/validate.js.map +1 -0
  500. package/dist/src/kernel/vocabulary.js +97 -0
  501. package/dist/src/kernel/vocabulary.js.map +1 -0
  502. package/dist/src/types.js +11 -0
  503. package/dist/src/types.js.map +1 -0
  504. package/package.json +25 -24
  505. package/pragma.conf.ts +853 -0
  506. package/src/bin.ts +276 -82
  507. package/src/capabilities/block.parity.test.ts +253 -0
  508. package/src/capabilities/capabilities/capabilities.render.ts +4 -3
  509. package/src/capabilities/capabilities/capabilities.verb.ts +32 -14
  510. package/src/capabilities/capabilities/catalog.ts +20 -12
  511. package/src/capabilities/capabilities/hints.ts +43 -24
  512. package/src/capabilities/capabilities/index.ts +1 -1
  513. package/src/capabilities/colophon/collectColophon.ts +50 -24
  514. package/src/capabilities/colophon/colophon.render.ts +25 -1
  515. package/src/capabilities/colophon/colophon.test.ts +42 -21
  516. package/src/capabilities/colophon/colophon.verb.ts +9 -5
  517. package/src/capabilities/colophon/types.ts +16 -7
  518. package/src/capabilities/config/collectConfigShow.test.ts +58 -0
  519. package/src/capabilities/config/collectConfigShow.ts +43 -6
  520. package/src/capabilities/config/field.render.ts +4 -5
  521. package/src/capabilities/config/fields.ts +17 -37
  522. package/src/capabilities/config/get.verb.ts +70 -0
  523. package/src/capabilities/config/getUnset.test.ts +264 -0
  524. package/src/capabilities/config/index.ts +5 -1
  525. package/src/capabilities/config/runField.ts +40 -22
  526. package/src/capabilities/config/runGet.ts +34 -0
  527. package/src/capabilities/config/runSet.ts +35 -9
  528. package/src/capabilities/config/set.test.ts +48 -14
  529. package/src/capabilities/config/set.verb.ts +14 -9
  530. package/src/capabilities/config/show.render.test.ts +77 -5
  531. package/src/capabilities/config/show.render.ts +8 -13
  532. package/src/capabilities/config/show.verb.ts +13 -9
  533. package/src/capabilities/config/types.ts +26 -4
  534. package/src/capabilities/config/unset.verb.ts +58 -0
  535. package/src/capabilities/create/byteEquality.subprocess.test.ts +2 -3
  536. package/src/capabilities/create/byteEquality.test.ts +95 -103
  537. package/src/capabilities/create/cliProjection.ts +227 -0
  538. package/src/capabilities/create/constants.ts +50 -0
  539. package/src/capabilities/create/create.mcp.test.ts +91 -7
  540. package/src/capabilities/create/create.render.ts +71 -7
  541. package/src/capabilities/create/create.test.ts +523 -162
  542. package/src/capabilities/create/create.verb.ts +361 -321
  543. package/src/capabilities/create/createGrammar.test.ts +865 -0
  544. package/src/capabilities/create/createHelp.ts +71 -0
  545. package/src/capabilities/create/createSurface.generated.ts +400 -0
  546. package/src/capabilities/create/crossCli.subprocess.test.ts +745 -0
  547. package/src/capabilities/create/cwdJail.test.ts +24 -5
  548. package/src/capabilities/create/generatorToVerbSpec.test.ts +186 -0
  549. package/src/capabilities/create/generatorToVerbSpec.ts +13 -11
  550. package/src/capabilities/create/index.ts +9 -14
  551. package/src/capabilities/create/messages.ts +18 -0
  552. package/src/capabilities/create/mount.ts +391 -0
  553. package/src/capabilities/create/pathJail.test.ts +90 -0
  554. package/src/capabilities/create/pathJail.ts +8 -5
  555. package/src/capabilities/create/pickGenerator.ts +40 -46
  556. package/src/capabilities/create/shippedCreate.subprocess.test.ts +164 -0
  557. package/src/capabilities/create/types.ts +29 -5
  558. package/src/capabilities/{standard/parity.test.ts → distribution.parity.test.ts} +24 -14
  559. package/src/capabilities/distribution.test.ts +122 -0
  560. package/src/capabilities/distribution.ts +54 -0
  561. package/src/capabilities/doctor/checks/checkKeStore.ts +9 -7
  562. package/src/capabilities/doctor/checks/checkPackageRefs.ts +82 -46
  563. package/src/capabilities/doctor/checks/checkPragmaVersion.ts +5 -4
  564. package/src/capabilities/doctor/checks/checkShellCompletions.test.ts +125 -30
  565. package/src/capabilities/doctor/checks/checkShellCompletions.ts +113 -27
  566. package/src/capabilities/doctor/checks/index.ts +27 -0
  567. package/src/capabilities/doctor/checks/mcpCommand.test.ts +100 -0
  568. package/src/capabilities/doctor/checks/mcpCommand.ts +69 -0
  569. package/src/capabilities/doctor/checks/targetHealth.ts +274 -0
  570. package/src/capabilities/doctor/doctor.render.test.ts +24 -13
  571. package/src/capabilities/doctor/doctor.render.ts +45 -20
  572. package/src/capabilities/doctor/doctor.test.ts +158 -40
  573. package/src/capabilities/doctor/doctor.verb.ts +9 -5
  574. package/src/capabilities/doctor/runChecks.ts +65 -66
  575. package/src/capabilities/doctor/types.ts +21 -5
  576. package/src/capabilities/emptyRecovery.test.ts +44 -22
  577. package/src/capabilities/graph/examples.exec.test.ts +107 -0
  578. package/src/capabilities/graph/index.ts +1 -1
  579. package/src/capabilities/graph/inspect.render.ts +70 -22
  580. package/src/capabilities/graph/inspect.test.ts +260 -5
  581. package/src/capabilities/graph/inspect.verb.ts +18 -5
  582. package/src/capabilities/graph/query.render.ts +15 -3
  583. package/src/capabilities/graph/query.verb.ts +5 -4
  584. package/src/capabilities/graph/runQuery.ts +9 -7
  585. package/src/capabilities/index.ts +25 -19
  586. package/src/capabilities/info/collectInfo.ts +15 -10
  587. package/src/capabilities/info/info.render.ts +4 -3
  588. package/src/capabilities/info/info.verb.ts +4 -3
  589. package/src/capabilities/lazy.test.ts +125 -2
  590. package/src/capabilities/meta/complete.verb.ts +1 -1
  591. package/src/capabilities/meta/index.ts +1 -1
  592. package/src/capabilities/meta/mcp.verb.ts +10 -8
  593. package/src/capabilities/ontology/index.ts +15 -11
  594. package/src/capabilities/ontology/ontology.test.ts +6 -55
  595. package/src/capabilities/ontology/queries.ts +2 -2
  596. package/src/capabilities/ontology/render.ts +3 -4
  597. package/src/capabilities/ontology/verbs.ts +42 -83
  598. package/src/capabilities/prompt/index.ts +2 -2
  599. package/src/capabilities/prompt/prompt.render.ts +6 -2
  600. package/src/capabilities/prompt/prompt.test.ts +12 -6
  601. package/src/capabilities/prompt/types.ts +1 -1
  602. package/src/capabilities/prompt/verbs.ts +26 -14
  603. package/src/capabilities/reference.test.ts +63 -0
  604. package/src/capabilities/resources/provider.ts +367 -91
  605. package/src/capabilities/resources/resources.test.ts +411 -59
  606. package/src/capabilities/setup/buildPlan.ts +236 -0
  607. package/src/capabilities/setup/index.ts +7 -7
  608. package/src/capabilities/setup/mode.ts +74 -0
  609. package/src/capabilities/setup/operations/index.ts +74 -0
  610. package/src/capabilities/setup/operations/setupCompletions.ts +97 -24
  611. package/src/capabilities/setup/operations/setupConfig.ts +124 -0
  612. package/src/capabilities/setup/operations/setupGenerator.ts +397 -320
  613. package/src/capabilities/setup/operations/setupLsp.ts +329 -68
  614. package/src/capabilities/setup/operations/setupMcp.ts +137 -97
  615. package/src/capabilities/setup/operations/setupSkills.ts +336 -108
  616. package/src/capabilities/setup/plan.render.ts +241 -0
  617. package/src/capabilities/setup/plan.ts +236 -0
  618. package/src/capabilities/setup/setup.mcp.correctness.test.ts +432 -0
  619. package/src/capabilities/setup/setup.render.test.ts +281 -73
  620. package/src/capabilities/setup/setup.render.ts +45 -77
  621. package/src/capabilities/setup/setup.test.ts +972 -209
  622. package/src/capabilities/setup/setup.verb.ts +233 -74
  623. package/src/capabilities/setup/shell.test.ts +86 -0
  624. package/src/capabilities/setup/shell.ts +166 -14
  625. package/src/capabilities/setup/targets.ts +367 -0
  626. package/src/capabilities/setup/types.ts +18 -57
  627. package/src/capabilities/shared/assertExecOk.ts +68 -10
  628. package/src/capabilities/shared/index.ts +32 -0
  629. package/src/capabilities/shared/packageManager.test.ts +55 -0
  630. package/src/capabilities/shared/packageManager.ts +62 -6
  631. package/src/capabilities/skill/discover.ts +47 -9
  632. package/src/capabilities/skill/index.ts +1 -1
  633. package/src/capabilities/skill/render.ts +14 -14
  634. package/src/capabilities/skill/skill.test.ts +7 -5
  635. package/src/capabilities/skill/verbs.ts +9 -10
  636. package/src/capabilities/sources/collectStatus.ts +61 -51
  637. package/src/capabilities/sources/index.ts +2 -2
  638. package/src/capabilities/sources/installSkills.ts +86 -9
  639. package/src/capabilities/sources/runUpdate.ts +164 -141
  640. package/src/capabilities/sources/sources.test.ts +265 -119
  641. package/src/capabilities/sources/status.render.test.ts +53 -24
  642. package/src/capabilities/sources/status.render.ts +25 -30
  643. package/src/capabilities/sources/status.verb.ts +16 -9
  644. package/src/capabilities/sources/types.ts +18 -25
  645. package/src/capabilities/sources/update.render.ts +10 -3
  646. package/src/capabilities/sources/update.verb.ts +13 -24
  647. package/src/capabilities/surface.test.ts +165 -25
  648. package/src/capabilities/tier.parity.test.ts +144 -0
  649. package/src/capabilities/upgrade/runUpgrade.ts +8 -8
  650. package/src/capabilities/upgrade/upgrade.render.ts +4 -5
  651. package/src/capabilities/upgrade/upgrade.test.ts +3 -3
  652. package/src/capabilities/upgrade/upgrade.verb.ts +8 -4
  653. package/src/capabilities/version/index.ts +12 -0
  654. package/src/capabilities/version/version.test.ts +66 -0
  655. package/src/capabilities/version/version.verb.ts +39 -0
  656. package/src/constants.test.ts +8 -6
  657. package/src/constants.ts +78 -22
  658. package/src/identity.test.ts +348 -0
  659. package/src/index.ts +2 -2
  660. package/src/kernel/completion/__snapshots__/emitScripts.test.ts.snap +20 -15
  661. package/src/kernel/completion/complete.test.ts +12 -2
  662. package/src/kernel/completion/complete.ts +1 -1
  663. package/src/kernel/completion/emitScripts.test.ts +1 -1
  664. package/src/kernel/completion/emitScripts.ts +5 -3
  665. package/src/kernel/completion/entitySource.test.ts +185 -41
  666. package/src/kernel/completion/entitySource.ts +133 -110
  667. package/src/kernel/completion/model.test.ts +165 -13
  668. package/src/kernel/completion/model.ts +105 -14
  669. package/src/kernel/completion/parse.test.ts +4 -7
  670. package/src/kernel/completion/parse.ts +15 -0
  671. package/src/kernel/completion/resolve.test.ts +0 -1
  672. package/src/kernel/completion/safety.test.ts +104 -50
  673. package/src/kernel/completion/shellDrive.test.ts +611 -0
  674. package/src/kernel/completion/templates/bash.ts +7 -2
  675. package/src/kernel/completion/templates/fish.ts +28 -6
  676. package/src/kernel/completion/templates/index.ts +23 -0
  677. package/src/kernel/completion/types.ts +10 -3
  678. package/src/kernel/config/defaults.test.ts +108 -0
  679. package/src/kernel/config/defaults.ts +13 -23
  680. package/src/kernel/config/evaluateProjectConfig.ts +33 -11
  681. package/src/kernel/config/findProjectConfig.ts +4 -3
  682. package/src/kernel/config/firstRun.test.ts +44 -38
  683. package/src/kernel/config/firstRun.ts +24 -81
  684. package/src/kernel/config/globalConfig.ts +2 -2
  685. package/src/kernel/config/index.ts +5 -6
  686. package/src/kernel/config/paths.ts +17 -17
  687. package/src/kernel/config/readConfig.test.ts +246 -7
  688. package/src/kernel/config/readConfig.ts +8 -8
  689. package/src/kernel/config/schema.test.ts +50 -0
  690. package/src/kernel/config/schema.ts +100 -11
  691. package/src/kernel/config/types.ts +75 -19
  692. package/src/kernel/copy.test.ts +483 -0
  693. package/src/kernel/copy.ts +54 -0
  694. package/src/kernel/error/PragmaError.test.ts +6 -3
  695. package/src/kernel/error/PragmaError.ts +4 -2
  696. package/src/kernel/error/constants.ts +7 -4
  697. package/src/kernel/error/fromTaskError.ts +28 -6
  698. package/src/kernel/error/index.ts +1 -1
  699. package/src/kernel/error/recovery.test.ts +22 -45
  700. package/src/kernel/error/recovery.ts +36 -27
  701. package/src/kernel/error/renderError.ts +42 -1
  702. package/src/kernel/error/types.ts +25 -4
  703. package/src/kernel/index.ts +26 -0
  704. package/src/kernel/packs/collect.test.ts +195 -11
  705. package/src/kernel/packs/collect.ts +212 -61
  706. package/src/kernel/packs/compile.test.ts +293 -8
  707. package/src/kernel/packs/compile.ts +108 -15
  708. package/src/kernel/packs/disclosure.test.ts +5 -2
  709. package/src/kernel/packs/disclosure.ts +2 -2
  710. package/src/kernel/packs/graphql/buildLookupDocument.ts +1 -1
  711. package/src/kernel/packs/graphql/fetchGraphqlLookup.ts +13 -13
  712. package/src/kernel/packs/graphql/index.ts +20 -0
  713. package/src/kernel/packs/graphqlSource.test.ts +6 -3
  714. package/src/kernel/packs/index.ts +40 -0
  715. package/src/kernel/packs/iri.test.ts +73 -0
  716. package/src/kernel/packs/iri.ts +70 -20
  717. package/src/kernel/packs/renderPack.ts +28 -7
  718. package/src/kernel/packs/resolveEntity.test.ts +213 -0
  719. package/src/kernel/packs/resolveEntity.ts +132 -34
  720. package/src/kernel/packs/runBodies.ts +16 -16
  721. package/src/kernel/packs/sample.ts +1 -1
  722. package/src/kernel/packs/schema.ts +114 -11
  723. package/src/kernel/packs/sparql/applyFilters.ts +16 -17
  724. package/src/kernel/packs/sparql/buildLookupQuery.ts +75 -3
  725. package/src/kernel/packs/sparql/index.ts +37 -0
  726. package/src/kernel/packs/sparql/runSelect.test.ts +104 -10
  727. package/src/kernel/packs/sparql/runSelect.ts +75 -37
  728. package/src/kernel/packs/toolDescription.test.ts +52 -5
  729. package/src/kernel/packs/types.ts +52 -4
  730. package/src/kernel/packs/uniqueness.ts +5 -5
  731. package/src/kernel/project/cli/buildProgram.test.ts +157 -17
  732. package/src/kernel/project/cli/buildProgram.ts +77 -18
  733. package/src/kernel/project/cli/constants.ts +34 -0
  734. package/src/kernel/project/cli/dispatch.test.ts +227 -3
  735. package/src/kernel/project/cli/dispatch.ts +250 -48
  736. package/src/kernel/project/cli/exitCodes.ts +1 -1
  737. package/src/kernel/project/cli/globalFlags.test.ts +117 -1
  738. package/src/kernel/project/cli/globalFlags.ts +130 -30
  739. package/src/kernel/project/cli/helpFormat.ts +14 -1
  740. package/src/kernel/project/cli/rootHelp.ts +140 -111
  741. package/src/kernel/project/cli/suggest.test.ts +33 -1
  742. package/src/kernel/project/cli/suggest.ts +25 -1
  743. package/src/kernel/project/cli/verbHelp.test.ts +30 -0
  744. package/src/kernel/project/cli/verbHelp.ts +23 -7
  745. package/src/kernel/project/mcp/buildServer.ts +18 -10
  746. package/src/kernel/project/mcp/envelope.ts +2 -2
  747. package/src/kernel/project/mcp/instructions.ts +12 -7
  748. package/src/kernel/project/mcp/mcpError.ts +1 -1
  749. package/src/kernel/project/mcp/parity.test.ts +42 -1
  750. package/src/kernel/project/mcp/prompts/index.ts +37 -0
  751. package/src/kernel/project/mcp/prompts/prompts.test.ts +120 -5
  752. package/src/kernel/project/mcp/prompts/provider.ts +22 -8
  753. package/src/kernel/project/mcp/prompts/source.ts +69 -68
  754. package/src/kernel/project/mcp/registerVerb.ts +56 -11
  755. package/src/kernel/project/mcp/serve.ts +13 -5
  756. package/src/kernel/project/mcp/toolDescriptions.test.ts +6 -2
  757. package/src/kernel/render/__snapshots__/renderers.test.ts.snap +7 -4
  758. package/src/kernel/render/contracts.ts +21 -0
  759. package/src/kernel/render/disclosure.ts +4 -2
  760. package/src/kernel/render/envelope.ts +2 -3
  761. package/src/kernel/render/formatters.ts +1 -1
  762. package/src/kernel/render/prefixes.test.ts +36 -0
  763. package/src/kernel/render/prefixes.ts +40 -13
  764. package/src/kernel/render/renderers.test.ts +244 -7
  765. package/src/kernel/render/renderers.ts +222 -20
  766. package/src/kernel/render/style.test.ts +75 -24
  767. package/src/kernel/render/style.ts +31 -1
  768. package/src/kernel/render/turtle.ts +222 -0
  769. package/src/kernel/runtime/graphpack/build.ts +29 -3
  770. package/src/kernel/runtime/graphpack/buildIndex.test.ts +46 -0
  771. package/src/kernel/runtime/graphpack/buildIndex.ts +53 -18
  772. package/src/kernel/runtime/graphpack/embedded/pack.generated.ts +5 -5
  773. package/src/kernel/runtime/graphpack/embedded/pack.index.generated.ts +3 -3
  774. package/src/kernel/runtime/graphpack/embedded/pack.stories.generated.ts +3 -0
  775. package/src/kernel/runtime/graphpack/embedded.ts +39 -13
  776. package/src/kernel/runtime/graphpack/graphpack.test.ts +205 -5
  777. package/src/kernel/runtime/graphpack/hash.ts +3 -2
  778. package/src/kernel/runtime/graphpack/index.ts +1 -1
  779. package/src/kernel/runtime/graphpack/manifest.ts +114 -12
  780. package/src/kernel/runtime/graphpack/read.ts +5 -11
  781. package/src/kernel/runtime/graphpack/schemas.ts +74 -0
  782. package/src/kernel/runtime/graphpack/stories.ts +84 -0
  783. package/src/kernel/runtime/graphpack/types.ts +61 -48
  784. package/src/kernel/runtime/loadSession.ts +1 -1
  785. package/src/kernel/runtime/paths.ts +64 -15
  786. package/src/kernel/runtime/probe.test.ts +8 -4
  787. package/src/kernel/runtime/probe.ts +6 -6
  788. package/src/kernel/runtime/readEntity.ts +477 -32
  789. package/src/kernel/runtime/refs/gitOps.ts +1 -19
  790. package/src/kernel/runtime/refs/index.ts +39 -0
  791. package/src/kernel/runtime/refs/parseRef.ts +8 -8
  792. package/src/kernel/runtime/refs/resolve.ts +82 -66
  793. package/src/kernel/runtime/resolveSources.test.ts +63 -24
  794. package/src/kernel/runtime/resolveSources.ts +36 -25
  795. package/src/kernel/runtime/storeReadiness.ts +10 -10
  796. package/src/kernel/runtime/storeless.test.ts +1 -1
  797. package/src/kernel/runtime/types.ts +15 -1
  798. package/src/kernel/runtime/wasmEmbed.test.ts +56 -55
  799. package/src/kernel/spec/emitReference.test.ts +3 -3
  800. package/src/kernel/spec/emitReference.ts +242 -34
  801. package/src/kernel/spec/emitSurface.ts +57 -11
  802. package/src/kernel/spec/surfaceConformance.test.ts +4 -2
  803. package/src/kernel/spec/surfaceConformance.ts +8 -0
  804. package/src/kernel/spec/types.ts +277 -13
  805. package/src/kernel/spec/validate.ts +2 -1
  806. package/src/kernel/vocabulary.test.ts +164 -0
  807. package/src/kernel/vocabulary.ts +146 -0
  808. package/src/testing/PARITY_GAPS.ts +8 -2
  809. package/src/testing/behavioral/README.md +3 -3
  810. package/src/testing/behavioral/completion.test.ts +19 -6
  811. package/src/testing/behavioral/docExamples.test.ts +14 -0
  812. package/src/testing/behavioral/errorMatrix.mcp.test.ts +6 -5
  813. package/src/testing/behavioral/errorMatrix.storeless.test.ts +1 -1
  814. package/src/testing/behavioral/exitCodes.e2e.test.ts +6 -5
  815. package/src/testing/behavioral/firstRun.e2e.test.ts +24 -24
  816. package/src/testing/behavioral/harness.foundation.test.ts +14 -5
  817. package/src/testing/behavioral/journeys.cli.test.ts +117 -76
  818. package/src/testing/behavioral/journeys.defaultPack.test.ts +88 -182
  819. package/src/testing/behavioral/journeys.firstInstall.test.ts +167 -0
  820. package/src/testing/behavioral/journeys.implementation.test.ts +206 -0
  821. package/src/testing/behavioral/journeys.interactiveTty.e2e.test.ts +2 -2
  822. package/src/testing/behavioral/journeys.mcpLifecycle.mcp.test.ts +9 -6
  823. package/src/testing/behavioral/journeys.packageStories.test.ts +188 -0
  824. package/src/testing/behavioral/liveReadSurface.ts +4 -3
  825. package/src/testing/behavioral/mcpServe.e2e.test.ts +57 -2
  826. package/src/testing/behavioral/mcpSurface.test.ts +10 -5
  827. package/src/testing/behavioral/rootCli.e2e.test.ts +26 -2
  828. package/src/testing/behavioral/staleCommands.test.ts +10 -0
  829. package/src/testing/eval/__snapshots__/eval.test.ts.snap +7 -7
  830. package/src/testing/eval/cases/assembly.ts +13 -6
  831. package/src/testing/eval/cases/effectful.ts +28 -15
  832. package/src/testing/eval/cases/readNouns.ts +22 -20
  833. package/src/testing/eval/cases/stable.ts +5 -1
  834. package/src/testing/eval/eval.test.ts +7 -1
  835. package/src/testing/fixtures/blockGraph.ts +79 -0
  836. package/src/testing/fixtures/fixtureCapability.ts +45 -1
  837. package/src/testing/fixtures/graph/canonical.ts +94 -5
  838. package/src/testing/fixtures/graph/defaultPack.ts +7 -6
  839. package/src/testing/fixtures/graph/owlExport.ts +7 -5
  840. package/src/testing/helpers/blankCanonicalRanges.ts +36 -0
  841. package/src/testing/helpers/fixtureGraph.ts +33 -16
  842. package/src/testing/helpers/packRuntime.ts +2 -3
  843. package/src/testing/helpers/projectCli.ts +2 -1
  844. package/src/testing/helpers/projectMcp.ts +38 -6
  845. package/src/testing/helpers/runCli.ts +35 -18
  846. package/src/testing/perf/budgets.test.ts +24 -9
  847. package/src/testing/perf/budgets.ts +76 -12
  848. package/src/testing/perf/codegen.test.ts +126 -0
  849. package/src/testing/perf/globalSetup.test.ts +286 -0
  850. package/src/testing/perf/globalSetup.ts +611 -14
  851. package/src/testing/perf/measure.test.ts +38 -0
  852. package/src/testing/perf/measure.ts +23 -4
  853. package/src/testing/setupXdgIsolation.ts +81 -21
  854. package/src/testing/tempRoot.globalSetup.ts +122 -0
  855. package/src/types.ts +2 -2
  856. package/dist/pragma +0 -0
  857. package/src/capabilities/block/blockList.test.ts +0 -79
  858. package/src/capabilities/block/blockList.verb.ts +0 -163
  859. package/src/capabilities/block/index.ts +0 -25
  860. package/src/capabilities/block/pack.ts +0 -166
  861. package/src/capabilities/block/parity.test.ts +0 -187
  862. package/src/capabilities/block/tierChain.ts +0 -78
  863. package/src/capabilities/colophon/pragmaColophon.ts +0 -66
  864. package/src/capabilities/create/compiledCreate.subprocess.test.ts +0 -152
  865. package/src/capabilities/create/templates.embedded.generated.ts +0 -53
  866. package/src/capabilities/doctor/checks/checkConfigFile.ts +0 -41
  867. package/src/capabilities/doctor/checks/checkMcpCommands.ts +0 -114
  868. package/src/capabilities/doctor/checks/checkMcpConfigured.ts +0 -67
  869. package/src/capabilities/doctor/checks/checkSkillsSymlinked.ts +0 -60
  870. package/src/capabilities/doctor/checks/deriveBand.test.ts +0 -42
  871. package/src/capabilities/doctor/checks/deriveBand.ts +0 -28
  872. package/src/capabilities/modifier/index.ts +0 -15
  873. package/src/capabilities/modifier/pack.ts +0 -64
  874. package/src/capabilities/standard/index.ts +0 -18
  875. package/src/capabilities/standard/pack.ts +0 -128
  876. package/src/capabilities/tier/index.ts +0 -19
  877. package/src/capabilities/tier/lookup.render.test.ts +0 -72
  878. package/src/capabilities/tier/lookup.render.ts +0 -59
  879. package/src/capabilities/tier/lookup.test.ts +0 -83
  880. package/src/capabilities/tier/lookup.verb.ts +0 -44
  881. package/src/capabilities/tier/pack.ts +0 -28
  882. package/src/capabilities/tier/runLookup.ts +0 -87
  883. package/src/capabilities/token/addConfig.render.ts +0 -27
  884. package/src/capabilities/token/addConfig.test.ts +0 -62
  885. package/src/capabilities/token/addConfig.verb.ts +0 -40
  886. package/src/capabilities/token/index.ts +0 -19
  887. package/src/capabilities/token/pack.ts +0 -58
  888. package/src/capabilities/token/runAddConfig.ts +0 -95
  889. package/src/kernel/completion/bashDrive.test.ts +0 -73
  890. package/src/kernel/config/__snapshots__/firstRun.test.ts.snap +0 -11
  891. package/src/kernel/runtime/graphpack/embedded/sample.ttl +0 -39
  892. package/src/kernel/runtime/lock.ts +0 -90
@@ -15,18 +15,23 @@
15
15
 
16
16
  import {
17
17
  existsSync,
18
+ lstatSync,
18
19
  mkdirSync,
19
20
  mkdtempSync,
20
21
  readFileSync,
22
+ readlinkSync,
21
23
  rmSync,
24
+ statSync,
25
+ symlinkSync,
22
26
  writeFileSync,
23
27
  } from "node:fs";
24
28
  import { tmpdir } from "node:os";
25
- import { join } from "node:path";
29
+ import { join, relative } from "node:path";
26
30
  import { execute } from "@canonical/summon-core";
27
31
  import { dryRun, type Effect, type Task } from "@canonical/task";
28
- import { runTask } from "@canonical/task/node";
29
- import { afterEach, beforeEach, describe, expect, it } from "vitest";
32
+ import { runTask, runUndo } from "@canonical/task/node";
33
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
34
+ import { BIN_NAME } from "../../constants.js";
30
35
  import { emitScripts } from "../../kernel/completion/emitScripts.js";
31
36
  import { asPragmaError } from "../../kernel/error/fromTaskError.js";
32
37
  import { executeVerb } from "../../kernel/project/cli/dispatch.js";
@@ -36,10 +41,16 @@ import type { VerbSpec } from "../../kernel/spec/types.js";
36
41
  import { projectCli } from "../../testing/helpers/projectCli.js";
37
42
  import { projectMcp } from "../../testing/helpers/projectMcp.js";
38
43
  import { capabilities } from "../index.js";
39
- import { buildSetupPlan } from "./operations/setupGenerator.js";
40
- import { composeSkills, detectSkills } from "./operations/setupSkills.js";
44
+ import { detectCompletions } from "./operations/setupCompletions.js";
45
+ import { buildSetupRun } from "./operations/setupGenerator.js";
46
+ import {
47
+ composeSkills,
48
+ composeSkillsRemoval,
49
+ detectSkills,
50
+ ownedSkillLinks,
51
+ } from "./operations/setupSkills.js";
41
52
  import { setupModule } from "./setup.verb.js";
42
- import { completionScriptPath } from "./shell.js";
53
+ import { completionScriptPath, detectShell, type ShellId } from "./shell.js";
43
54
 
44
55
  const FLAGS: GlobalFlags = {
45
56
  llm: false,
@@ -63,85 +74,72 @@ const tmp = (prefix: string): string => {
63
74
  return dir;
64
75
  };
65
76
 
77
+ /**
78
+ * The shell this test process is actually running in.
79
+ *
80
+ * Detection reads the PROCESS TREE now, not `$SHELL` — `$SHELL` is the login
81
+ * shell and forcing it proved nothing about the shell in use. So the
82
+ * completions cases assert against whatever shell really started the run, and
83
+ * stand down where that cannot be established (a CI runner with no shell
84
+ * ancestor), rather than asserting a shell nobody is in.
85
+ */
86
+ const DETECTED = detectShell();
87
+ const SHELL = DETECTED.kind === "detected" ? DETECTED.shell : null;
88
+ const withShell = it.skipIf(SHELL === null);
89
+
90
+ /**
91
+ * A PATH directory carrying a `pragma` stub. The completions target refuses to
92
+ * install a script whose `pragma __complete` cannot be found — an inert script
93
+ * is worse than none — so every PATH these tests build has to satisfy that.
94
+ */
95
+ const stubPath = (): string => {
96
+ const dir = tmp("pragma-setup-path-");
97
+ writeFileSync(join(dir, "pragma"), "");
98
+ return dir;
99
+ };
100
+
66
101
  let prevHome: string | undefined;
67
- let prevShell: string | undefined;
102
+ let prevPath: string | undefined;
68
103
  beforeEach(() => {
69
104
  prevHome = process.env.HOME;
70
- prevShell = process.env.SHELL;
105
+ prevPath = process.env.PATH;
71
106
  process.env.HOME = tmp("pragma-setup-home-");
72
- process.env.SHELL = "/usr/bin/zsh";
107
+ process.env.PATH = stubPath();
73
108
  });
74
109
  afterEach(() => {
75
110
  process.env.HOME = prevHome;
76
- process.env.SHELL = prevShell;
111
+ process.env.PATH = prevPath;
77
112
  for (const dir of roots) rmSync(dir, { recursive: true, force: true });
78
113
  roots.length = 0;
79
114
  });
80
115
 
81
116
  describe("setup completions", () => {
82
- it("writes exactly emitScripts(capabilities)[shell]", async () => {
83
- const path = completionScriptPath("zsh");
84
- const outcome = await executeVerb(
85
- completionsVerb,
86
- {},
87
- YES,
88
- bootRuntime(FLAGS, tmp("pragma-setup-proj-")),
89
- );
90
- expect(outcome.exitCode).toBe(0);
91
- expect(existsSync(path)).toBe(true);
92
- expect(readFileSync(path, "utf-8")).toBe(emitScripts(capabilities).zsh);
93
- });
117
+ it("sees a Windows `.cmd` shim, so the row does not skip on Windows", async () => {
118
+ // npm installs this CLI as `pragma.cmd` on Windows and never as a bare
119
+ // name, so joining the name onto each PATH directory found nothing: the
120
+ // row skipped with "not on PATH" on a machine where the command runs.
121
+ // Spelled to match a default `PATHEXT` entry exactly: this test runs on a
122
+ // case-sensitive filesystem, while Windows matches `pragma.cmd` against
123
+ // `.CMD` case-insensitively.
124
+ const dir = tmp("pragma-setup-winpath-");
125
+ writeFileSync(join(dir, `${BIN_NAME}.CMD`), "");
94
126
 
95
- it("bakes the completion config (minChars + per-family opt-out) into the emitted script", async () => {
96
- // Proves the reconciled `detectCompletions(cwd)` reads `completion` config
97
- // and threads {minChars, disabledFamilies} into `emitScripts` — a silent
98
- // regression if the fold had dropped autocomplete's config-threading for
99
- // setup's argument-free detect.
100
- const cwd = tmp("pragma-setup-proj-");
101
- writeFileSync(
102
- join(cwd, "pragma.config.ts"),
103
- "export default { completion: { minChars: 5, families: { block: false } } };\n",
104
- );
105
- const path = completionScriptPath("zsh");
106
- const outcome = await executeVerb(
107
- completionsVerb,
108
- {},
109
- YES,
110
- bootRuntime(FLAGS, cwd),
111
- );
112
- expect(outcome.exitCode).toBe(0);
113
- const written = readFileSync(path, "utf-8");
114
- // The installed script is EXACTLY the config-baked emit (minChars 5, the
115
- // `block` family scrubbed) — the config was threaded end to end.
116
- expect(written).toBe(
117
- emitScripts(capabilities, {
118
- minChars: 5,
119
- disabledFamilies: ["block"],
120
- }).zsh,
121
- );
122
- // Both knobs actually moved the output: it differs from the default emit
123
- // (minChars), and from the minChars-only emit (the family opt-out).
124
- expect(written).not.toBe(emitScripts(capabilities).zsh);
125
- expect(written).not.toBe(emitScripts(capabilities, { minChars: 5 }).zsh);
126
- });
127
-
128
- it("--dry-run previews the write against the detected shell, writing nothing", async () => {
129
- const path = completionScriptPath("zsh");
130
- const outcome = await executeVerb(
131
- completionsVerb,
132
- {},
133
- DRY,
134
- bootRuntime(FLAGS, tmp("pragma-setup-proj-")),
127
+ const detected = await detectCompletions(
128
+ dir,
129
+ { kind: "unknown" },
130
+ {
131
+ platform: "win32",
132
+ env: { PATH: dir },
133
+ home: dir,
134
+ isWsl: false,
135
+ },
135
136
  );
136
- expect(outcome.stdout).toContain("Write file");
137
- expect(outcome.stdout).toContain(path);
138
- // The confirm gate / answer prompts are never part of a plan.
139
- expect(outcome.stdout).not.toContain("Prompt");
140
- expect(existsSync(path)).toBe(false);
137
+
138
+ expect(detected.binOnPath).toBe(true);
141
139
  });
142
140
 
143
- it("warns and writes nothing when no shell is detected", async () => {
144
- process.env.SHELL = "";
141
+ withShell("writes exactly emitScripts(capabilities)[shell]", async () => {
142
+ const path = completionScriptPath(SHELL as ShellId);
145
143
  const outcome = await executeVerb(
146
144
  completionsVerb,
147
145
  {},
@@ -149,11 +147,94 @@ describe("setup completions", () => {
149
147
  bootRuntime(FLAGS, tmp("pragma-setup-proj-")),
150
148
  );
151
149
  expect(outcome.exitCode).toBe(0);
152
- expect(existsSync(completionScriptPath("zsh"))).toBe(false);
150
+ expect(existsSync(path)).toBe(true);
151
+ expect(readFileSync(path, "utf-8")).toBe(
152
+ emitScripts(capabilities)[SHELL as ShellId],
153
+ );
153
154
  });
154
155
 
155
- it("--undo reverses the write", async () => {
156
- const path = completionScriptPath("zsh");
156
+ withShell(
157
+ "bakes the completion config (minChars + per-family opt-out) into the emitted script",
158
+ async () => {
159
+ // Proves the reconciled `detectCompletions(cwd)` reads `completion` config
160
+ // and threads {minChars, disabledFamilies} into `emitScripts` — a silent
161
+ // regression if the fold had dropped autocomplete's config-threading for
162
+ // setup's argument-free detect.
163
+ const cwd = tmp("pragma-setup-proj-");
164
+ writeFileSync(
165
+ join(cwd, "pragma.config.ts"),
166
+ "export default { completion: { minChars: 5, families: { block: false } } };\n",
167
+ );
168
+ const path = completionScriptPath(SHELL as ShellId);
169
+ const outcome = await executeVerb(
170
+ completionsVerb,
171
+ {},
172
+ YES,
173
+ bootRuntime(FLAGS, cwd),
174
+ );
175
+ expect(outcome.exitCode).toBe(0);
176
+ const written = readFileSync(path, "utf-8");
177
+ // The installed script is EXACTLY the config-baked emit (minChars 5, the
178
+ // `block` family scrubbed) — the config was threaded end to end.
179
+ expect(written).toBe(
180
+ emitScripts(capabilities, {
181
+ minChars: 5,
182
+ disabledFamilies: ["block"],
183
+ })[SHELL as ShellId],
184
+ );
185
+ // Both knobs actually moved the output: it differs from the default emit
186
+ // (minChars), and from the minChars-only emit (the family opt-out).
187
+ expect(written).not.toBe(emitScripts(capabilities)[SHELL as ShellId]);
188
+ expect(written).not.toBe(
189
+ emitScripts(capabilities, { minChars: 5 })[SHELL as ShellId],
190
+ );
191
+ },
192
+ );
193
+
194
+ withShell(
195
+ "--dry-run previews the write against the detected shell, writing nothing",
196
+ async () => {
197
+ const path = completionScriptPath(SHELL as ShellId);
198
+ const outcome = await executeVerb(
199
+ completionsVerb,
200
+ {},
201
+ DRY,
202
+ bootRuntime(FLAGS, tmp("pragma-setup-proj-")),
203
+ );
204
+ // The dry-run renders the PLAN through the verb's formatPlan seam, not the
205
+ // kernel's raw effect dump: one row, the shell it detected, and the script
206
+ // path rendered against the header's roots instead of repeated absolutely.
207
+ expect(outcome.stdout).toContain("Setup plan — global band");
208
+ expect(outcome.stdout).toMatch(
209
+ new RegExp(`completions\\s+install\\s+${SHELL} →`),
210
+ );
211
+ // The confirm gate / answer prompts are never part of a plan.
212
+ expect(outcome.stdout).not.toContain("Prompt");
213
+ expect(existsSync(path)).toBe(false);
214
+ },
215
+ );
216
+
217
+ withShell(
218
+ "refuses to install a script whose `pragma` the shell cannot find",
219
+ async () => {
220
+ // The script delegates every name context to `pragma __complete`. With
221
+ // the binary unreachable the file installs cleanly, doctor goes green,
222
+ // and TAB does nothing — so this is a named skip, not a silent write.
223
+ process.env.PATH = tmp("pragma-setup-nobin-"); // no `pragma` in it
224
+ const outcome = await executeVerb(
225
+ completionsVerb,
226
+ {},
227
+ YES,
228
+ bootRuntime(FLAGS, tmp("pragma-setup-proj-")),
229
+ );
230
+ expect(outcome.exitCode).toBe(0);
231
+ expect(outcome.stdout).toContain("cannot find on PATH");
232
+ expect(existsSync(completionScriptPath(SHELL as ShellId))).toBe(false);
233
+ },
234
+ );
235
+
236
+ withShell("--undo reverses the write", async () => {
237
+ const path = completionScriptPath(SHELL as ShellId);
157
238
  const cwd = tmp("pragma-setup-proj-");
158
239
  await executeVerb(completionsVerb, {}, YES, bootRuntime(FLAGS, cwd));
159
240
  expect(existsSync(path)).toBe(true);
@@ -168,9 +249,11 @@ describe("setup mcp — recap gate", () => {
168
249
  mkdirSync(join(cwd, ".cursor"), { recursive: true }); // makes Cursor detected
169
250
  const configPath = join(cwd, ".cursor", "mcp.json");
170
251
 
252
+ // `--local`: the project band is opt-in now, so a bare `setup mcp` would
253
+ // write the HOME config and leave this repository untouched.
171
254
  const outcome = await executeVerb(
172
255
  verbOf("mcp"),
173
- {},
256
+ { local: true },
174
257
  YES,
175
258
  bootRuntime(FLAGS, cwd),
176
259
  );
@@ -185,7 +268,7 @@ describe("setup mcp — recap gate", () => {
185
268
  mkdirSync(join(cwd, ".cursor"), { recursive: true });
186
269
  const configPath = join(cwd, ".cursor", "mcp.json");
187
270
 
188
- const { generator } = await buildSetupPlan(
271
+ const { generator } = await buildSetupRun(
189
272
  bootRuntime(FLAGS, cwd),
190
273
  "mcp",
191
274
  "both",
@@ -207,7 +290,7 @@ describe("setup mcp — scope & dedup", () => {
207
290
  let prevPath: string | undefined;
208
291
  beforeEach(() => {
209
292
  prevPath = process.env.PATH;
210
- process.env.PATH = tmp("pragma-scope-path-");
293
+ process.env.PATH = stubPath();
211
294
  });
212
295
  afterEach(() => {
213
296
  process.env.PATH = prevPath;
@@ -230,7 +313,7 @@ describe("setup mcp — scope & dedup", () => {
230
313
  writeFileSync(join(clineExt, "package.json"), "{}");
231
314
  const outcome = await executeVerb(
232
315
  verbOf("mcp"),
233
- {},
316
+ { local: true },
234
317
  YES,
235
318
  bootRuntime(FLAGS, cwd),
236
319
  );
@@ -317,13 +400,93 @@ describe("setup mcp — scope & dedup", () => {
317
400
  ).toBe(true);
318
401
  expect(existsSync(join(cwd, ".cursor", "mcp.json"))).toBe(false);
319
402
  });
403
+
404
+ it("a GLOBAL entry omits cwd — registrations from two directories are byte-identical", async () => {
405
+ // A per-user server must not be pinned to whatever directory `setup mcp
406
+ // --global` happened to run from (a registration made from ~/Downloads
407
+ // used to serve ~/Downloads forever, and re-running from repo B flipped
408
+ // the machine band to repo B).
409
+ const home = process.env.HOME ?? "";
410
+ const configPath = join(home, ".codeium", "windsurf", "mcp_config.json");
411
+ const register = async (cwd: string): Promise<string> => {
412
+ mkdirSync(join(cwd, ".windsurf"), { recursive: true });
413
+ await executeVerb(
414
+ verbOf("mcp"),
415
+ { global: true },
416
+ YES,
417
+ bootRuntime(FLAGS, cwd),
418
+ );
419
+ return readFileSync(configPath, "utf-8");
420
+ };
421
+ const fromA = await register(tmp("pragma-setup-projA-"));
422
+ const fromB = await register(tmp("pragma-setup-projB-"));
423
+ expect(fromA).toBe(fromB);
424
+ const entry = JSON.parse(fromA).mcpServers?.pragma;
425
+ expect(entry).toEqual({ command: "pragma", args: ["mcp", "serve"] });
426
+ expect(entry).not.toHaveProperty("cwd");
427
+ });
428
+
429
+ it("a stale cwd-pinned GLOBAL entry converges: drifted once, configured after", async () => {
430
+ const home = process.env.HOME ?? "";
431
+ const configPath = join(home, ".codeium", "windsurf", "mcp_config.json");
432
+ mkdirSync(join(home, ".codeium", "windsurf"), { recursive: true });
433
+ writeFileSync(
434
+ configPath,
435
+ JSON.stringify({
436
+ mcpServers: {
437
+ pragma: {
438
+ command: "pragma",
439
+ args: ["mcp"],
440
+ cwd: "/home/u/Downloads",
441
+ },
442
+ },
443
+ }),
444
+ );
445
+ const cwd = tmp("pragma-setup-proj-");
446
+ mkdirSync(join(cwd, ".windsurf"), { recursive: true });
447
+ const rt = bootRuntime(FLAGS, cwd);
448
+ const { detectMcp, mcpGroupState } = await import(
449
+ "./operations/setupMcp.js"
450
+ );
451
+ // The pinned entry reads as drift (the omitted cwd is a controlled field).
452
+ const before = await detectMcp(rt, "global");
453
+ expect(mcpGroupState(before, configPath)).toBe("drifted");
454
+ // One write converges it...
455
+ await executeVerb(
456
+ verbOf("mcp"),
457
+ { global: true },
458
+ YES,
459
+ bootRuntime(FLAGS, cwd),
460
+ );
461
+ expect(
462
+ JSON.parse(readFileSync(configPath, "utf-8")).mcpServers.pragma,
463
+ ).toEqual({ command: "pragma", args: ["mcp", "serve"] });
464
+ // ...and it stays `configured` afterwards (no churn on every run).
465
+ const after = await detectMcp(bootRuntime(FLAGS, cwd), "global");
466
+ expect(mcpGroupState(after, configPath)).toBe("configured");
467
+ });
468
+
469
+ it("a PROJECT entry still records the project root as cwd", async () => {
470
+ const cwd = tmp("pragma-setup-proj-");
471
+ mkdirSync(join(cwd, ".cursor"), { recursive: true });
472
+ await executeVerb(
473
+ verbOf("mcp"),
474
+ { local: true },
475
+ YES,
476
+ bootRuntime(FLAGS, cwd),
477
+ );
478
+ const entry = JSON.parse(
479
+ readFileSync(join(cwd, ".cursor", "mcp.json"), "utf-8"),
480
+ ).mcpServers?.pragma;
481
+ expect(entry.cwd).toBe(cwd);
482
+ });
320
483
  });
321
484
 
322
485
  describe("setup mcp — customize opt-in gate (Item 6)", () => {
323
486
  it("gates the per-file multiselect behind an explicit customize=true", async () => {
324
487
  const cwd = tmp("pragma-setup-proj-");
325
488
  mkdirSync(join(cwd, ".cursor"), { recursive: true }); // ⇒ one MCP target group
326
- const { generator } = await buildSetupPlan(
489
+ const { generator } = await buildSetupRun(
327
490
  bootRuntime(FLAGS, cwd),
328
491
  "mcp",
329
492
  "both",
@@ -339,20 +502,134 @@ describe("setup mcp — customize opt-in gate (Item 6)", () => {
339
502
  expect(targets?.when?.({ customize: false })).toBe(false);
340
503
  expect(targets?.when?.({ customize: true })).toBe(true);
341
504
  });
505
+
506
+ it("recaps the files the run KEPT, not the ones it offered", async () => {
507
+ // The note counted every child of the plan row, so narrowing two MCP files
508
+ // down to one still recapped `2 added`: the plan and the result described
509
+ // different work.
510
+ const cwd = tmp("pragma-setup-proj-");
511
+ mkdirSync(join(cwd, ".cursor"), { recursive: true });
512
+ mkdirSync(join(cwd, ".gemini"), { recursive: true });
513
+ const run = await buildSetupRun(bootRuntime(FLAGS, cwd), "mcp", "project");
514
+ const row = run.plan.rows.find((r) => r.target === "mcp");
515
+ expect(row?.children?.length).toBe(2);
516
+ const kept = row?.children?.[0]?.key as string;
517
+
518
+ const applied = run.applied({
519
+ targets: ["project:mcp"],
520
+ customize: true,
521
+ mcpTargets: [kept],
522
+ });
523
+ expect(applied.rows.find((r) => r.target === "mcp")?.outcome?.note).toBe(
524
+ "1 added",
525
+ );
526
+ });
527
+ });
528
+
529
+ describe("setup lsp — per-editor multiselect (child rows)", () => {
530
+ let prevPath: string | undefined;
531
+ let stubDir = "";
532
+ beforeEach(() => {
533
+ prevPath = process.env.PATH;
534
+ stubDir = stubPath();
535
+ // Two VS Code forks on PATH, neither carrying the extension.
536
+ writeFileSync(join(stubDir, "code"), "");
537
+ writeFileSync(join(stubDir, "codium"), "");
538
+ process.env.PATH = stubDir;
539
+ });
540
+ afterEach(() => {
541
+ process.env.PATH = prevPath;
542
+ });
543
+
544
+ it("offers one child per detected editor, like the mcp row offers files", async () => {
545
+ const { plan } = await buildSetupRun(
546
+ bootRuntime(FLAGS, tmp("pragma-setup-proj-")),
547
+ "lsp",
548
+ "global",
549
+ );
550
+ const row = plan.rows.find((r) => r.target === "lsp");
551
+ expect(row?.action).toBe("install");
552
+ expect(row?.children?.map((c) => c.key)).toEqual(["code", "codium"]);
553
+ expect(row?.children?.every((c) => c.action === "add")).toBe(true);
554
+ });
555
+
556
+ it("marks an editor that already has the extension unchanged", async () => {
557
+ mkdirSync(
558
+ join(
559
+ process.env.HOME ?? "",
560
+ ".vscode",
561
+ "extensions",
562
+ "canonical.terrazzo-lsp-extension-1.2.3",
563
+ ),
564
+ { recursive: true },
565
+ );
566
+ const { plan } = await buildSetupRun(
567
+ bootRuntime(FLAGS, tmp("pragma-setup-proj-")),
568
+ "lsp",
569
+ "global",
570
+ );
571
+ const children = plan.rows.find((r) => r.target === "lsp")?.children;
572
+ expect(children?.find((c) => c.key === "code")?.action).toBe("unchanged");
573
+ expect(children?.find((c) => c.key === "codium")?.action).toBe("add");
574
+ });
575
+
576
+ it("gates the per-editor multiselect behind the same customize confirm", async () => {
577
+ const { generator } = await buildSetupRun(
578
+ bootRuntime(FLAGS, tmp("pragma-setup-proj-")),
579
+ "lsp",
580
+ "global",
581
+ );
582
+ const customize = generator.prompts.find((p) => p.name === "customize");
583
+ const editors = generator.prompts.find((p) => p.name === "lspEditors");
584
+ expect(customize?.default).toBe(false);
585
+ // Both pending editors are pre-selected, so the default path installs into
586
+ // every fork without an extra question.
587
+ expect(editors?.default).toEqual(["code", "codium"]);
588
+ expect(editors?.when?.({ customize: false })).toBe(false);
589
+ expect(editors?.when?.({ customize: true })).toBe(true);
590
+ });
591
+
592
+ it("sideloads only into the editors the user kept", async () => {
593
+ const { detectLsp, composeLsp } = await import("./operations/setupLsp.js");
594
+ const detected = await detectLsp(tmp("pragma-setup-proj-"));
595
+ const { effects } = dryRun(composeLsp(detected, ["codium"]));
596
+ const execs = effects.filter((e) => e._tag === "Exec") as (Effect & {
597
+ _tag: "Exec";
598
+ command: string;
599
+ })[];
600
+ // The fetch still runs once; the sideload runs for `codium` alone.
601
+ expect(execs.map((e) => e.command)).toEqual(["bun", "codium"]);
602
+ });
603
+
604
+ it("composes nothing at all when every editor is deselected", async () => {
605
+ // Not even the package fetch: there is nothing to install it into.
606
+ const { detectLsp, composeLsp } = await import("./operations/setupLsp.js");
607
+ const detected = await detectLsp(tmp("pragma-setup-proj-"));
608
+ const { effects } = dryRun(composeLsp(detected, []));
609
+ expect(effects.some((e) => e._tag === "Exec")).toBe(false);
610
+ });
342
611
  });
343
612
 
344
613
  describe("setup (run-all wizard) — scope threading", () => {
345
614
  // Isolate PATH so an ambient `claude`/`codex` can't inject a harness via a
346
615
  // `process` signal — detection is driven only by the dirs each test makes.
616
+ // A stubbed `code` CLI makes the LSP step's editor detection deterministic.
347
617
  let prevPath: string | undefined;
618
+ let editorPath = "";
348
619
  beforeEach(() => {
349
620
  prevPath = process.env.PATH;
350
- process.env.PATH = tmp("pragma-runall-path-");
621
+ editorPath = stubPath();
622
+ process.env.PATH = editorPath;
351
623
  });
352
624
  afterEach(() => {
353
625
  process.env.PATH = prevPath;
354
626
  });
355
627
 
628
+ /** Put a stub `code` on the isolated PATH so an LSP install is composable. */
629
+ const seedEditorCli = (): void => {
630
+ writeFileSync(join(editorPath, "code"), "");
631
+ };
632
+
356
633
  /** Seed a discoverable skill so the project-band skills step is offerable. */
357
634
  const seedSkill = (cwd: string): void => {
358
635
  const dir = join(cwd, ".pragma", "skills", "s");
@@ -375,39 +652,56 @@ describe("setup (run-all wizard) — scope threading", () => {
375
652
  );
376
653
  expect(outcome.exitCode).toBe(0);
377
654
  const plan = outcome.stdout ?? "";
378
- // The global-band steps are gone under --local (the bug: they used to run).
379
- expect(plan).not.toContain(completionScriptPath("zsh"));
380
- expect(plan).not.toContain("terrazzo-lsp-extension");
655
+ // A band FILTERS the run-all: the out-of-band targets are still named, as
656
+ // rows saying why they are not in this run, so nothing is silently absent.
657
+ expect(plan).toContain("completions skip");
658
+ expect(plan).toContain("user-level only — it has no project band");
381
659
  // The project-band steps remain: MCP into .cursor, skills into .agents/skills.
382
660
  expect(plan).toContain("mcp.json");
383
661
  expect(plan).toContain(".agents/skills");
384
662
  });
385
663
 
386
- it("--global omits the project-band skills step, keeping global steps", async () => {
664
+ withShell(
665
+ "--global omits the project-band skills step, keeping global steps",
666
+ async () => {
667
+ const cwd = tmp("pragma-setup-proj-");
668
+ seedSkill(cwd); // WOULD be offered under the default `both`
669
+ seedEditorCli(); // vscode detected ⇒ the LSP install is in the plan
670
+ const outcome = await executeVerb(
671
+ setupSelfVerb,
672
+ { global: true },
673
+ DRY,
674
+ bootRuntime(FLAGS, cwd),
675
+ );
676
+ expect(outcome.exitCode).toBe(0);
677
+ const plan = outcome.stdout ?? "";
678
+ // The global-band steps are present under --global. Paths render against
679
+ // the header's roots, so the row shows `~/.zfunc/_pragma`, not an absolute
680
+ // prefix repeated on every line.
681
+ expect(plan).toMatch(new RegExp(`completions\\s+install\\s+${SHELL} →`));
682
+ expect(plan).toContain("lsp");
683
+ // The project-band skills step is gone (the bug: it used to run under --global).
684
+ expect(plan).not.toContain(".agents/skills");
685
+ },
686
+ );
687
+ });
688
+
689
+ describe("setup skills", () => {
690
+ it("empty skills are a named skip at exit 0, not a failure", async () => {
691
+ // A skip is "nothing to do here, honestly named" — the same semantics
692
+ // doctor gives its skip glyph. Failing the direct sub-verb made a dotfiles
693
+ // script fail on a machine that simply has no skills installed yet, which
694
+ // teaches people to ignore the exit code.
387
695
  const cwd = tmp("pragma-setup-proj-");
388
- seedSkill(cwd); // WOULD be offered under the default `both`
389
696
  const outcome = await executeVerb(
390
- setupSelfVerb,
391
- { global: true },
392
- DRY,
697
+ verbOf("skills"),
698
+ { local: true },
699
+ YES,
393
700
  bootRuntime(FLAGS, cwd),
394
701
  );
395
702
  expect(outcome.exitCode).toBe(0);
396
- const plan = outcome.stdout ?? "";
397
- // The global-band steps are present under --global.
398
- expect(plan).toContain(completionScriptPath("zsh"));
399
- expect(plan).toContain("terrazzo-lsp-extension");
400
- // The project-band skills step is gone (the bug: it used to run under --global).
401
- expect(plan).not.toContain(".agents/skills");
402
- });
403
- });
404
-
405
- describe("setup skills", () => {
406
- it("empty skills raise EMPTY_RESULTS on the direct sub-verb", async () => {
407
- const cwd = tmp("pragma-setup-proj-");
408
- await expect(
409
- executeVerb(verbOf("skills"), {}, YES, bootRuntime(FLAGS, cwd)),
410
- ).rejects.toMatchObject({ code: "EMPTY_RESULTS" });
703
+ expect(outcome.stdout).toContain("skills");
704
+ expect(outcome.stdout).toContain("no project skills");
411
705
  });
412
706
 
413
707
  it("detects a created action and composes a symlink carrying an undo", async () => {
@@ -419,7 +713,7 @@ describe("setup skills", () => {
419
713
  "---\nname: my-skill\ndescription: A test skill.\n---\n",
420
714
  );
421
715
 
422
- const detected = await detectSkills(bootRuntime(FLAGS, cwd));
716
+ const detected = await detectSkills(bootRuntime(FLAGS, cwd), "project");
423
717
  expect(detected.available).toBe(true);
424
718
  expect(detected.skillCount).toBe(1);
425
719
  expect(detected.actions.some((a) => a.action === "created")).toBe(true);
@@ -432,16 +726,209 @@ describe("setup skills", () => {
432
726
  expect(symlinkEffect?.undo).toBeDefined();
433
727
  expect(existsSync(join(cwd, ".agents", "skills", "my-skill"))).toBe(false);
434
728
  });
729
+
730
+ it("classifies a DANGLING symlink as replaced and repairs it (S1-2)", async () => {
731
+ // `existsSync` FOLLOWS a symlink, so a dangling link used to read as
732
+ // "absent" → plan `created` → the real symlink() crashed EEXIST labeled
733
+ // INTERNAL_ERROR ("please report this issue"), leaving the broken link in
734
+ // place — and the dry-run previewed a clean create the run then belied.
735
+ const cwd = tmp("pragma-setup-proj-");
736
+ const skillDir = join(cwd, ".pragma", "skills", "my-skill");
737
+ mkdirSync(skillDir, { recursive: true });
738
+ writeFileSync(
739
+ join(skillDir, "SKILL.md"),
740
+ "---\nname: my-skill\ndescription: A test skill.\n---\n",
741
+ );
742
+ const linkDir = join(cwd, ".agents", "skills");
743
+ mkdirSync(linkDir, { recursive: true });
744
+ const linkPath = join(linkDir, "my-skill");
745
+ symlinkSync(join(cwd, "nonexistent-target"), linkPath);
746
+
747
+ const detected = await detectSkills(bootRuntime(FLAGS, cwd), "project");
748
+ const action = detected.actions.find((a) => a.linkPath === linkPath);
749
+ expect(action?.action).toBe("replaced");
750
+
751
+ // The dry-run preview now PREDICTS the repair: a delete before the relink.
752
+ const { effects } = dryRun(composeSkills(detected));
753
+ const deletes = effects.filter(
754
+ (e) =>
755
+ e._tag === "DeleteFile" && (e as { path?: string }).path === linkPath,
756
+ );
757
+ expect(deletes.length).toBe(1);
758
+
759
+ // The real run repairs the link instead of crashing EEXIST.
760
+ await runTask(composeSkills(detected));
761
+ expect(readlinkSync(linkPath)).toBe(skillDir);
762
+ });
763
+
764
+ it("skips a real (non-symlink) directory at the link path, never deleting it", async () => {
765
+ // A hand-placed skill directory is not this command's to delete — the
766
+ // sibling planner (sources/installSkills.ts) already refuses to clobber
767
+ // it, and the two planners must agree on classification.
768
+ const cwd = tmp("pragma-setup-proj-");
769
+ const skillDir = join(cwd, ".pragma", "skills", "my-skill");
770
+ mkdirSync(skillDir, { recursive: true });
771
+ writeFileSync(
772
+ join(skillDir, "SKILL.md"),
773
+ "---\nname: my-skill\ndescription: A test skill.\n---\n",
774
+ );
775
+ const realDir = join(cwd, ".agents", "skills", "my-skill");
776
+ mkdirSync(realDir, { recursive: true });
777
+ writeFileSync(join(realDir, "SKILL.md"), "hand-placed\n");
778
+
779
+ const detected = await detectSkills(bootRuntime(FLAGS, cwd), "project");
780
+ const action = detected.actions.find((a) => a.linkPath === realDir);
781
+ expect(action?.action).toBe("skipped");
782
+
783
+ const { effects } = dryRun(composeSkills(detected));
784
+ expect(
785
+ effects.some(
786
+ (e) =>
787
+ e._tag === "DeleteFile" && (e as { path?: string }).path === realDir,
788
+ ),
789
+ ).toBe(false);
790
+ expect(readFileSync(join(realDir, "SKILL.md"), "utf-8")).toBe(
791
+ "hand-placed\n",
792
+ );
793
+ });
794
+
795
+ it("never owns — and never removes — a link into a SIBLING of the skill root", async () => {
796
+ // Ownership was a string prefix, and `<sourceRoot>-backup/my-skill` starts
797
+ // with `<sourceRoot>`. So a link the USER made into their own backup
798
+ // directory was classified as ours, and `--undo` deleted it: a file this
799
+ // command never created and had no business touching.
800
+ const cwd = tmp("pragma-setup-proj-");
801
+ const skillDir = join(cwd, ".pragma", "skills", "my-skill");
802
+ mkdirSync(skillDir, { recursive: true });
803
+ writeFileSync(
804
+ join(skillDir, "SKILL.md"),
805
+ "---\nname: my-skill\ndescription: A test skill.\n---\n",
806
+ );
807
+ const backup = join(cwd, ".pragma", "skills-backup", "my-skill");
808
+ mkdirSync(backup, { recursive: true });
809
+ writeFileSync(join(backup, "SKILL.md"), "the user's own copy\n");
810
+ const linkDir = join(cwd, ".agents", "skills");
811
+ mkdirSync(linkDir, { recursive: true });
812
+ const linkPath = join(linkDir, "my-skill");
813
+ symlinkSync(backup, linkPath);
814
+
815
+ const detected = await detectSkills(bootRuntime(FLAGS, cwd), "project");
816
+ const action = detected.actions.find((a) => a.linkPath === linkPath);
817
+ expect(action?.owned).toBe(false);
818
+ expect(ownedSkillLinks(detected).map((a) => a.linkPath)).not.toContain(
819
+ linkPath,
820
+ );
821
+
822
+ // The removal is what `--undo` runs: it must leave both the link and the
823
+ // directory it points at exactly where they were.
824
+ await runUndo(composeSkillsRemoval(detected));
825
+ expect(readlinkSync(linkPath)).toBe(backup);
826
+ expect(readFileSync(join(backup, "SKILL.md"), "utf-8")).toBe(
827
+ "the user's own copy\n",
828
+ );
829
+ });
830
+
831
+ it("removes an owned link whose skill is gone from an emptied source root", async () => {
832
+ // Detection returned before target discovery whenever the source root held
833
+ // no skills, so `--undo` looked at an empty action list, reported zero work
834
+ // and left this command's own links on disk forever.
835
+ const cwd = tmp("pragma-setup-proj-");
836
+ const skillsRoot = join(cwd, ".pragma", "skills");
837
+ const skillDir = join(skillsRoot, "my-skill");
838
+ mkdirSync(skillDir, { recursive: true });
839
+ writeFileSync(
840
+ join(skillDir, "SKILL.md"),
841
+ "---\nname: my-skill\ndescription: A test skill.\n---\n",
842
+ );
843
+ const linkDir = join(cwd, ".agents", "skills");
844
+ mkdirSync(linkDir, { recursive: true });
845
+ const linkPath = join(linkDir, "my-skill");
846
+ symlinkSync(skillDir, linkPath);
847
+ // The skill is removed upstream: the link is now ours, owned, and dangling.
848
+ rmSync(skillDir, { recursive: true, force: true });
849
+
850
+ const detected = await detectSkills(bootRuntime(FLAGS, cwd), "project");
851
+ expect(detected.available).toBe(false);
852
+ expect(ownedSkillLinks(detected).map((a) => a.linkPath)).toEqual([
853
+ linkPath,
854
+ ]);
855
+
856
+ await runUndo(composeSkillsRemoval(detected));
857
+ // `existsSync` follows the link, so the dangling one is read through lstat.
858
+ expect(() => lstatSync(linkPath)).toThrow();
859
+ });
860
+
861
+ it("owns a RELATIVE link into the skill root, resolved against the link dir", async () => {
862
+ // `readlink` reports the raw target, which may be relative — resolving it
863
+ // against the link's own directory is what makes the containment test
864
+ // answer about a real path rather than a string.
865
+ const cwd = tmp("pragma-setup-proj-");
866
+ const skillDir = join(cwd, ".pragma", "skills", "my-skill");
867
+ mkdirSync(skillDir, { recursive: true });
868
+ writeFileSync(
869
+ join(skillDir, "SKILL.md"),
870
+ "---\nname: my-skill\ndescription: A test skill.\n---\n",
871
+ );
872
+ const linkDir = join(cwd, ".agents", "skills");
873
+ mkdirSync(linkDir, { recursive: true });
874
+ const linkPath = join(linkDir, "my-skill");
875
+ symlinkSync(relative(linkDir, skillDir), linkPath);
876
+
877
+ const detected = await detectSkills(bootRuntime(FLAGS, cwd), "project");
878
+ const action = detected.actions.find((a) => a.linkPath === linkPath);
879
+ expect(action?.owned).toBe(true);
880
+
881
+ await runUndo(composeSkillsRemoval(detected));
882
+ expect(existsSync(linkPath)).toBe(false);
883
+ });
435
884
  });
436
885
 
437
- describe("setup lsp — missing-binary guard (bunx absent)", () => {
438
- it("surfaces a NAMED UNSUPPORTED (not INTERNAL_ERROR) when bunx is off PATH", async () => {
439
- // Drives the REAL composeLsp exec (YES — not a dry-run mock) with `bunx`
440
- // removed from PATH, so the spawn REJECTS with ENOENT. The reconciled guard
441
- // at composeLsp's use site names it; without the guard the raw reject
442
- // collapses to INTERNAL_ERROR ("please report this issue") at the boundary.
886
+ describe("setup — detections settle independently", () => {
887
+ it("gives a rejecting detection its own failed row, leaving the rest planned", async () => {
888
+ // The targets are independent, and one `Promise.all` made them share a
889
+ // fate: a single rejecting detection prevented a run being built at all,
890
+ // so NONE of the other targets ran and doctor rendered no banded rows.
891
+ const { TARGETS } = await import("./targets.js");
892
+ const lsp = TARGETS.find((t) => t.id === "lsp") as (typeof TARGETS)[number];
893
+ const spy = vi
894
+ .spyOn(lsp, "detect")
895
+ .mockRejectedValue(new Error("EACCES: permission denied, open '/x'"));
896
+ try {
897
+ const run = await buildSetupRun(
898
+ bootRuntime(FLAGS, tmp("pragma-setup-proj-")),
899
+ "all",
900
+ "global",
901
+ );
902
+ const row = run.plan.rows.find((r) => r.target === "lsp");
903
+ expect(row?.reason).toContain("EACCES");
904
+ // The point of the fix: the siblings are still planned.
905
+ expect(
906
+ run.plan.rows.filter((r) => r.target !== "lsp").length,
907
+ ).toBeGreaterThan(0);
908
+ // And the row is attributable, not a quiet skip: it exits non-zero.
909
+ const applied = run.applied({});
910
+ expect(
911
+ applied.rows.find((r) => r.target === "lsp")?.outcome?.status,
912
+ ).toBe("failed");
913
+ } finally {
914
+ spy.mockRestore();
915
+ }
916
+ });
917
+ });
918
+
919
+ describe("setup lsp — prerequisites (bun absent / no editor CLI)", () => {
920
+ it("surfaces a NAMED UNSUPPORTED (not INTERNAL_ERROR) when bun is off PATH", async () => {
921
+ // Drives the REAL composeLsp fetch (YES — not a dry-run mock) with a
922
+ // stubbed `code` CLI on PATH (so the sideload is attempted) but no `bun`,
923
+ // so the fetch spawn REJECTS with ENOENT. The guard names it; without the
924
+ // guard the raw reject collapses to INTERNAL_ERROR ("please report this
925
+ // issue") at the boundary.
443
926
  const prevPath = process.env.PATH;
444
- process.env.PATH = tmp("pragma-empty-path-"); // empty dir ⇒ bunx unresolvable
927
+ const prevData = process.env.XDG_DATA_HOME;
928
+ const stubDir = stubPath();
929
+ writeFileSync(join(stubDir, "code"), ""); // present ⇒ vscode is a target
930
+ process.env.PATH = stubDir;
931
+ process.env.XDG_DATA_HOME = tmp("pragma-lsp-data-"); // jail the staging dir
445
932
  let thrown: unknown;
446
933
  try {
447
934
  await executeVerb(
@@ -454,16 +941,207 @@ describe("setup lsp — missing-binary guard (bunx absent)", () => {
454
941
  thrown = error;
455
942
  } finally {
456
943
  process.env.PATH = prevPath;
944
+ process.env.XDG_DATA_HOME = prevData;
457
945
  }
458
946
  expect(thrown).toBeDefined();
459
947
  const err = asPragmaError(thrown);
460
948
  expect(err.code).toBe("UNSUPPORTED");
461
949
  expect(err.code).not.toBe("INTERNAL_ERROR");
462
- expect(err.message).toContain("bunx");
950
+ expect(err.message).toContain("bun");
463
951
  expect(err.message).toMatch(/not found on your PATH/i);
464
952
  // The recovery is the actionable install hint, not "report this issue".
465
953
  expect(err.recovery?.message ?? "").not.toMatch(/report this issue/i);
466
954
  });
955
+
956
+ it("sideloads the bundled VSIX into EACH detected editor missing it (per-editor CLI, not a hardcoded `code`)", async () => {
957
+ // Two editor CLIs on PATH; VS Code already has the extension (its
958
+ // extensions dir carries a versioned copy), VSCodium does not — so the
959
+ // composed plan fetches the package once (bun, into a durable staging dir)
960
+ // and sideloads ONLY into `codium`. This is the VSCodium fix: the editor
961
+ // CLI is a registry lookup, no longer the hardcoded `code`.
962
+ const prevPath = process.env.PATH;
963
+ const stubDir = tmp("pragma-forks-path-");
964
+ writeFileSync(join(stubDir, "code"), "");
965
+ writeFileSync(join(stubDir, "codium"), "");
966
+ process.env.PATH = stubDir;
967
+ try {
968
+ mkdirSync(
969
+ join(
970
+ process.env.HOME ?? "",
971
+ ".vscode",
972
+ "extensions",
973
+ "canonical.terrazzo-lsp-extension-1.2.3",
974
+ ),
975
+ { recursive: true },
976
+ );
977
+ const { detectLsp, composeLsp } = await import(
978
+ "./operations/setupLsp.js"
979
+ );
980
+ const detected = await detectLsp(tmp("pragma-setup-proj-"));
981
+ expect(detected.state).toBe("absent");
982
+ expect(detected.editors.map((e) => [e.editor.cli, e.installed])).toEqual([
983
+ ["code", true],
984
+ ["codium", false],
985
+ ]);
986
+
987
+ const { effects } = dryRun(composeLsp(detected));
988
+ const execs = effects.filter((e) => e._tag === "Exec") as (Effect & {
989
+ _tag: "Exec";
990
+ command: string;
991
+ args: string[];
992
+ })[];
993
+ // One fetch (bun add, durable staging) + one sideload per PENDING editor.
994
+ expect(execs.map((e) => e.command)).toEqual(["bun", "codium"]);
995
+ expect(execs[0].args).toEqual([
996
+ "add",
997
+ "@canonical/terrazzo-lsp-extension@latest",
998
+ ]);
999
+ expect(execs[1].args[0]).toBe("--install-extension");
1000
+ // The VSIX path is the staging dir's — durable, not a bunx /tmp cache.
1001
+ expect(execs[1].args[1]).toContain(detected.stagingDir);
1002
+ expect(execs[1].args[1]).toContain("terrazzo-lsp.vsix");
1003
+ } finally {
1004
+ process.env.PATH = prevPath;
1005
+ }
1006
+ });
1007
+
1008
+ it("treats an extension too old to WORK as not installed", async () => {
1009
+ // Every VSIX before 0.8.3 ships a language server that cannot resolve its
1010
+ // own dependencies — it only appeared to run because the extension prefers
1011
+ // Bun and Bun fetches missing packages off the network at spawn time.
1012
+ // Matching any version meant such a machine reported `installed`, so
1013
+ // `setup lsp` skipped it and `doctor` called it healthy: a wrong skip that
1014
+ // reads as correct, which is worse than a wrong failure. Everyone who ran
1015
+ // setup before that release would have kept a dead server forever.
1016
+ const prevPath = process.env.PATH;
1017
+ const stubDir = tmp("pragma-stale-ext-path-");
1018
+ writeFileSync(join(stubDir, "code"), "");
1019
+ process.env.PATH = stubDir;
1020
+ try {
1021
+ mkdirSync(
1022
+ join(
1023
+ process.env.HOME ?? "",
1024
+ ".vscode",
1025
+ "extensions",
1026
+ "canonical.terrazzo-lsp-extension-0.8.1",
1027
+ ),
1028
+ { recursive: true },
1029
+ );
1030
+ const { detectLsp } = await import("./operations/setupLsp.js");
1031
+ const detected = await detectLsp(tmp("pragma-setup-proj-"));
1032
+ expect(detected.state).not.toBe("installed");
1033
+ } finally {
1034
+ process.env.PATH = prevPath;
1035
+ }
1036
+ });
1037
+
1038
+ it("treats a version it cannot READ as too old, not as newer", async () => {
1039
+ // The fail-closed claim has to survive the parser. `Number.parseInt`
1040
+ // accepts numeric prefixes and a `|| 0` fallback turns NaN into zero, so
1041
+ // `1.invalid.0` and `1.0.0junk` both read as [1,0,0] — comparing NEWER
1042
+ // than the minimum and reporting a broken extension as installed, which
1043
+ // is the wrong skip this check exists to prevent.
1044
+ const prevPath = process.env.PATH;
1045
+ const stubDir = tmp("pragma-badver-path-");
1046
+ writeFileSync(join(stubDir, "code"), "");
1047
+ process.env.PATH = stubDir;
1048
+ try {
1049
+ for (const suffix of ["1.invalid.0", "1.0.0junk", "garbage", ""]) {
1050
+ mkdirSync(
1051
+ join(
1052
+ process.env.HOME ?? "",
1053
+ ".vscode",
1054
+ "extensions",
1055
+ `canonical.terrazzo-lsp-extension-${suffix}`,
1056
+ ),
1057
+ { recursive: true },
1058
+ );
1059
+ }
1060
+ const { detectLsp } = await import("./operations/setupLsp.js");
1061
+ const detected = await detectLsp(tmp("pragma-setup-proj-"));
1062
+ expect(detected.state).not.toBe("installed");
1063
+ } finally {
1064
+ process.env.PATH = prevPath;
1065
+ }
1066
+ });
1067
+
1068
+ it("reports already-installed (a true no-op) when every detected editor has the extension", async () => {
1069
+ const prevPath = process.env.PATH;
1070
+ const stubDir = tmp("pragma-installed-path-");
1071
+ writeFileSync(join(stubDir, "code"), "");
1072
+ process.env.PATH = stubDir;
1073
+ try {
1074
+ mkdirSync(
1075
+ join(
1076
+ process.env.HOME ?? "",
1077
+ ".vscode",
1078
+ "extensions",
1079
+ "canonical.terrazzo-lsp-extension-1.2.3",
1080
+ ),
1081
+ { recursive: true },
1082
+ );
1083
+ const { detectLsp, composeLsp } = await import(
1084
+ "./operations/setupLsp.js"
1085
+ );
1086
+ const detected = await detectLsp(tmp("pragma-setup-proj-"));
1087
+ expect(detected.state).toBe("installed");
1088
+ const { effects } = dryRun(composeLsp(detected));
1089
+ expect(effects.some((e) => e._tag === "Exec")).toBe(false);
1090
+ } finally {
1091
+ process.env.PATH = prevPath;
1092
+ }
1093
+ });
1094
+
1095
+ it("matches the FULL extension id, not any terrazzo-named directory", async () => {
1096
+ // The old probe matched the substring "terrazzo", which any other
1097
+ // terrazzo-named extension would false-positive.
1098
+ const prevPath = process.env.PATH;
1099
+ const stubDir = tmp("pragma-substr-path-");
1100
+ writeFileSync(join(stubDir, "code"), "");
1101
+ process.env.PATH = stubDir;
1102
+ try {
1103
+ mkdirSync(
1104
+ join(
1105
+ process.env.HOME ?? "",
1106
+ ".vscode",
1107
+ "extensions",
1108
+ "someoneelse.terrazzo-tools-9.9.9",
1109
+ ),
1110
+ { recursive: true },
1111
+ );
1112
+ const { detectLsp } = await import("./operations/setupLsp.js");
1113
+ const detected = await detectLsp(tmp("pragma-setup-proj-"));
1114
+ expect(detected.state).toBe("absent");
1115
+ } finally {
1116
+ process.env.PATH = prevPath;
1117
+ }
1118
+ });
1119
+
1120
+ it("skips with a NAMED message (exit 0) when no editor CLI exists at all", async () => {
1121
+ // A machine with no VS Code-family CLI cannot be installed into — the
1122
+ // honest outcome is a named skip, not the old UNSUPPORTED with a
1123
+ // "permissions or network" guess and a dead /tmp path (§3, S1-1's trigger).
1124
+ const prevPath = process.env.PATH;
1125
+ process.env.PATH = stubPath(); // empty ⇒ no editor CLI
1126
+ try {
1127
+ const outcome = await executeVerb(
1128
+ verbOf("lsp"),
1129
+ {},
1130
+ YES,
1131
+ bootRuntime(FLAGS, tmp("pragma-setup-proj-")),
1132
+ );
1133
+ expect(outcome.exitCode).toBe(0);
1134
+ expect(outcome.stdout).toContain("skipped");
1135
+ expect(outcome.stdout).toContain("no VS Code-family editor CLI on PATH");
1136
+ // The remedy names what would count, and states plainly that nothing is
1137
+ // possible here yet rather than offering a command this machine lacks.
1138
+ expect(outcome.stdout).toContain(
1139
+ "no action is possible on this machine yet",
1140
+ );
1141
+ } finally {
1142
+ process.env.PATH = prevPath;
1143
+ }
1144
+ });
467
1145
  });
468
1146
 
469
1147
  describe("setup — idempotent detection of already-present config", () => {
@@ -471,7 +1149,7 @@ describe("setup — idempotent detection of already-present config", () => {
471
1149
  let prevPath: string | undefined;
472
1150
  beforeEach(() => {
473
1151
  prevPath = process.env.PATH;
474
- process.env.PATH = tmp("pragma-idem-path-");
1152
+ process.env.PATH = stubPath();
475
1153
  });
476
1154
  afterEach(() => {
477
1155
  process.env.PATH = prevPath;
@@ -483,42 +1161,41 @@ describe("setup — idempotent detection of already-present config", () => {
483
1161
  const configPath = join(cwd, ".cursor", "mcp.json");
484
1162
 
485
1163
  // First run writes the pragma entry.
486
- await executeVerb(verbOf("mcp"), {}, YES, bootRuntime(FLAGS, cwd));
1164
+ await executeVerb(
1165
+ verbOf("mcp"),
1166
+ { local: true },
1167
+ YES,
1168
+ bootRuntime(FLAGS, cwd),
1169
+ );
487
1170
  const firstBody = readFileSync(configPath, "utf-8");
488
1171
 
489
- // Detection now classifies the group `configured`.
490
- const { toResult } = await buildSetupPlan(
1172
+ // The plan's mcp row now reads `none` — nothing to do — and its child for
1173
+ // this file reads `unchanged`.
1174
+ const { plan } = await buildSetupRun(
491
1175
  bootRuntime(FLAGS, cwd),
492
1176
  "mcp",
493
- "both",
1177
+ "project",
494
1178
  );
495
- const result = toResult({});
496
- expect(result.kind).toBe("mcp");
497
- if (result.kind === "mcp") {
498
- expect(result.targets.at(0)?.state).toBe("configured");
499
- }
1179
+ const row = plan.rows.find((r) => r.target === "mcp");
1180
+ expect(row?.action).toBe("none");
1181
+ expect(row?.children?.find((c) => c.key === configPath)?.action).toBe(
1182
+ "unchanged",
1183
+ );
1184
+ // An already-current row is offered DE-selected, so a re-run never proposes
1185
+ // to rewrite what is already correct.
1186
+ expect(row?.selected).toBe(false);
500
1187
 
501
- // The wizard default-DESELECTS a configured target (so a plain re-run offers
502
- // it unchecked) — proven on the generated multiselect's default.
503
- const { generator } = await buildSetupPlan(
1188
+ // A real second run is idempotent: the file stays byte-identical AND the
1189
+ // converged row composes no effect at all, so the mtime is untouched.
1190
+ const before = statSync(configPath).mtimeMs;
1191
+ await executeVerb(
1192
+ verbOf("mcp"),
1193
+ { local: true },
1194
+ YES,
504
1195
  bootRuntime(FLAGS, cwd),
505
- "mcp",
506
- "both",
507
1196
  );
508
- const multiselect = generator.prompts.find((p) => p.name === "mcpTargets");
509
- expect(multiselect?.default).toEqual([]); // the only file is configured
510
- expect(
511
- (multiselect?.choices ?? []).some((c) =>
512
- typeof c === "object" && "label" in c
513
- ? c.label.includes("already configured")
514
- : false,
515
- ),
516
- ).toBe(true);
517
-
518
- // A real second run is idempotent: the file stays byte-identical (the
519
- // read-modify-write re-merges the SAME pragma entry).
520
- await executeVerb(verbOf("mcp"), {}, YES, bootRuntime(FLAGS, cwd));
521
1197
  expect(readFileSync(configPath, "utf-8")).toBe(firstBody);
1198
+ expect(statSync(configPath).mtimeMs).toBe(before);
522
1199
  });
523
1200
 
524
1201
  it("mcp: a drifted pragma entry (wrong cwd) reads as `drifted` and is updated", async () => {
@@ -539,98 +1216,179 @@ describe("setup — idempotent detection of already-present config", () => {
539
1216
  )}\n`,
540
1217
  );
541
1218
 
542
- const { toResult, generator } = await buildSetupPlan(
1219
+ const { plan } = await buildSetupRun(
543
1220
  bootRuntime(FLAGS, cwd),
544
1221
  "mcp",
545
- "both",
1222
+ "project",
546
1223
  );
547
- const result = toResult({});
548
- if (result.kind === "mcp") {
549
- expect(result.targets.at(0)?.state).toBe("drifted");
550
- }
551
- // A drifted target stays SELECTED by default (it needs the update).
552
- const multiselect = generator.prompts.find((p) => p.name === "mcpTargets");
553
- expect(multiselect?.default).toEqual([configPath]);
1224
+ const row = plan.rows.find((r) => r.target === "mcp");
1225
+ // A drifted file is an `update` child, and the row stays SELECTED by
1226
+ // default because it needs the write.
1227
+ expect(row?.children?.find((c) => c.key === configPath)?.action).toBe(
1228
+ "update",
1229
+ );
1230
+ expect(row?.action).toBe("update");
1231
+ expect(row?.selected).toBe(true);
554
1232
 
555
- await executeVerb(verbOf("mcp"), {}, YES, bootRuntime(FLAGS, cwd));
1233
+ await executeVerb(
1234
+ verbOf("mcp"),
1235
+ { local: true },
1236
+ YES,
1237
+ bootRuntime(FLAGS, cwd),
1238
+ );
556
1239
  const config = JSON.parse(readFileSync(configPath, "utf-8"));
557
1240
  expect(config.mcpServers.pragma.cwd).toBe(cwd); // updated to the real cwd
558
1241
  });
559
1242
 
560
- it("completions: a second run detects the up-to-date script (state=installed)", async () => {
561
- const cwd = tmp("pragma-setup-proj-");
562
- const path = completionScriptPath("zsh");
563
- // First install.
564
- await executeVerb(completionsVerb, {}, YES, bootRuntime(FLAGS, cwd));
565
- const firstBody = readFileSync(path, "utf-8");
1243
+ withShell(
1244
+ "completions: a second run detects the up-to-date script (state=installed)",
1245
+ async () => {
1246
+ const cwd = tmp("pragma-setup-proj-");
1247
+ const path = completionScriptPath(SHELL as ShellId);
1248
+ // First install.
1249
+ await executeVerb(completionsVerb, {}, YES, bootRuntime(FLAGS, cwd));
1250
+ const firstBody = readFileSync(path, "utf-8");
566
1251
 
567
- // Detection now classifies the installed script `installed`.
568
- const { toResult } = await buildSetupPlan(
569
- bootRuntime(FLAGS, cwd),
570
- "completions",
571
- "both",
572
- );
573
- const result = toResult({});
574
- if (result.kind === "completions") {
575
- expect(result.state).toBe("installed");
576
- }
1252
+ // The plan's completions row now reads `none` — the installed bytes are
1253
+ // current, so a re-run composes nothing.
1254
+ const { plan } = await buildSetupRun(
1255
+ bootRuntime(FLAGS, cwd),
1256
+ "completions",
1257
+ "global",
1258
+ );
1259
+ expect(plan.rows.find((r) => r.target === "completions")?.action).toBe(
1260
+ "none",
1261
+ );
577
1262
 
578
- // A real second run is idempotent — the byte-identical script survives.
579
- await executeVerb(completionsVerb, {}, YES, bootRuntime(FLAGS, cwd));
580
- expect(readFileSync(path, "utf-8")).toBe(firstBody);
581
- });
1263
+ // A real second run is idempotent — the byte-identical script survives.
1264
+ await executeVerb(completionsVerb, {}, YES, bootRuntime(FLAGS, cwd));
1265
+ expect(readFileSync(path, "utf-8")).toBe(firstBody);
1266
+ },
1267
+ );
582
1268
 
583
- it("completions: a stale script (different body) reads as `stale`", async () => {
584
- const cwd = tmp("pragma-setup-proj-");
585
- const path = completionScriptPath("zsh");
586
- mkdirSync(join(path, ".."), { recursive: true });
587
- writeFileSync(path, "# stale hand-edited completion\n");
588
- const { toResult } = await buildSetupPlan(
589
- bootRuntime(FLAGS, cwd),
590
- "completions",
591
- "both",
592
- );
593
- const result = toResult({});
594
- if (result.kind === "completions") {
595
- expect(result.state).toBe("stale");
596
- }
597
- });
1269
+ withShell(
1270
+ "completions: a stale script (different body) reads as `stale`",
1271
+ async () => {
1272
+ const cwd = tmp("pragma-setup-proj-");
1273
+ const path = completionScriptPath(SHELL as ShellId);
1274
+ mkdirSync(join(path, ".."), { recursive: true });
1275
+ writeFileSync(path, "# stale hand-edited completion\n");
1276
+ const { plan } = await buildSetupRun(
1277
+ bootRuntime(FLAGS, cwd),
1278
+ "completions",
1279
+ "global",
1280
+ );
1281
+ // A hand-edited script is `update`, not `install`: the row says which of
1282
+ // the two it is, because overwriting someone's edit is worth naming.
1283
+ expect(plan.rows.find((r) => r.target === "completions")?.action).toBe(
1284
+ "update",
1285
+ );
1286
+ },
1287
+ );
598
1288
 
599
1289
  it("lsp: reports `unknown` when the `code` CLI is absent from PATH", async () => {
600
1290
  // PATH is the isolated empty dir (beforeEach), so `code` is unresolvable:
601
1291
  // detection cannot enumerate and reports `unknown` (installer still runs).
602
- const { toResult } = await buildSetupPlan(
1292
+ const { plan } = await buildSetupRun(
603
1293
  bootRuntime(FLAGS, tmp("pragma-setup-proj-")),
604
1294
  "lsp",
605
- "both",
1295
+ "global",
606
1296
  );
607
- const result = toResult({});
608
- expect(result.kind).toBe("lsp");
609
- if (result.kind === "lsp") {
610
- expect(result.state).toBe("unknown");
611
- }
1297
+ // No editor CLI resolves, so the row is a SKIP carrying its reason —
1298
+ // never a silent omission, and never a failure.
1299
+ const row = plan.rows.find((r) => r.target === "lsp");
1300
+ expect(row?.action).toBe("skip");
1301
+ expect(row?.reason).toContain("no VS Code-family editor CLI on PATH");
612
1302
  });
613
1303
  });
614
1304
 
615
1305
  describe("setup (run-all wizard)", () => {
616
- it("--dry-run previews every DETECTED step (completions + lsp + mcp), writing nothing", async () => {
1306
+ // Pin PATH to a stub dir carrying only a `code` CLI: the LSP step's editor
1307
+ // detection (and the ambient-harness process signals) must not depend on
1308
+ // what happens to be installed on the host running the tests.
1309
+ let prevPath: string | undefined;
1310
+ beforeEach(() => {
1311
+ prevPath = process.env.PATH;
1312
+ const dir = stubPath();
1313
+ writeFileSync(join(dir, "code"), "");
1314
+ process.env.PATH = dir;
1315
+ });
1316
+ afterEach(() => {
1317
+ process.env.PATH = prevPath;
1318
+ });
1319
+
1320
+ withShell(
1321
+ "--dry-run previews every DETECTED step (completions + lsp + mcp), writing nothing",
1322
+ async () => {
1323
+ const cwd = tmp("pragma-setup-proj-");
1324
+ mkdirSync(join(cwd, ".cursor"), { recursive: true }); // harness detected
1325
+ const outcome = await executeVerb(
1326
+ setupSelfVerb,
1327
+ {},
1328
+ DRY,
1329
+ bootRuntime(FLAGS, cwd),
1330
+ );
1331
+ expect(outcome.exitCode).toBe(0);
1332
+ const plan = outcome.stdout ?? "";
1333
+ // EVERY target is a visible row — including the ones that will skip. The
1334
+ // run-all used to build its choices only from detectable steps, so a
1335
+ // target it could not offer vanished from the plan and from the recap.
1336
+ for (const id of ["config", "completions", "lsp", "mcp", "skills"]) {
1337
+ expect(plan).toContain(id);
1338
+ }
1339
+ expect(plan).toContain(`${SHELL} →`); // completions row
1340
+ expect(plan).not.toContain("Prompt"); // recap gate / multiselects filtered
1341
+ // Nothing is written by a preview.
1342
+ expect(existsSync(completionScriptPath(SHELL as ShellId))).toBe(false);
1343
+ expect(existsSync(join(cwd, ".cursor", "mcp.json"))).toBe(false);
1344
+ },
1345
+ );
1346
+
1347
+ it("a failing LSP step no longer aborts the run — MCP and skills still apply, exit reflects the failure (S1-1)", async () => {
1348
+ // The audit's headline break: with an unsatisfiable LSP prerequisite the
1349
+ // single advertised onboarding command configured NOTHING (no MCP write,
1350
+ // no skills link) and exited 1. Steps are independent — one failure must
1351
+ // report, let the rest proceed, and only then fail the run. Here the LSP
1352
+ // step has an editor (`code` stub on PATH) but no `bun`, so its fetch
1353
+ // fails; MCP (.cursor) and skills (a seeded skill) must still apply.
1354
+ const prevData = process.env.XDG_DATA_HOME;
1355
+ process.env.XDG_DATA_HOME = tmp("pragma-s11-data-"); // jail the staging dir
617
1356
  const cwd = tmp("pragma-setup-proj-");
618
- mkdirSync(join(cwd, ".cursor"), { recursive: true }); // harness detected
619
- const outcome = await executeVerb(
620
- setupSelfVerb,
621
- {},
622
- DRY,
623
- bootRuntime(FLAGS, cwd),
1357
+ mkdirSync(join(cwd, ".cursor"), { recursive: true });
1358
+ const skillDir = join(cwd, ".pragma", "skills", "s");
1359
+ mkdirSync(skillDir, { recursive: true });
1360
+ writeFileSync(
1361
+ join(skillDir, "SKILL.md"),
1362
+ "---\nname: s\ndescription: A skill.\n---\n",
624
1363
  );
625
- expect(outcome.exitCode).toBe(0);
626
- const plan = outcome.stdout ?? "";
627
- expect(plan).toContain(completionScriptPath("zsh")); // completions step
628
- expect(plan).toContain("terrazzo-lsp-extension"); // lsp step (exec, mocked)
629
- expect(plan).toContain("mcp.json"); // mcp step
630
- expect(plan).not.toContain("Prompt"); // recap gate / multiselects filtered
631
- // Nothing is written by a preview.
632
- expect(existsSync(completionScriptPath("zsh"))).toBe(false);
633
- expect(existsSync(join(cwd, ".cursor", "mcp.json"))).toBe(false);
1364
+ let thrown: unknown;
1365
+ try {
1366
+ // `--scope both`: the failing target (lsp) is global-only, and the two
1367
+ // that must survive it write the project band, so the run has to cover
1368
+ // both. A bare run-all is the GLOBAL band now and would not touch this
1369
+ // repository at all.
1370
+ await executeVerb(
1371
+ setupSelfVerb,
1372
+ { scope: "both" },
1373
+ YES,
1374
+ bootRuntime(FLAGS, cwd),
1375
+ );
1376
+ } catch (error) {
1377
+ thrown = error;
1378
+ } finally {
1379
+ process.env.XDG_DATA_HOME = prevData;
1380
+ }
1381
+ // The satisfiable steps ran to completion...
1382
+ expect(existsSync(join(cwd, ".cursor", "mcp.json"))).toBe(true);
1383
+ expect(existsSync(join(cwd, ".agents", "skills", "s"))).toBe(true);
1384
+ // ...and the run still failed, with the ORIGINAL step error surfaced.
1385
+ expect(thrown).toBeDefined();
1386
+ const err = asPragmaError(thrown);
1387
+ expect(err.code).toBe("UNSUPPORTED");
1388
+ // One failed row reports ITS OWN cause — a count would throw away the only
1389
+ // sentence that says what is wrong.
1390
+ expect(err.message).toContain("lsp");
1391
+ expect(err.message).toContain("bun");
634
1392
  });
635
1393
 
636
1394
  it("omits skills gracefully when none are discovered (no mid-wizard EMPTY_RESULTS)", async () => {
@@ -643,7 +1401,11 @@ describe("setup (run-all wizard)", () => {
643
1401
  bootRuntime(FLAGS, tmp("pragma-setup-proj-")),
644
1402
  );
645
1403
  expect(outcome.exitCode).toBe(0);
646
- expect(outcome.stdout).toContain("terrazzo-lsp-extension"); // lsp still there
1404
+ // The skills row is PRESENT and says why it cannot act, rather than being
1405
+ // dropped from a plan that then claims completeness.
1406
+ expect(outcome.stdout).toContain("skills");
1407
+ expect(outcome.stdout).toContain("no skills installed");
1408
+ expect(outcome.stdout).toContain("lsp");
647
1409
  });
648
1410
  });
649
1411
 
@@ -669,7 +1431,7 @@ describe("lazy-React discipline (PROTECTED)", () => {
669
1431
  });
670
1432
 
671
1433
  describe("setup — mixed-noun wiring & MCP surface", () => {
672
- it("registers a single `setup` command with an action and four sub-verbs", () => {
1434
+ it("registers a single `setup` command with an action and five sub-verbs", () => {
673
1435
  const program = projectCli([setupModule]);
674
1436
  const setups = program.commands.filter((c) => c.name() === "setup");
675
1437
  expect(setups).toHaveLength(1);
@@ -678,9 +1440,10 @@ describe("setup — mixed-noun wiring & MCP surface", () => {
678
1440
  expect(setup?.options.map((o) => o.long)).toEqual(
679
1441
  expect.arrayContaining(["--dry-run", "--undo", "--yes"]),
680
1442
  );
681
- // The four CLI-only sub-verbs hang under it.
1443
+ // The five CLI-only sub-verbs hang under it — one per target-table row.
682
1444
  expect(setup?.commands.map((c) => c.name()).sort()).toEqual([
683
1445
  "completions",
1446
+ "config",
684
1447
  "lsp",
685
1448
  "mcp",
686
1449
  "skills",