@canonical/pragma-cli 0.36.0 → 0.38.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 (312) hide show
  1. package/README.md +3 -2
  2. package/bundled-skills/adoption-a1-styles/SKILL.md +24 -0
  3. package/bundled-skills/adoption-a2-components/SKILL.md +22 -6
  4. package/bundled-skills/anatomy-author/ANATOMY_DSL_SPEC.md +1397 -327
  5. package/bundled-skills/anatomy-author/SKILL.md +840 -137
  6. package/bundled-skills/design-auditor/SKILL.md +133 -9
  7. package/bundled-skills/implement-component/SKILL.md +192 -23
  8. package/bundled-skills/specify-component/SKILL.md +124 -12
  9. package/bundled-skills/specify-pattern/SKILL.md +47 -8
  10. package/dist/package.json +23 -20
  11. package/dist/pragma.conf.js +963 -40
  12. package/dist/pragma.conf.js.map +1 -1
  13. package/dist/src/capabilities/capabilities/catalog.js +14 -5
  14. package/dist/src/capabilities/capabilities/catalog.js.map +1 -1
  15. package/dist/src/capabilities/capabilities/hints.js +33 -5
  16. package/dist/src/capabilities/capabilities/hints.js.map +1 -1
  17. package/dist/src/capabilities/config/show.render.js +30 -2
  18. package/dist/src/capabilities/config/show.render.js.map +1 -1
  19. package/dist/src/capabilities/create/create.verb.js +2 -1
  20. package/dist/src/capabilities/create/create.verb.js.map +1 -1
  21. package/dist/src/capabilities/create/createSurface.generated.js +23 -1
  22. package/dist/src/capabilities/create/createSurface.generated.js.map +1 -1
  23. package/dist/src/capabilities/doctor/checks/checkNodeVersion.js +11 -6
  24. package/dist/src/capabilities/doctor/checks/checkNodeVersion.js.map +1 -1
  25. package/dist/src/capabilities/doctor/checks/harnessInventory.js +1 -1
  26. package/dist/src/capabilities/doctor/checks/harnessInventory.js.map +1 -1
  27. package/dist/src/capabilities/doctor/checks/isSupportedNodeVersion.js +38 -0
  28. package/dist/src/capabilities/doctor/checks/isSupportedNodeVersion.js.map +1 -0
  29. package/dist/src/capabilities/doctor/checks/targetHealth.js +104 -14
  30. package/dist/src/capabilities/doctor/checks/targetHealth.js.map +1 -1
  31. package/dist/src/capabilities/graph/connect.render.js +101 -0
  32. package/dist/src/capabilities/graph/connect.render.js.map +1 -0
  33. package/dist/src/capabilities/graph/connect.types.js +21 -0
  34. package/dist/src/capabilities/graph/connect.types.js.map +1 -0
  35. package/dist/src/capabilities/graph/connect.verb.js +147 -0
  36. package/dist/src/capabilities/graph/connect.verb.js.map +1 -0
  37. package/dist/src/capabilities/graph/index.js +5 -3
  38. package/dist/src/capabilities/graph/index.js.map +1 -1
  39. package/dist/src/capabilities/graph/queryText.js +206 -0
  40. package/dist/src/capabilities/graph/queryText.js.map +1 -0
  41. package/dist/src/capabilities/graph/relationIndex.js +302 -0
  42. package/dist/src/capabilities/graph/relationIndex.js.map +1 -0
  43. package/dist/src/capabilities/graph/runConnect.js +456 -0
  44. package/dist/src/capabilities/graph/runConnect.js.map +1 -0
  45. package/dist/src/capabilities/graph/runQuery.js +21 -4
  46. package/dist/src/capabilities/graph/runQuery.js.map +1 -1
  47. package/dist/src/capabilities/setup/buildPlan.js +6 -2
  48. package/dist/src/capabilities/setup/buildPlan.js.map +1 -1
  49. package/dist/src/capabilities/setup/operations/index.js +7 -2
  50. package/dist/src/capabilities/setup/operations/index.js.map +1 -1
  51. package/dist/src/capabilities/setup/operations/setupCompletions.js +6 -2
  52. package/dist/src/capabilities/setup/operations/setupCompletions.js.map +1 -1
  53. package/dist/src/capabilities/setup/operations/setupConfig.js +4 -2
  54. package/dist/src/capabilities/setup/operations/setupConfig.js.map +1 -1
  55. package/dist/src/capabilities/setup/operations/setupGenerator.js +72 -14
  56. package/dist/src/capabilities/setup/operations/setupGenerator.js.map +1 -1
  57. package/dist/src/capabilities/setup/operations/setupLsp.js +372 -62
  58. package/dist/src/capabilities/setup/operations/setupLsp.js.map +1 -1
  59. package/dist/src/capabilities/setup/operations/setupMcp.js +150 -5
  60. package/dist/src/capabilities/setup/operations/setupMcp.js.map +1 -1
  61. package/dist/src/capabilities/setup/operations/setupSkills.js +4 -2
  62. package/dist/src/capabilities/setup/operations/setupSkills.js.map +1 -1
  63. package/dist/src/capabilities/setup/operations/writability.js +114 -0
  64. package/dist/src/capabilities/setup/operations/writability.js.map +1 -0
  65. package/dist/src/capabilities/setup/plan.js.map +1 -1
  66. package/dist/src/capabilities/setup/setup.verb.js +80 -31
  67. package/dist/src/capabilities/setup/setup.verb.js.map +1 -1
  68. package/dist/src/capabilities/setup/targets.js +124 -42
  69. package/dist/src/capabilities/setup/targets.js.map +1 -1
  70. package/dist/src/capabilities/shared/index.js +1 -1
  71. package/dist/src/capabilities/shared/index.js.map +1 -1
  72. package/dist/src/constants.js +7 -1
  73. package/dist/src/constants.js.map +1 -1
  74. package/dist/src/kernel/error/renderError.js +3 -2
  75. package/dist/src/kernel/error/renderError.js.map +1 -1
  76. package/dist/src/kernel/error/validOptions.js +41 -0
  77. package/dist/src/kernel/error/validOptions.js.map +1 -0
  78. package/dist/src/kernel/packs/compile.js +90 -6
  79. package/dist/src/kernel/packs/compile.js.map +1 -1
  80. package/dist/src/kernel/packs/cursor.js +135 -0
  81. package/dist/src/kernel/packs/cursor.js.map +1 -0
  82. package/dist/src/kernel/packs/graphql/buildLookupDocument.js +9 -2
  83. package/dist/src/kernel/packs/graphql/buildLookupDocument.js.map +1 -1
  84. package/dist/src/kernel/packs/paging.js +108 -0
  85. package/dist/src/kernel/packs/paging.js.map +1 -0
  86. package/dist/src/kernel/packs/renderPack.js +154 -5
  87. package/dist/src/kernel/packs/renderPack.js.map +1 -1
  88. package/dist/src/kernel/packs/resolveEntity.js +118 -19
  89. package/dist/src/kernel/packs/resolveEntity.js.map +1 -1
  90. package/dist/src/kernel/packs/runBodies.js +156 -18
  91. package/dist/src/kernel/packs/runBodies.js.map +1 -1
  92. package/dist/src/kernel/packs/schema.js +160 -12
  93. package/dist/src/kernel/packs/schema.js.map +1 -1
  94. package/dist/src/kernel/packs/sparql/authorQuery.js +227 -0
  95. package/dist/src/kernel/packs/sparql/authorQuery.js.map +1 -0
  96. package/dist/src/kernel/packs/sparql/buildListQuery.js +251 -0
  97. package/dist/src/kernel/packs/sparql/buildListQuery.js.map +1 -0
  98. package/dist/src/kernel/packs/sparql/buildLookupQuery.js +125 -22
  99. package/dist/src/kernel/packs/sparql/buildLookupQuery.js.map +1 -1
  100. package/dist/src/kernel/packs/sparql/filterValues.js +145 -0
  101. package/dist/src/kernel/packs/sparql/filterValues.js.map +1 -0
  102. package/dist/src/kernel/packs/sparql/index.js +24 -14
  103. package/dist/src/kernel/packs/sparql/index.js.map +1 -1
  104. package/dist/src/kernel/packs/sparql/runSelect.js +10 -4
  105. package/dist/src/kernel/packs/sparql/runSelect.js.map +1 -1
  106. package/dist/src/kernel/packs/sparql/searchTerm.js +25 -0
  107. package/dist/src/kernel/packs/sparql/searchTerm.js.map +1 -0
  108. package/dist/src/kernel/packs/storyRules.js +151 -0
  109. package/dist/src/kernel/packs/storyRules.js.map +1 -0
  110. package/dist/src/kernel/packs/tierScope.js +310 -0
  111. package/dist/src/kernel/packs/tierScope.js.map +1 -0
  112. package/dist/src/kernel/packs/types.js +80 -0
  113. package/dist/src/kernel/packs/types.js.map +1 -1
  114. package/dist/src/kernel/project/cli/buildProgram.js +10 -1
  115. package/dist/src/kernel/project/cli/buildProgram.js.map +1 -1
  116. package/dist/src/kernel/project/cli/dispatch.js +104 -3
  117. package/dist/src/kernel/project/cli/dispatch.js.map +1 -1
  118. package/dist/src/kernel/project/cli/rootHelp.js +8 -5
  119. package/dist/src/kernel/project/cli/rootHelp.js.map +1 -1
  120. package/dist/src/kernel/project/cli/suggestNames.js +31 -7
  121. package/dist/src/kernel/project/cli/suggestNames.js.map +1 -1
  122. package/dist/src/kernel/project/mcp/registerVerb.js +5 -1
  123. package/dist/src/kernel/project/mcp/registerVerb.js.map +1 -1
  124. package/dist/src/kernel/render/renderers.js +34 -2
  125. package/dist/src/kernel/render/renderers.js.map +1 -1
  126. package/dist/src/kernel/runtime/graphpack/embedded/pack.generated.js +2 -3
  127. package/dist/src/kernel/runtime/graphpack/embedded/pack.generated.js.map +1 -1
  128. package/dist/src/kernel/runtime/graphpack/embedded/pack.index.generated.js +1 -1
  129. package/dist/src/kernel/runtime/graphpack/embedded/pack.index.generated.js.map +1 -1
  130. package/dist/src/kernel/runtime/graphpack/embedded/pack.manifest.generated.js +4 -0
  131. package/dist/src/kernel/runtime/graphpack/embedded/pack.manifest.generated.js.map +1 -0
  132. package/dist/src/kernel/runtime/graphpack/embedded.js +13 -6
  133. package/dist/src/kernel/runtime/graphpack/embedded.js.map +1 -1
  134. package/dist/src/kernel/runtime/graphpack/read.js +17 -10
  135. package/dist/src/kernel/runtime/graphpack/read.js.map +1 -1
  136. package/dist/src/kernel/runtime/loadSession.js +3 -1
  137. package/dist/src/kernel/runtime/loadSession.js.map +1 -1
  138. package/dist/src/kernel/runtime/probe.js +1 -1
  139. package/dist/src/kernel/runtime/probe.js.map +1 -1
  140. package/dist/src/kernel/runtime/readEntity.js +19 -4
  141. package/dist/src/kernel/runtime/readEntity.js.map +1 -1
  142. package/dist/src/kernel/runtime/refs/parseRef.js +17 -1
  143. package/dist/src/kernel/runtime/refs/parseRef.js.map +1 -1
  144. package/dist/src/kernel/runtime/refs/resolve.js +38 -5
  145. package/dist/src/kernel/runtime/refs/resolve.js.map +1 -1
  146. package/dist/src/kernel/spec/emitReference.js +1 -1
  147. package/dist/src/kernel/spec/emitReference.js.map +1 -1
  148. package/package.json +23 -20
  149. package/pragma.conf.ts +983 -40
  150. package/src/capabilities/block.parity.test.ts +207 -27
  151. package/src/capabilities/block.shipped.exec.test.ts +156 -14
  152. package/src/capabilities/block.tierRank.exec.test.ts +46 -9
  153. package/src/capabilities/capabilities/catalog.ts +14 -5
  154. package/src/capabilities/capabilities/hints.ts +40 -5
  155. package/src/capabilities/config/collectConfigShow.test.ts +1 -0
  156. package/src/capabilities/config/set.test.ts +24 -25
  157. package/src/capabilities/config/show.render.test.ts +9 -4
  158. package/src/capabilities/config/show.render.ts +30 -2
  159. package/src/capabilities/create/byteEquality.test.ts +28 -1
  160. package/src/capabilities/create/create.test.ts +1 -0
  161. package/src/capabilities/create/create.verb.ts +2 -1
  162. package/src/capabilities/create/createGrammar.test.ts +4 -1
  163. package/src/capabilities/create/createSurface.generated.ts +23 -1
  164. package/src/capabilities/create/crossCli.subprocess.test.ts +10 -3
  165. package/src/capabilities/distribution.parity.test.ts +12 -11
  166. package/src/capabilities/distribution.test.ts +77 -7
  167. package/src/capabilities/doctor/checks/checkNodeVersion.test.ts +49 -0
  168. package/src/capabilities/doctor/checks/checkNodeVersion.ts +11 -7
  169. package/src/capabilities/doctor/checks/harnessInventory.test.ts +13 -6
  170. package/src/capabilities/doctor/checks/harnessInventory.ts +8 -4
  171. package/src/capabilities/doctor/checks/isSupportedNodeVersion.test.ts +111 -0
  172. package/src/capabilities/doctor/checks/isSupportedNodeVersion.ts +34 -0
  173. package/src/capabilities/doctor/checks/mcpCommand.test.ts +7 -1
  174. package/src/capabilities/doctor/checks/targetHealth.ts +114 -13
  175. package/src/capabilities/doctor/doctor.test.ts +327 -36
  176. package/src/capabilities/emptyRecovery.test.ts +84 -28
  177. package/src/capabilities/graph/connect.render.ts +141 -0
  178. package/src/capabilities/graph/connect.shipped.exec.test.ts +370 -0
  179. package/src/capabilities/graph/connect.test.ts +361 -0
  180. package/src/capabilities/graph/connect.types.ts +136 -0
  181. package/src/capabilities/graph/connect.verb.ts +165 -0
  182. package/src/capabilities/graph/index.ts +5 -3
  183. package/src/capabilities/graph/query.shipped.exec.test.ts +66 -0
  184. package/src/capabilities/graph/query.test.ts +17 -0
  185. package/src/capabilities/graph/queryText.test.ts +121 -0
  186. package/src/capabilities/graph/queryText.ts +240 -0
  187. package/src/capabilities/graph/relationIndex.ts +372 -0
  188. package/src/capabilities/graph/runConnect.ts +577 -0
  189. package/src/capabilities/graph/runQuery.ts +21 -6
  190. package/src/capabilities/lazy.test.ts +32 -2
  191. package/src/capabilities/listBudget.shipped.exec.test.ts +239 -0
  192. package/src/capabilities/listLookup.shipped.exec.test.ts +387 -0
  193. package/src/capabilities/listQuery.shipped.exec.test.ts +400 -0
  194. package/src/capabilities/resources/resources.test.ts +38 -2
  195. package/src/capabilities/setup/buildPlan.ts +7 -1
  196. package/src/capabilities/setup/operations/index.ts +14 -0
  197. package/src/capabilities/setup/operations/setupCompletions.ts +9 -2
  198. package/src/capabilities/setup/operations/setupConfig.ts +7 -2
  199. package/src/capabilities/setup/operations/setupGenerator.ts +93 -15
  200. package/src/capabilities/setup/operations/setupLsp.ts +544 -85
  201. package/src/capabilities/setup/operations/setupMcp.ts +212 -4
  202. package/src/capabilities/setup/operations/setupSkills.ts +7 -2
  203. package/src/capabilities/setup/operations/writability.test.ts +151 -0
  204. package/src/capabilities/setup/operations/writability.ts +135 -0
  205. package/src/capabilities/setup/plan.ts +11 -0
  206. package/src/capabilities/setup/setup.test.ts +992 -16
  207. package/src/capabilities/setup/setup.verb.ts +84 -36
  208. package/src/capabilities/setup/targets.ts +152 -48
  209. package/src/capabilities/setup/types.ts +41 -5
  210. package/src/capabilities/shared/index.ts +1 -0
  211. package/src/capabilities/sources/sources.test.ts +55 -0
  212. package/src/capabilities/surface.test.ts +59 -11
  213. package/src/capabilities/tier.parity.test.ts +2 -2
  214. package/src/capabilities/token.parity.test.ts +784 -0
  215. package/src/capabilities/tokenGraph.cli.test.ts +279 -0
  216. package/src/capabilities/tokenGraph.shipped.exec.test.ts +653 -0
  217. package/src/capabilities/variable.parity.test.ts +448 -0
  218. package/src/constants.ts +8 -0
  219. package/src/kernel/completion/shellDrive.test.ts +32 -30
  220. package/src/kernel/config/defaults.test.ts +21 -2
  221. package/src/kernel/config/readConfig.test.ts +1 -1
  222. package/src/kernel/error/renderError.ts +3 -2
  223. package/src/kernel/error/validOptions.test.ts +57 -0
  224. package/src/kernel/error/validOptions.ts +45 -0
  225. package/src/kernel/packs/collect.test.ts +2 -0
  226. package/src/kernel/packs/compile.test.ts +188 -15
  227. package/src/kernel/packs/compile.ts +109 -14
  228. package/src/kernel/packs/cursor.ts +135 -0
  229. package/src/kernel/packs/filteredEmpty.test.ts +213 -0
  230. package/src/kernel/packs/graphql/buildLookupDocument.ts +16 -9
  231. package/src/kernel/packs/graphqlSource.test.ts +152 -0
  232. package/src/kernel/packs/listNotice.test.ts +156 -0
  233. package/src/kernel/packs/paging.ts +110 -0
  234. package/src/kernel/packs/renderPack.ts +186 -13
  235. package/src/kernel/packs/resolveEntity.test.ts +62 -3
  236. package/src/kernel/packs/resolveEntity.ts +186 -26
  237. package/src/kernel/packs/runBodies.ts +196 -26
  238. package/src/kernel/packs/schema.ts +180 -13
  239. package/src/kernel/packs/sparql/authorQuery.test.ts +179 -0
  240. package/src/kernel/packs/sparql/authorQuery.ts +257 -0
  241. package/src/kernel/packs/sparql/buildListQuery.test.ts +344 -0
  242. package/src/kernel/packs/sparql/buildListQuery.ts +344 -0
  243. package/src/kernel/packs/sparql/buildLookupQuery.test.ts +138 -0
  244. package/src/kernel/packs/sparql/buildLookupQuery.ts +140 -20
  245. package/src/kernel/packs/sparql/filterValues.test.ts +86 -0
  246. package/src/kernel/packs/sparql/filterValues.ts +182 -0
  247. package/src/kernel/packs/sparql/index.ts +30 -14
  248. package/src/kernel/packs/sparql/runSelect.test.ts +63 -0
  249. package/src/kernel/packs/sparql/runSelect.ts +42 -2
  250. package/src/kernel/packs/sparql/searchTerm.ts +29 -0
  251. package/src/kernel/packs/storyRules.ts +180 -0
  252. package/src/kernel/packs/tierScope.test.ts +487 -0
  253. package/src/kernel/packs/tierScope.ts +409 -0
  254. package/src/kernel/packs/toolDescription.test.ts +13 -10
  255. package/src/kernel/packs/types.ts +311 -9
  256. package/src/kernel/project/cli/buildProgram.ts +17 -1
  257. package/src/kernel/project/cli/dispatch.test.ts +87 -0
  258. package/src/kernel/project/cli/dispatch.ts +114 -4
  259. package/src/kernel/project/cli/rootHelp.ts +8 -5
  260. package/src/kernel/project/cli/suggest.test.ts +35 -0
  261. package/src/kernel/project/cli/suggestNames.ts +34 -7
  262. package/src/kernel/project/mcp/registerVerb.ts +5 -1
  263. package/src/kernel/render/contracts.ts +2 -0
  264. package/src/kernel/render/prefixes.test.ts +2 -0
  265. package/src/kernel/render/renderers.test.ts +43 -0
  266. package/src/kernel/render/renderers.ts +34 -1
  267. package/src/kernel/runtime/graphpack/embedded/pack.generated.ts +2 -3
  268. package/src/kernel/runtime/graphpack/embedded/pack.index.generated.ts +1 -1
  269. package/src/kernel/runtime/graphpack/embedded/pack.manifest.generated.ts +3 -0
  270. package/src/kernel/runtime/graphpack/embedded.ts +14 -6
  271. package/src/kernel/runtime/graphpack/graphpack.test.ts +31 -5
  272. package/src/kernel/runtime/graphpack/read.ts +17 -13
  273. package/src/kernel/runtime/loadSession.ts +3 -1
  274. package/src/kernel/runtime/probe.ts +1 -1
  275. package/src/kernel/runtime/readEntity.ts +19 -4
  276. package/src/kernel/runtime/refs/parseRef.test.ts +49 -1
  277. package/src/kernel/runtime/refs/parseRef.ts +25 -1
  278. package/src/kernel/runtime/refs/resolve.ts +39 -5
  279. package/src/kernel/runtime/types.ts +30 -0
  280. package/src/kernel/spec/emitReference.ts +1 -1
  281. package/src/kernel/spec/surfaceConformance.test.ts +2 -2
  282. package/src/kernel/spec/types.ts +17 -0
  283. package/src/testing/PARITY_GAPS.ts +4 -4
  284. package/src/testing/behavioral/agentSession.mcp.test.ts +43 -39
  285. package/src/testing/behavioral/completion.test.ts +7 -6
  286. package/src/testing/behavioral/disclosure.test.ts +39 -37
  287. package/src/testing/behavioral/errorMatrix.mcp.test.ts +82 -64
  288. package/src/testing/behavioral/errorMatrix.storeless.test.ts +11 -10
  289. package/src/testing/behavioral/journeys.cli.test.ts +71 -32
  290. package/src/testing/behavioral/journeys.defaultPack.test.ts +6 -4
  291. package/src/testing/behavioral/journeys.firstInstall.test.ts +70 -5
  292. package/src/testing/behavioral/journeys.mcpLifecycle.mcp.test.ts +4 -3
  293. package/src/testing/behavioral/journeys.packageStories.test.ts +2 -1
  294. package/src/testing/behavioral/mcpSurface.test.ts +28 -25
  295. package/src/testing/behavioral/parity.test.ts +25 -23
  296. package/src/testing/eval/__snapshots__/eval.test.ts.snap +20 -0
  297. package/src/testing/eval/cases/effectful.ts +7 -1
  298. package/src/testing/eval/cases/readNouns.ts +100 -11
  299. package/src/testing/fixtures/blockGraph.ts +89 -5
  300. package/src/testing/fixtures/connectGraph.ts +159 -0
  301. package/src/testing/fixtures/graph/canonical.ts +128 -32
  302. package/src/testing/helpers/fsProbe.ts +88 -0
  303. package/src/testing/helpers/runCli.test.ts +44 -0
  304. package/src/testing/helpers/runCli.ts +27 -3
  305. package/src/testing/perf/budgets.test.ts +3 -1
  306. package/src/testing/perf/budgets.ts +138 -54
  307. package/dist/src/kernel/packs/sparql/applyFilters.js +0 -167
  308. package/dist/src/kernel/packs/sparql/applyFilters.js.map +0 -1
  309. package/dist/src/kernel/packs/sparql/applySearch.js +0 -25
  310. package/dist/src/kernel/packs/sparql/applySearch.js.map +0 -1
  311. package/src/kernel/packs/sparql/applyFilters.ts +0 -203
  312. package/src/kernel/packs/sparql/applySearch.ts +0 -33
@@ -20,10 +20,15 @@ import {
20
20
  type WriteState,
21
21
  } from "./harnessInventory.js";
22
22
 
23
+ // Roo Code is the `unscoped` example: it is `scope: "project"`, so it can
24
+ // never hold a global entry. VS Code used to stand here and stopped being an
25
+ // example of anything when its per-user `mcp.json` was added — it is
26
+ // `scope: "both"` now, and a fixture claiming otherwise would pin a fact the
27
+ // registry no longer holds.
23
28
  const REGISTRY: InventoryHarness[] = [
24
29
  { id: "claude-code", name: "Claude Code", inScope: true },
25
30
  { id: "cursor", name: "Cursor", inScope: true },
26
- { id: "vscode", name: "VS Code", inScope: false },
31
+ { id: "roo-code", name: "Roo Code", inScope: false },
27
32
  ];
28
33
 
29
34
  /** One harness's share of a file — its OWN write's state, never the file's. */
@@ -50,7 +55,7 @@ describe("harnessInventory — the roll-up", () => {
50
55
  "cursor:detected",
51
56
  // Not detected AND has no location in this scope at all — the two are
52
57
  // different facts and the row says which one applies.
53
- "vscode:unscoped",
58
+ "roo-code:unscoped",
54
59
  ]);
55
60
  expect(rows[0]?.locations).toEqual(["~/.claude.json"]);
56
61
  expect(rows[2]?.locations).toEqual([]);
@@ -58,12 +63,14 @@ describe("harnessInventory — the roll-up", () => {
58
63
 
59
64
  it("keeps 'undetected' and 'unscoped' apart", () => {
60
65
  // Same absence, two different reasons: Cursor could be here and is not,
61
- // VS Code could never be here at all.
66
+ // Roo Code could never be here at all.
62
67
  const rows = roll([]);
63
68
  expect(rows.find((r) => r.harnessId === "cursor")?.state).toBe(
64
69
  "undetected",
65
70
  );
66
- expect(rows.find((r) => r.harnessId === "vscode")?.state).toBe("unscoped");
71
+ expect(rows.find((r) => r.harnessId === "roo-code")?.state).toBe(
72
+ "unscoped",
73
+ );
67
74
  });
68
75
 
69
76
  it("reports a drifted group as drifted, not as registered", () => {
@@ -158,11 +165,11 @@ describe("harnessInventory — the listing", () => {
158
165
  expect(items.map((i) => i.label)).toEqual([
159
166
  "Claude Code",
160
167
  "Cursor",
161
- "VS Code",
168
+ "Roo Code",
162
169
  ]);
163
170
  expect(items[1]?.detail).toBe("not detected");
164
171
  // Scope-aware, and it states the FACT rather than naming a partition:
165
- // VS Code keeps its MCP config per project, so there is no global file for
172
+ // Roo Code keeps its MCP config per project, so there is no global file for
166
173
  // pragma to be in. `no Global band` named an internal word for that.
167
174
  expect(items[2]?.detail).toBe(
168
175
  "keeps no global config — it is per-project only",
@@ -68,9 +68,13 @@ import type { CheckItem, CheckStatus, Scope } from "../types.js";
68
68
  * - `detected` — the harness is on this machine; pragma is not registered.
69
69
  * - `undetected` — in the registry, has a location in this scope, not found.
70
70
  * - `unscoped` — in the registry, but has NO config location in this scope at
71
- * all (VS Code is `scope: "project"`, so it can never hold a global entry).
72
- * Stated rather than omitted: a verbose listing that silently dropped a row
73
- * reads as a bug in the listing.
71
+ * all. Roo Code and Cline are `scope: "project"`, so neither can ever hold a
72
+ * global entry: Cline is a VS Code EXTENSION whose config is the workspace
73
+ * `.vscode/mcp.json`, and Roo Code's is `.roo/mcp.json` — neither keeps a
74
+ * per-user file of its own. (VS Code itself used to be the example here, and
75
+ * stopped being one when its per-user `mcp.json` was added: it is
76
+ * `scope: "both"` now.) Stated rather than omitted: a verbose listing that
77
+ * silently dropped a row reads as a bug in the listing.
74
78
  */
75
79
  export type InventoryState =
76
80
  | "registered"
@@ -202,7 +206,7 @@ const STATE_STATUS: Record<InventoryState, CheckStatus> = {
202
206
  *
203
207
  * The unscoped line says WHERE the harness keeps its config rather than naming
204
208
  * an internal partition. `no Global band` told the reader nothing: "band" was
205
- * this repository's word, and the fact underneath it — VS Code stores its MCP
209
+ * this repository's word, and the fact underneath it — Roo Code stores its MCP
206
210
  * config per project, so there is no global file for pragma to be in — is the
207
211
  * thing that explains the row.
208
212
  */
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Pins the hand-written comparison in `isSupportedNodeVersion` to the range the
3
+ * package actually declares.
4
+ *
5
+ * The implementation encodes `engines.node` as a chain of comparisons rather
6
+ * than parsing it, so nothing structural stops the two drifting apart. This
7
+ * suite closes that by building a SECOND, independent predicate directly from
8
+ * the declared range string and requiring both to agree across a version grid.
9
+ * A change to `engines.node` therefore moves the oracle and fails here until the
10
+ * implementation is moved with it — which a table of hand-written expectations
11
+ * could not do, since its cheapest repair is editing the expectations.
12
+ */
13
+
14
+ import { describe, expect, it } from "vitest";
15
+ import { SUPPORTED_NODE_RANGE } from "../../../constants.js";
16
+ import { isSupportedNodeVersion } from "./isSupportedNodeVersion.js";
17
+
18
+ /**
19
+ * Evaluate a version against a range of the form `>=22.18 <23 || >=23.6`.
20
+ *
21
+ * Deliberately naive and deliberately separate from the implementation: it is
22
+ * an oracle, not a utility, and it throws on any comparator shape it was not
23
+ * written for so a widened range fails loudly instead of silently passing.
24
+ */
25
+ function satisfiesDeclaredRange(version: string, range: string): boolean {
26
+ // Semver excludes prereleases from a range unless a comparator opts in, and
27
+ // none of ours does. Encoded here too, so the oracle and the implementation
28
+ // are held to the same rule rather than agreeing by omission.
29
+ if (version.includes("-")) return false;
30
+
31
+ const rank = (major: number, minor: number) => major * 1_000_000 + minor;
32
+ const [major = Number.NaN, minor = Number.NaN] = version
33
+ .split(".")
34
+ .map(Number);
35
+
36
+ return range.split("||").some((clause) =>
37
+ clause
38
+ .trim()
39
+ .split(/\s+/)
40
+ .every((comparator) => {
41
+ // A comparator may be major-only (`<23`) or major.minor (`>=22.18`).
42
+ const parsed = /^(>=|<)(\d+)(?:\.(\d+))?$/.exec(comparator);
43
+ if (!parsed)
44
+ throw new Error(`oracle cannot parse comparator: ${comparator}`);
45
+ const [, operator, boundMajor, boundMinor] = parsed;
46
+ const actual = rank(major, minor);
47
+ const bound = rank(Number(boundMajor), Number(boundMinor ?? 0));
48
+ return operator === ">=" ? actual >= bound : actual < bound;
49
+ }),
50
+ );
51
+ }
52
+
53
+ const GRID = [
54
+ "20.19.0",
55
+ "21.7.3",
56
+ "22.0.0",
57
+ "22.12.0",
58
+ "22.17.9",
59
+ "22.18.0",
60
+ "22.18.1",
61
+ "22.99.0",
62
+ "23.0.0",
63
+ "23.5.9",
64
+ "23.6.0",
65
+ "23.9.0",
66
+ "24.0.0",
67
+ "24.18.1",
68
+ "25.0.0",
69
+ "26.0.0",
70
+ // Prereleases: numerically inside the range, excluded by semver.
71
+ "22.18.0-pre",
72
+ "23.6.0-nightly",
73
+ "24.0.0-rc.1",
74
+ ];
75
+
76
+ describe("isSupportedNodeVersion — agrees with the declared engines range", () => {
77
+ it.each(GRID)("matches the oracle for %s", (version) => {
78
+ expect(isSupportedNodeVersion(version)).toBe(
79
+ satisfiesDeclaredRange(version, SUPPORTED_NODE_RANGE),
80
+ );
81
+ });
82
+
83
+ // The oracle is only trustworthy if it can disagree; prove it discriminates
84
+ // rather than returning a constant.
85
+ it("uses an oracle that actually discriminates", () => {
86
+ const verdicts = GRID.map((v) =>
87
+ satisfiesDeclaredRange(v, SUPPORTED_NODE_RANGE),
88
+ );
89
+ expect(verdicts).toContain(true);
90
+ expect(verdicts).toContain(false);
91
+ });
92
+
93
+ it("rejects prerelease builds whose numeric part is in range", () => {
94
+ // Asserted directly, not only through the oracle: if both sides dropped the
95
+ // rule together the grid would still agree and prove nothing.
96
+ for (const pre of ["22.18.0-pre", "23.6.0-nightly", "24.0.0-rc.1"]) {
97
+ expect(isSupportedNodeVersion(pre)).toBe(false);
98
+ }
99
+ // The same versions without the suffix are supported, so it is the suffix
100
+ // doing the work and not the numbers.
101
+ for (const stable of ["22.18.0", "23.6.0", "24.0.0"]) {
102
+ expect(isSupportedNodeVersion(stable)).toBe(true);
103
+ }
104
+ });
105
+
106
+ it("fails closed on a version it cannot parse", () => {
107
+ for (const bad of ["not-a-version", "", "v24.1.0", "24"]) {
108
+ expect(isSupportedNodeVersion(bad)).toBe(false);
109
+ }
110
+ });
111
+ });
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Whether a Node.js version satisfies the range this package declares in its
3
+ * own `engines.node`, surfaced as `SUPPORTED_NODE_RANGE` in `constants.ts`.
4
+ *
5
+ * The range is expressed here as a comparison rather than parsed: the CLI
6
+ * carries no semver dependency, and one range does not justify a range parser.
7
+ * What keeps the two honest is `isSupportedNodeVersion.test.ts`, which builds an
8
+ * independent predicate straight from the declared range string and requires
9
+ * the two to agree across a version grid — so a change to `engines.node` that
10
+ * is not mirrored here fails the suite rather than shipping.
11
+ *
12
+ * A major-only comparison cannot express this range: it cannot say "22.18 but
13
+ * not 22.0", and it cannot exclude the 23.0–23.5 window.
14
+ *
15
+ * @param version - A Node version string, e.g. `process.versions.node`.
16
+ * @returns Whether the version falls inside the declared range.
17
+ */
18
+ export function isSupportedNodeVersion(version: string): boolean {
19
+ // A semver range excludes prereleases unless a comparator opts into them, and
20
+ // `engines.node` opts into none. So `22.18.0-pre` is outside the declared
21
+ // range even though its numeric part is inside it, and reporting it as
22
+ // supported would be doctor passing a runtime the install contract rejects.
23
+ if (version.includes("-")) return false;
24
+
25
+ const [rawMajor, rawMinor] = version.split(".");
26
+ const major = Number(rawMajor);
27
+ const minor = Number(rawMinor);
28
+
29
+ if (!Number.isInteger(major) || !Number.isInteger(minor)) return false;
30
+ if (major < 22) return false;
31
+ if (major === 22) return minor >= 18;
32
+ if (major === 23) return minor >= 6;
33
+ return true;
34
+ }
@@ -45,6 +45,12 @@ describe("commandOf — one executable, either entry shape", () => {
45
45
  });
46
46
  });
47
47
 
48
+ // Every test in this suite writes into a temp directory and then resolves
49
+ // against it, so each one is bound by filesystem latency rather than by any
50
+ // work of its own. Vitest's 5s default is comfortable on an idle machine and
51
+ // marginal on a loaded one — this suite has timed out in CI while asserting
52
+ // nothing slow. Ten seconds keeps the assertions meaningful without making a
53
+ // genuine hang cheap to ignore.
48
54
  describe("commandResolves — the host's own resolution rules", () => {
49
55
  const dirs: string[] = [];
50
56
  const tmp = (): string => {
@@ -97,4 +103,4 @@ describe("commandResolves — the host's own resolution rules", () => {
97
103
  await commandResolves("./local-bin", dir, hostOf("linux", dir)),
98
104
  ).toBe(true);
99
105
  });
100
- });
106
+ }, 10_000);
@@ -38,17 +38,26 @@ import type {
38
38
  ConfigDetection,
39
39
  } from "../../setup/operations/index.js";
40
40
  import {
41
+ blockedMcpGroups,
42
+ describeEditorSource,
43
+ type FsProbe,
44
+ firstLspBlock,
45
+ firstMcpBlock,
46
+ installableEditors,
47
+ LSP_SKIP_REMEDY,
41
48
  type LspDetection,
49
+ lspBlockReason,
42
50
  lspEditorNames,
43
51
  lspSkipReason,
44
52
  type McpDetection,
53
+ mcpBlockReason,
45
54
  mcpGroupState,
46
55
  mcpWriteState,
47
56
  type SkillsDetection,
48
57
  skillsSkipReason,
49
58
  skillsSkipRemedy,
50
59
  } from "../../setup/operations/index.js";
51
- import { shortenPath, TARGET_IDS } from "../../setup/plan.js";
60
+ import { type Roots, shortenPath, TARGET_IDS } from "../../setup/plan.js";
52
61
  import { MCP_NO_LOCATION } from "../../setup/targets.js";
53
62
  import type { CheckItem, CheckResult, Scope } from "../types.js";
54
63
  import { checkShellCompletions } from "./checkShellCompletions.js";
@@ -88,15 +97,78 @@ const configHealth = (
88
97
  return { status: "pass", detail: `${path} — valid` };
89
98
  };
90
99
 
91
- /** The `lsp` row — the first this report has ever had. */
92
- const lspHealth = (d: LspDetection): Health => {
100
+ /**
101
+ * The `lsp` row: one sub-item per editor, saying HOW it was found and where
102
+ * pragma stands in it.
103
+ *
104
+ * Provenance per editor is the point. A colleague whose macOS `setup lsp`
105
+ * reported nothing had VS Code installed the whole time, and a one-line row
106
+ * ("not installed in …", or a bare skip) could not tell them whether pragma had
107
+ * missed the editor or found it and could not act. Per the owner, 2026-09-16,
108
+ * this report says per editor how it was found and why a step was skipped.
109
+ *
110
+ * The row's own status keeps the report's invariant that every `available` row
111
+ * has a remedy: any editor that CAN be installed into makes the row
112
+ * `available`, whose derived fix is `pragma setup lsp`. A row where NOTHING is
113
+ * installed and nothing is installable is a `skip` carrying the first block's
114
+ * own remedy — a Nix declaration, a `chmod`, or the palette command — never
115
+ * the derived command, which would only reproduce the skip.
116
+ *
117
+ * An INSTALLED editor is a `pass` even when its extensions folder is blocked.
118
+ * The copy is there and works; the block only says this command cannot CHANGE
119
+ * it. Reporting a skip instead told the owner of a site-managed folder that
120
+ * already held the extension to chmod it and install again, and made a machine
121
+ * that is installed everywhere it can be a permanent `○ lsp`.
122
+ */
123
+ const lspHealth = (d: LspDetection, roots: Roots): Health => {
93
124
  if (d.state === "unknown") {
94
- return { status: "skip", detail: lspSkipReason(d) };
125
+ return {
126
+ status: "skip",
127
+ detail: lspSkipReason(d),
128
+ remedy: LSP_SKIP_REMEDY,
129
+ };
130
+ }
131
+ const items: CheckItem[] = d.editors.map((e): CheckItem => {
132
+ const reason = e.installed ? undefined : lspBlockReason(e);
133
+ const state = reason ?? (e.installed ? "installed" : "not installed");
134
+ return {
135
+ label: e.editor.name,
136
+ status: e.installed
137
+ ? "pass"
138
+ : reason !== undefined
139
+ ? "skip"
140
+ : "available",
141
+ detail: `${describeEditorSource(e, roots)} · ${state}`,
142
+ };
143
+ });
144
+ const installable = installableEditors(d);
145
+ if (installable.length > 0) {
146
+ return {
147
+ status: "available",
148
+ detail: `not installed in ${installable.map((e) => e.editor.name).join(", ")}`,
149
+ items,
150
+ };
151
+ }
152
+ // Nothing installable. The row is the block's only when nothing is installed
153
+ // either — otherwise the honest headline is what IS in place.
154
+ const block = d.editors.some((e) => e.installed)
155
+ ? undefined
156
+ : firstLspBlock(d);
157
+ if (block !== undefined) {
158
+ return {
159
+ status: "skip",
160
+ // The headline names the editor; the item's own detail does not, because
161
+ // its label already has.
162
+ detail: block.headline,
163
+ items,
164
+ remedy: block.remedy,
165
+ };
95
166
  }
96
- const editors = lspEditorNames(d).join(", ");
97
- return d.state === "installed"
98
- ? { status: "pass", detail: `installed in ${editors}` }
99
- : { status: "available", detail: `not installed in ${editors}` };
167
+ return {
168
+ status: "pass",
169
+ detail: `installed in ${lspEditorNames(d).join(", ")}`,
170
+ items,
171
+ };
100
172
  };
101
173
 
102
174
  /**
@@ -118,6 +190,12 @@ async function mcpHealth(
118
190
  const items: CheckItem[] = d.groups.map((group) => {
119
191
  const state = mcpGroupState(d, group.path);
120
192
  const label = shortenPath(group.path, roots);
193
+ // A file pragma must not write is reported as a skip, not as an
194
+ // `available` whose `fix:` would fail the moment it ran.
195
+ const blocked = mcpBlockReason(d, group, roots);
196
+ if (blocked !== undefined) {
197
+ return { label, status: "skip" as const, detail: blocked };
198
+ }
121
199
  if (state === "absent") {
122
200
  return { label, status: "available", detail: "not registered" };
123
201
  }
@@ -132,6 +210,22 @@ async function mcpHealth(
132
210
  : `registered, but \`${MCP_SERVER_NAME}\` is not on PATH`,
133
211
  };
134
212
  });
213
+ // Every location blocked means the row's whole answer is the block: it
214
+ // carries the first one's remedy — the entry to declare by hand — rather than
215
+ // the derived `pragma setup mcp`, which would only reproduce the skip.
216
+ const blockedGroups = blockedMcpGroups(d);
217
+ const block =
218
+ blockedGroups.length === d.groups.length
219
+ ? firstMcpBlock(d, roots)
220
+ : undefined;
221
+ if (block !== undefined) {
222
+ return {
223
+ status: "skip",
224
+ detail: block.reason,
225
+ items,
226
+ remedy: block.remedy,
227
+ };
228
+ }
135
229
  const configured = items.filter((item) => item.status === "pass").length;
136
230
  const failing = items.filter((item) => item.status === "fail").length;
137
231
  if (failing > 0) {
@@ -141,6 +235,7 @@ async function mcpHealth(
141
235
  items,
142
236
  };
143
237
  }
238
+ const writable = items.length - blockedGroups.length;
144
239
  if (configured === 0) {
145
240
  // Absence in the LOCAL PROJECT is not a failure: registering there is
146
241
  // opt-in, so a repository with no checked-in MCP config is healthy. The
@@ -153,13 +248,13 @@ async function mcpHealth(
153
248
  }
154
249
  : {
155
250
  status: "available",
156
- detail: `not registered in any of ${items.length} config files`,
251
+ detail: `not registered in any of ${writable} config files`,
157
252
  items,
158
253
  };
159
254
  }
160
255
  return {
161
- status: configured === items.length ? "pass" : "available",
162
- detail: `registered in ${configured} of ${items.length} config files`,
256
+ status: configured === writable ? "pass" : "available",
257
+ detail: `registered in ${configured} of ${writable} config files`,
163
258
  items,
164
259
  };
165
260
  }
@@ -267,7 +362,7 @@ async function healthOf(
267
362
  };
268
363
  }
269
364
  case "lsp":
270
- return lspHealth(row.detection as LspDetection);
365
+ return lspHealth(row.detection as LspDetection, roots);
271
366
  case "mcp":
272
367
  return mcpHealth(row.detection as McpDetection, row.scope, rt.cwd, roots);
273
368
  default:
@@ -406,15 +501,21 @@ export const fixCommandFor = (id: string, scope: Scope, bin: string): string =>
406
501
  *
407
502
  * @param rt - The per-invocation runtime.
408
503
  * @param bin - The binary name the `fix:` lines are derived from.
504
+ * @param probe - The writability filesystem seam, threaded to every row's
505
+ * detection. Defaults to the real one; a test injects a fixture to reach the
506
+ * blocked arms — a `/nix/store` path, a directory that refuses `W_OK` —
507
+ * through THIS entry point, rather than opening the row bodies up as exports
508
+ * nothing in production would call.
409
509
  * @returns One {@link CheckResult} per (target, scope), plus one per scope.
410
510
  * @note Impure — every target's detection reads the real filesystem.
411
511
  */
412
512
  export async function scopedChecks(
413
513
  rt: PragmaRuntime,
414
514
  bin: string,
515
+ probe?: FsProbe,
415
516
  ): Promise<CheckResult[]> {
416
517
  const roots = await resolveRoots(rt);
417
- const detected = await detectTargets(rt, [...TARGET_IDS], "both");
518
+ const detected = await detectTargets(rt, [...TARGET_IDS], "both", probe);
418
519
  const [targetRows, inventoryRows] = await Promise.all([
419
520
  Promise.all(
420
521
  detected.map(async (row): Promise<CheckResult> => {