@canonical/pragma-cli 0.35.0 → 0.37.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 (295) hide show
  1. package/README.md +2 -2
  2. package/bundled-skills/add-standard/SKILL.md +338 -0
  3. package/bundled-skills/adoption-a1-styles/SKILL.md +269 -0
  4. package/bundled-skills/adoption-a2-components/SKILL.md +291 -0
  5. package/bundled-skills/adoption-a3-forms/SKILL.md +260 -0
  6. package/bundled-skills/anatomy-author/ANATOMY_DSL_SPEC.md +406 -0
  7. package/bundled-skills/anatomy-author/SKILL.md +1226 -0
  8. package/bundled-skills/design-auditor/SKILL.md +455 -0
  9. package/bundled-skills/implement-component/SKILL.md +333 -0
  10. package/bundled-skills/specify-component/SKILL.md +331 -0
  11. package/bundled-skills/specify-pattern/SKILL.md +292 -0
  12. package/dist/package.json +24 -20
  13. package/dist/pragma.conf.js +266 -34
  14. package/dist/pragma.conf.js.map +1 -1
  15. package/dist/src/bin.js +1 -1
  16. package/dist/src/bin.js.map +1 -1
  17. package/dist/src/capabilities/capabilities/hints.js +1 -1
  18. package/dist/src/capabilities/capabilities/hints.js.map +1 -1
  19. package/dist/src/capabilities/colophon/colophon.render.js +1 -1
  20. package/dist/src/capabilities/colophon/colophon.render.js.map +1 -1
  21. package/dist/src/capabilities/create/create.verb.js +2 -1
  22. package/dist/src/capabilities/create/create.verb.js.map +1 -1
  23. package/dist/src/capabilities/create/createSurface.generated.js +23 -1
  24. package/dist/src/capabilities/create/createSurface.generated.js.map +1 -1
  25. package/dist/src/capabilities/create/mount.js +4 -3
  26. package/dist/src/capabilities/create/mount.js.map +1 -1
  27. package/dist/src/capabilities/doctor/checks/checkNodeVersion.js +11 -6
  28. package/dist/src/capabilities/doctor/checks/checkNodeVersion.js.map +1 -1
  29. package/dist/src/capabilities/doctor/checks/checkPackageRefs.js +60 -4
  30. package/dist/src/capabilities/doctor/checks/checkPackageRefs.js.map +1 -1
  31. package/dist/src/capabilities/doctor/checks/checkShellCompletions.js +8 -8
  32. package/dist/src/capabilities/doctor/checks/checkShellCompletions.js.map +1 -1
  33. package/dist/src/capabilities/doctor/checks/harnessInventory.js +195 -0
  34. package/dist/src/capabilities/doctor/checks/harnessInventory.js.map +1 -0
  35. package/dist/src/capabilities/doctor/checks/index.js +6 -6
  36. package/dist/src/capabilities/doctor/checks/isSupportedNodeVersion.js +38 -0
  37. package/dist/src/capabilities/doctor/checks/isSupportedNodeVersion.js.map +1 -0
  38. package/dist/src/capabilities/doctor/checks/targetHealth.js +194 -50
  39. package/dist/src/capabilities/doctor/checks/targetHealth.js.map +1 -1
  40. package/dist/src/capabilities/doctor/doctor.render.js +43 -64
  41. package/dist/src/capabilities/doctor/doctor.render.js.map +1 -1
  42. package/dist/src/capabilities/doctor/doctor.verb.js +6 -2
  43. package/dist/src/capabilities/doctor/doctor.verb.js.map +1 -1
  44. package/dist/src/capabilities/doctor/runChecks.js +14 -11
  45. package/dist/src/capabilities/doctor/runChecks.js.map +1 -1
  46. package/dist/src/capabilities/graph/query.render.js +2 -2
  47. package/dist/src/capabilities/graph/query.render.js.map +1 -1
  48. package/dist/src/capabilities/info/collectInfo.js +8 -2
  49. package/dist/src/capabilities/info/collectInfo.js.map +1 -1
  50. package/dist/src/capabilities/info/info.render.js +6 -2
  51. package/dist/src/capabilities/info/info.render.js.map +1 -1
  52. package/dist/src/capabilities/prompt/prompt.render.js +2 -2
  53. package/dist/src/capabilities/prompt/prompt.render.js.map +1 -1
  54. package/dist/src/capabilities/setup/buildPlan.js +43 -39
  55. package/dist/src/capabilities/setup/buildPlan.js.map +1 -1
  56. package/dist/src/capabilities/setup/mode.js +2 -2
  57. package/dist/src/capabilities/setup/operations/index.js +4 -4
  58. package/dist/src/capabilities/setup/operations/index.js.map +1 -1
  59. package/dist/src/capabilities/setup/operations/setupCompletions.js +6 -2
  60. package/dist/src/capabilities/setup/operations/setupCompletions.js.map +1 -1
  61. package/dist/src/capabilities/setup/operations/setupConfig.js +4 -2
  62. package/dist/src/capabilities/setup/operations/setupConfig.js.map +1 -1
  63. package/dist/src/capabilities/setup/operations/setupGenerator.js +156 -58
  64. package/dist/src/capabilities/setup/operations/setupGenerator.js.map +1 -1
  65. package/dist/src/capabilities/setup/operations/setupLsp.js +136 -38
  66. package/dist/src/capabilities/setup/operations/setupLsp.js.map +1 -1
  67. package/dist/src/capabilities/setup/operations/setupMcp.js +101 -52
  68. package/dist/src/capabilities/setup/operations/setupMcp.js.map +1 -1
  69. package/dist/src/capabilities/setup/operations/setupSkills.js +329 -49
  70. package/dist/src/capabilities/setup/operations/setupSkills.js.map +1 -1
  71. package/dist/src/capabilities/setup/plan.js.map +1 -1
  72. package/dist/src/capabilities/setup/plan.render.js +144 -66
  73. package/dist/src/capabilities/setup/plan.render.js.map +1 -1
  74. package/dist/src/capabilities/setup/setup.render.js +1 -1
  75. package/dist/src/capabilities/setup/setup.verb.js +165 -63
  76. package/dist/src/capabilities/setup/setup.verb.js.map +1 -1
  77. package/dist/src/capabilities/setup/targets.js +122 -51
  78. package/dist/src/capabilities/setup/targets.js.map +1 -1
  79. package/dist/src/capabilities/shared/index.js +7 -6
  80. package/dist/src/capabilities/shared/index.js.map +1 -1
  81. package/dist/src/capabilities/shared/packageManager.js +300 -54
  82. package/dist/src/capabilities/shared/packageManager.js.map +1 -1
  83. package/dist/src/capabilities/skill/discover.js +97 -11
  84. package/dist/src/capabilities/skill/discover.js.map +1 -1
  85. package/dist/src/capabilities/skill/render.js +2 -2
  86. package/dist/src/capabilities/skill/render.js.map +1 -1
  87. package/dist/src/capabilities/sources/installSkills.js +145 -3
  88. package/dist/src/capabilities/sources/installSkills.js.map +1 -1
  89. package/dist/src/capabilities/sources/runUpdate.js +68 -2
  90. package/dist/src/capabilities/sources/runUpdate.js.map +1 -1
  91. package/dist/src/capabilities/sources/update.verb.js +3 -3
  92. package/dist/src/capabilities/sources/update.verb.js.map +1 -1
  93. package/dist/src/capabilities/upgrade/runUpgrade.js +43 -7
  94. package/dist/src/capabilities/upgrade/runUpgrade.js.map +1 -1
  95. package/dist/src/capabilities/upgrade/upgrade.render.js +10 -2
  96. package/dist/src/capabilities/upgrade/upgrade.render.js.map +1 -1
  97. package/dist/src/constants.js +13 -2
  98. package/dist/src/constants.js.map +1 -1
  99. package/dist/src/kernel/index.js +11 -2
  100. package/dist/src/kernel/index.js.map +1 -1
  101. package/dist/src/kernel/interactivity.js +68 -0
  102. package/dist/src/kernel/interactivity.js.map +1 -0
  103. package/dist/src/kernel/packs/renderPack.js +3 -3
  104. package/dist/src/kernel/packs/renderPack.js.map +1 -1
  105. package/dist/src/kernel/packs/resolveEntity.js +63 -12
  106. package/dist/src/kernel/packs/resolveEntity.js.map +1 -1
  107. package/dist/src/kernel/packs/runBodies.js +57 -4
  108. package/dist/src/kernel/packs/runBodies.js.map +1 -1
  109. package/dist/src/kernel/packs/schema.js +54 -2
  110. package/dist/src/kernel/packs/schema.js.map +1 -1
  111. package/dist/src/kernel/packs/sparql/applyFilters.js +104 -5
  112. package/dist/src/kernel/packs/sparql/applyFilters.js.map +1 -1
  113. package/dist/src/kernel/packs/sparql/buildLookupQuery.js +310 -43
  114. package/dist/src/kernel/packs/sparql/buildLookupQuery.js.map +1 -1
  115. package/dist/src/kernel/packs/types.js.map +1 -1
  116. package/dist/src/kernel/project/cli/buildProgram.js +1 -1
  117. package/dist/src/kernel/project/cli/buildProgram.js.map +1 -1
  118. package/dist/src/kernel/project/cli/dispatch.js +46 -24
  119. package/dist/src/kernel/project/cli/dispatch.js.map +1 -1
  120. package/dist/src/kernel/project/cli/globalFlags.js +4 -1
  121. package/dist/src/kernel/project/cli/globalFlags.js.map +1 -1
  122. package/dist/src/kernel/project/cli/helpFormat.js +1 -2
  123. package/dist/src/kernel/project/cli/helpFormat.js.map +1 -1
  124. package/dist/src/kernel/project/cli/rootHelp.js +42 -9
  125. package/dist/src/kernel/project/cli/rootHelp.js.map +1 -1
  126. package/dist/src/kernel/project/mcp/registerVerb.js +16 -1
  127. package/dist/src/kernel/project/mcp/registerVerb.js.map +1 -1
  128. package/dist/src/kernel/render/renderers.js +1 -1
  129. package/dist/src/kernel/render/style.js +29 -9
  130. package/dist/src/kernel/render/style.js.map +1 -1
  131. package/dist/src/kernel/render/vocabulary.js +174 -0
  132. package/dist/src/kernel/render/vocabulary.js.map +1 -0
  133. package/dist/src/kernel/runtime/graphpack/embedded/pack.generated.js +2 -3
  134. package/dist/src/kernel/runtime/graphpack/embedded/pack.generated.js.map +1 -1
  135. package/dist/src/kernel/runtime/graphpack/embedded/pack.index.generated.js +1 -1
  136. package/dist/src/kernel/runtime/graphpack/embedded/pack.index.generated.js.map +1 -1
  137. package/dist/src/kernel/runtime/graphpack/embedded/pack.manifest.generated.js +4 -0
  138. package/dist/src/kernel/runtime/graphpack/embedded/pack.manifest.generated.js.map +1 -0
  139. package/dist/src/kernel/runtime/graphpack/embedded.js +13 -6
  140. package/dist/src/kernel/runtime/graphpack/embedded.js.map +1 -1
  141. package/dist/src/kernel/runtime/graphpack/read.js +14 -9
  142. package/dist/src/kernel/runtime/graphpack/read.js.map +1 -1
  143. package/dist/src/kernel/runtime/loadSession.js +3 -1
  144. package/dist/src/kernel/runtime/loadSession.js.map +1 -1
  145. package/dist/src/kernel/runtime/probe.js +1 -1
  146. package/dist/src/kernel/runtime/probe.js.map +1 -1
  147. package/dist/src/kernel/runtime/refs/parseRef.js +17 -1
  148. package/dist/src/kernel/runtime/refs/parseRef.js.map +1 -1
  149. package/dist/src/kernel/runtime/refs/resolve.js +9 -3
  150. package/dist/src/kernel/runtime/refs/resolve.js.map +1 -1
  151. package/dist/src/kernel/spec/emitReference.js +1 -1
  152. package/dist/src/kernel/spec/emitReference.js.map +1 -1
  153. package/package.json +24 -20
  154. package/pragma.conf.ts +272 -34
  155. package/src/bin.ts +8 -1
  156. package/src/capabilities/block.parity.test.ts +25 -11
  157. package/src/capabilities/block.shipped.exec.test.ts +282 -0
  158. package/src/capabilities/block.tierRank.exec.test.ts +300 -0
  159. package/src/capabilities/capabilities/hints.ts +1 -1
  160. package/src/capabilities/colophon/colophon.render.ts +1 -2
  161. package/src/capabilities/colophon/colophon.test.ts +2 -2
  162. package/src/capabilities/config/collectConfigShow.test.ts +1 -0
  163. package/src/capabilities/create/byteEquality.test.ts +28 -1
  164. package/src/capabilities/create/create.test.ts +1 -0
  165. package/src/capabilities/create/create.verb.ts +2 -1
  166. package/src/capabilities/create/createGrammar.test.ts +5 -2
  167. package/src/capabilities/create/createSurface.generated.ts +23 -1
  168. package/src/capabilities/create/crossCli.subprocess.test.ts +10 -3
  169. package/src/capabilities/create/mount.ts +3 -3
  170. package/src/capabilities/distribution.parity.test.ts +4 -0
  171. package/src/capabilities/doctor/checks/checkNodeVersion.test.ts +49 -0
  172. package/src/capabilities/doctor/checks/checkNodeVersion.ts +11 -7
  173. package/src/capabilities/doctor/checks/checkPackageRefs.ts +69 -4
  174. package/src/capabilities/doctor/checks/checkShellCompletions.test.ts +6 -4
  175. package/src/capabilities/doctor/checks/checkShellCompletions.ts +8 -8
  176. package/src/capabilities/doctor/checks/harnessInventory.test.ts +219 -0
  177. package/src/capabilities/doctor/checks/harnessInventory.ts +298 -0
  178. package/src/capabilities/doctor/checks/index.ts +6 -6
  179. package/src/capabilities/doctor/checks/isSupportedNodeVersion.test.ts +111 -0
  180. package/src/capabilities/doctor/checks/isSupportedNodeVersion.ts +34 -0
  181. package/src/capabilities/doctor/checks/mcpCommand.test.ts +7 -1
  182. package/src/capabilities/doctor/checks/targetHealth.ts +229 -59
  183. package/src/capabilities/doctor/doctor.render.test.ts +108 -32
  184. package/src/capabilities/doctor/doctor.render.ts +56 -80
  185. package/src/capabilities/doctor/doctor.test.ts +247 -35
  186. package/src/capabilities/doctor/doctor.verb.ts +7 -2
  187. package/src/capabilities/doctor/runChecks.ts +14 -11
  188. package/src/capabilities/doctor/types.ts +15 -24
  189. package/src/capabilities/emptyRecovery.test.ts +10 -2
  190. package/src/capabilities/graph/query.render.ts +2 -2
  191. package/src/capabilities/info/collectInfo.ts +8 -1
  192. package/src/capabilities/info/info.render.ts +6 -2
  193. package/src/capabilities/info/info.test.ts +37 -1
  194. package/src/capabilities/info/types.ts +10 -2
  195. package/src/capabilities/lazy.test.ts +26 -2
  196. package/src/capabilities/listLookup.shipped.exec.test.ts +340 -0
  197. package/src/capabilities/prompt/prompt.render.ts +2 -2
  198. package/src/capabilities/renderVocabularySync.test.ts +82 -0
  199. package/src/capabilities/resources/resources.test.ts +38 -2
  200. package/src/capabilities/scopeTypeSync.test.ts +29 -15
  201. package/src/capabilities/setup/buildPlan.ts +45 -41
  202. package/src/capabilities/setup/index.ts +1 -1
  203. package/src/capabilities/setup/mode.ts +2 -2
  204. package/src/capabilities/setup/operations/index.ts +6 -1
  205. package/src/capabilities/setup/operations/setupCompletions.ts +9 -2
  206. package/src/capabilities/setup/operations/setupConfig.ts +7 -2
  207. package/src/capabilities/setup/operations/setupGenerator.ts +227 -84
  208. package/src/capabilities/setup/operations/setupLsp.ts +203 -38
  209. package/src/capabilities/setup/operations/setupMcp.ts +139 -62
  210. package/src/capabilities/setup/operations/setupSkills.ts +399 -60
  211. package/src/capabilities/setup/plan.render.ts +171 -76
  212. package/src/capabilities/setup/plan.ts +17 -12
  213. package/src/capabilities/setup/setup.mcp.correctness.test.ts +9 -9
  214. package/src/capabilities/setup/setup.render.test.ts +50 -36
  215. package/src/capabilities/setup/setup.render.ts +1 -1
  216. package/src/capabilities/setup/setup.test.ts +1126 -50
  217. package/src/capabilities/setup/setup.verb.ts +174 -68
  218. package/src/capabilities/setup/targets.ts +138 -60
  219. package/src/capabilities/setup/types.ts +10 -18
  220. package/src/capabilities/shared/index.ts +18 -6
  221. package/src/capabilities/shared/packageManager.test.ts +421 -6
  222. package/src/capabilities/shared/packageManager.ts +394 -59
  223. package/src/capabilities/skill/bundledSkills.test.ts +347 -0
  224. package/src/capabilities/skill/discover.ts +100 -11
  225. package/src/capabilities/skill/render.ts +2 -2
  226. package/src/capabilities/skill/skill.test.ts +17 -4
  227. package/src/capabilities/sources/installSkills.ts +181 -3
  228. package/src/capabilities/sources/runUpdate.ts +78 -2
  229. package/src/capabilities/sources/sources.test.ts +228 -0
  230. package/src/capabilities/sources/update.verb.ts +3 -3
  231. package/src/capabilities/surface.test.ts +14 -6
  232. package/src/capabilities/upgrade/runUpgrade.ts +45 -6
  233. package/src/capabilities/upgrade/types.ts +10 -2
  234. package/src/capabilities/upgrade/upgrade.render.ts +9 -2
  235. package/src/capabilities/upgrade/upgrade.test.ts +72 -0
  236. package/src/constants.ts +14 -1
  237. package/src/identity.test.ts +18 -5
  238. package/src/kernel/completion/entitySource.test.ts +15 -2
  239. package/src/kernel/completion/safety.test.ts +16 -9
  240. package/src/kernel/config/defaults.test.ts +19 -3
  241. package/src/kernel/config/readConfig.test.ts +1 -1
  242. package/src/kernel/index.ts +11 -2
  243. package/src/kernel/interactivity.test.ts +76 -0
  244. package/src/kernel/interactivity.ts +69 -0
  245. package/src/kernel/packs/compile.test.ts +92 -0
  246. package/src/kernel/packs/renderPack.ts +3 -3
  247. package/src/kernel/packs/resolveEntity.test.ts +75 -14
  248. package/src/kernel/packs/resolveEntity.ts +88 -24
  249. package/src/kernel/packs/runBodies.ts +84 -19
  250. package/src/kernel/packs/schema.ts +60 -4
  251. package/src/kernel/packs/sparql/applyFilters.ts +125 -4
  252. package/src/kernel/packs/sparql/buildLookupQuery.test.ts +290 -0
  253. package/src/kernel/packs/sparql/buildLookupQuery.ts +327 -45
  254. package/src/kernel/packs/toolDescription.test.ts +35 -1
  255. package/src/kernel/packs/types.ts +125 -2
  256. package/src/kernel/project/cli/buildProgram.test.ts +73 -2
  257. package/src/kernel/project/cli/buildProgram.ts +8 -1
  258. package/src/kernel/project/cli/dispatch.test.ts +5 -4
  259. package/src/kernel/project/cli/dispatch.ts +48 -25
  260. package/src/kernel/project/cli/globalFlags.ts +4 -1
  261. package/src/kernel/project/cli/helpFormat.ts +1 -2
  262. package/src/kernel/project/cli/rootHelp.ts +44 -8
  263. package/src/kernel/project/mcp/registerVerb.ts +20 -1
  264. package/src/kernel/render/prefixes.test.ts +2 -0
  265. package/src/kernel/render/renderers.ts +1 -1
  266. package/src/kernel/render/style.ts +32 -9
  267. package/src/kernel/render/vocabulary.ts +276 -0
  268. package/src/kernel/runtime/graphpack/embedded/pack.generated.ts +2 -3
  269. package/src/kernel/runtime/graphpack/embedded/pack.index.generated.ts +1 -1
  270. package/src/kernel/runtime/graphpack/embedded/pack.manifest.generated.ts +3 -0
  271. package/src/kernel/runtime/graphpack/embedded.ts +14 -6
  272. package/src/kernel/runtime/graphpack/graphpack.test.ts +31 -5
  273. package/src/kernel/runtime/graphpack/read.ts +14 -12
  274. package/src/kernel/runtime/loadSession.ts +3 -1
  275. package/src/kernel/runtime/probe.ts +1 -1
  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 +9 -3
  279. package/src/kernel/runtime/types.ts +17 -0
  280. package/src/kernel/spec/emitReference.ts +1 -1
  281. package/src/kernel/spec/types.ts +35 -6
  282. package/src/testing/PARITY_GAPS.ts +9 -3
  283. package/src/testing/behavioral/docExamples.test.ts +4 -2
  284. package/src/testing/behavioral/errorMatrix.mcp.test.ts +110 -7
  285. package/src/testing/behavioral/journeys.firstInstall.test.ts +7 -2
  286. package/src/testing/behavioral/journeys.packageStories.test.ts +6 -3
  287. package/src/testing/eval/__snapshots__/eval.test.ts.snap +26 -1
  288. package/src/testing/eval/cases/readNouns.ts +245 -9
  289. package/src/testing/fixtures/blockGraph.ts +28 -13
  290. package/src/testing/fixtures/graph/canonical.ts +95 -12
  291. package/src/testing/helpers/runCli.test.ts +44 -0
  292. package/src/testing/helpers/runCli.ts +27 -3
  293. package/dist/src/capabilities/shared/bands.js +0 -16
  294. package/dist/src/capabilities/shared/bands.js.map +0 -1
  295. package/src/capabilities/shared/bands.ts +0 -16
@@ -0,0 +1,195 @@
1
+ /**
2
+ * The harness inventory: one row per harness per scope, saying what was detected
3
+ * and whether pragma is registered in it.
4
+ *
5
+ * Every fact this needs was ALREADY COMPUTED and thrown away.
6
+ * `TargetGroup.harnessNames` carries every harness sharing a config file and
7
+ * both consumers reduced it to `shortenPath(group.path)`; `McpDetection`
8
+ * carries the `absent`/`registered`/`drifted` classification of every WRITE in
9
+ * that file. The only thing detection could NOT answer is the negative —
10
+ * `detectHarnesses` filters to hits, so a harness the machine does not have was
11
+ * unrepresentable — which is why the roll-up takes the REGISTRY alongside the
12
+ * groups: the registry supplies the universe, the groups supply the state.
13
+ *
14
+ * The state arrives PER HARNESS, never per file. A shared `.vscode/mcp.json`
15
+ * holds VS Code's `servers` and Cline's `mcpServers` as two independent
16
+ * entries, so the file's aggregate — `drifted`, when one key is current and the
17
+ * other absent — is the standing of NEITHER harness. Pairing each name with
18
+ * the write it owns is the caller's job (it holds the registry's `mcpKey`s and
19
+ * the group's writes); by the time a group reaches this module the association
20
+ * is resolved and every name carries its own state.
21
+ *
22
+ * Three deliberate constraints:
23
+ *
24
+ * 1. **No `@canonical/harnesses` import, of any kind.** `capabilities/lazy.test`
25
+ * forbids one from any file statically reachable from `capabilities/index`,
26
+ * and its regex has no `import type` exemption. This module is reached only
27
+ * through `runChecks`'s dynamic import today, but it is written so that
28
+ * staying legal never depends on that: the inputs are declared here as
29
+ * narrow structural records.
30
+ *
31
+ * 2. **Narrowed to DATA, so `mutates: false` is enforced rather than asserted.**
32
+ * `McpDetection` is write-capable — it carries `writeMcpConfigTargets` and
33
+ * `removeMcpConfigFrom` as live function references. Projecting it down to
34
+ * {@link InventoryGroup} before it reaches this file means a read-only
35
+ * report literally cannot reach a writer.
36
+ *
37
+ * 3. **It never invents a failure.** The states an inventory can report map onto
38
+ * `pass`/`available`/`skip` and NEVER `fail` (see `../types.ts`): a machine
39
+ * that simply does not have Windsurf is not a broken machine, and the `mcp`
40
+ * row already owns the actionable finding for the harnesses it does have.
41
+ * Reporting it twice is how a failure count becomes noise.
42
+ *
43
+ * Placement: this is a doctor helper because doctor is its only consumer today,
44
+ * and `shared/` is explicitly not a utility drawer (see `shared/index.ts`). It
45
+ * is written to MOVE there unchanged — pure, no capability imports beyond the
46
+ * scope type — the moment `setup`'s detection summary becomes the second caller.
47
+ */
48
+ /** Whether a state means "this harness is on the machine". */
49
+ export const isDetectedState = (state) => state === "registered" || state === "drifted" || state === "detected";
50
+ /**
51
+ * Roll one scope's detected config groups up against the registry into one row
52
+ * per harness.
53
+ *
54
+ * Groups are keyed by harness NAME because that is the only identity
55
+ * `TargetGroup` records; the registry is the map back to an id, which is why it
56
+ * is a parameter rather than a lookup. A harness naming no registry row is
57
+ * ignored rather than invented — the inventory reports on the registry, not on
58
+ * whatever a group happened to say.
59
+ *
60
+ * @param registry - Every known harness, each flagged for scope membership.
61
+ * @param groups - This scope's detected config files (narrowed, path-shortened),
62
+ * each naming the harnesses that share it with their own per-write state.
63
+ * @param scope - The scope being rolled up.
64
+ * @returns One row per registry harness, in registry order.
65
+ * @note Pure — it reads no filesystem and holds no live effect.
66
+ */
67
+ export function harnessInventory(registry, groups, scope) {
68
+ const states = new Map();
69
+ const locations = new Map();
70
+ for (const group of groups) {
71
+ for (const member of group.harnesses) {
72
+ const paths = locations.get(member.name) ?? [];
73
+ paths.push(group.path);
74
+ locations.set(member.name, paths);
75
+ // A harness sharing two files is registered only where EVERY one of them
76
+ // carries the current entry — the same "all or nothing" rule
77
+ // `aggregateMcpStates` applies across the writes within a single file.
78
+ const prior = states.get(member.name);
79
+ states.set(member.name, prior === undefined || prior === member.state
80
+ ? member.state
81
+ : "drifted");
82
+ }
83
+ }
84
+ return registry.map((harness) => {
85
+ const detected = states.get(harness.name);
86
+ const paths = locations.get(harness.name) ?? [];
87
+ const state = detected === undefined
88
+ ? harness.inScope
89
+ ? "undetected"
90
+ : "unscoped"
91
+ : detected === "registered"
92
+ ? "registered"
93
+ : detected === "drifted"
94
+ ? "drifted"
95
+ : "detected";
96
+ return {
97
+ harnessId: harness.id,
98
+ harnessName: harness.name,
99
+ scope,
100
+ state,
101
+ locations: paths,
102
+ };
103
+ });
104
+ }
105
+ /** The per-item status each state renders as. Never `fail` — see the docblock. */
106
+ const STATE_STATUS = {
107
+ registered: "pass",
108
+ drifted: "available",
109
+ detected: "available",
110
+ undetected: "skip",
111
+ unscoped: "skip",
112
+ };
113
+ /**
114
+ * The phrase each state reads as, scope-aware for the unscoped case.
115
+ *
116
+ * The unscoped line says WHERE the harness keeps its config rather than naming
117
+ * an internal partition. `no Global band` told the reader nothing: "band" was
118
+ * this repository's word, and the fact underneath it — VS Code stores its MCP
119
+ * config per project, so there is no global file for pragma to be in — is the
120
+ * thing that explains the row.
121
+ */
122
+ const stateDetail = (row) => {
123
+ switch (row.state) {
124
+ case "registered":
125
+ return "registered";
126
+ case "drifted":
127
+ return "registered, entry differs from current";
128
+ case "detected":
129
+ return "detected, not registered";
130
+ case "undetected":
131
+ return "not detected";
132
+ case "unscoped":
133
+ return row.scope === "global"
134
+ ? "keeps no global config — it is per-project only"
135
+ : "keeps no per-project config — it is global only";
136
+ }
137
+ };
138
+ /**
139
+ * Project inventory rows into doctor sub-items.
140
+ *
141
+ * The default listing is DETECTED HARNESSES ONLY — a report that names every
142
+ * tool the user does not have buries the ones they do. `--verbose` widens it to
143
+ * the whole registry, which is the only view in which `undetected`/`unscoped`
144
+ * rows appear at all.
145
+ *
146
+ * @param rows - This scope's inventory rows.
147
+ * @param verbose - Whether to list the whole registry rather than the hits.
148
+ * @returns The sub-items, harness name in the left column.
149
+ */
150
+ export function inventoryItems(rows, verbose) {
151
+ const shown = verbose
152
+ ? rows
153
+ : rows.filter((row) => isDetectedState(row.state));
154
+ return shown.map((row) => {
155
+ const where = row.locations.join(", ");
156
+ return {
157
+ label: row.harnessName,
158
+ status: STATE_STATUS[row.state],
159
+ detail: where ? `${stateDetail(row)} — ${where}` : stateDetail(row),
160
+ };
161
+ });
162
+ }
163
+ /**
164
+ * The whole inventory row for one scope: a listing, not a verdict.
165
+ *
166
+ * Its status is only ever `pass` (this scope has harnesses) or `skip` (it has
167
+ * none). It is the one scoped row that is not a setup target, so it derives no
168
+ * `fix:` line — the `mcp` and `skills` rows beside it own every action a
169
+ * harness can need, and a second row proposing the same command would be the
170
+ * "same finding seen twice" the report is built to avoid.
171
+ *
172
+ * @param rows - This scope's inventory rows.
173
+ * @param verbose - Whether the listing widens to the whole registry.
174
+ * @returns The status, headline and sub-items for the scope's inventory check.
175
+ */
176
+ export function inventoryHealth(rows, verbose) {
177
+ const detected = rows.filter((row) => isDetectedState(row.state));
178
+ const registered = rows.filter((row) => row.state === "registered");
179
+ const items = inventoryItems(rows, verbose);
180
+ if (detected.length === 0) {
181
+ return {
182
+ status: "skip",
183
+ detail: verbose
184
+ ? `none of ${rows.length} known harnesses detected`
185
+ : "no harnesses detected",
186
+ items,
187
+ };
188
+ }
189
+ return {
190
+ status: "pass",
191
+ detail: `${detected.length} detected · ${registered.length} registered${verbose ? ` · ${rows.length} known` : ""}`,
192
+ items,
193
+ };
194
+ }
195
+ //# sourceMappingURL=harnessInventory.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"harnessInventory.js","sourceRoot":"","sources":["../../../../../src/capabilities/doctor/checks/harnessInventory.ts"],"names":[],"mappings":"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"}
@@ -6,22 +6,22 @@
6
6
  * them apart from the report body is what lets `runChecks` stay a composition
7
7
  * — it orders and awaits, and knows nothing about what any check looks at.
8
8
  *
9
- * Two kinds live here, and the barrel exposes them differently. The UNBANDED
9
+ * Two kinds live here, and the barrel exposes them differently. The UNSCOPED
10
10
  * checks diagnose what `setup` cannot install — the Node floor, the CLI's own
11
11
  * version and install source, the pack answering this project's reads, and the
12
- * store — so each is its own named entry point. The BANDED rows are derived
13
- * per target, per band, from the setup target table rather than authored here,
12
+ * store — so each is its own named entry point. The SCOPED rows are derived
13
+ * per target, per scope, from the setup target table rather than authored here,
14
14
  * so they surface as the one generator that derives them, never as a list of
15
15
  * row names this file would then have to keep in step with `setup`.
16
16
  *
17
17
  * The per-target probes (the completions script check, the MCP command
18
- * resolution helpers) stay internal on purpose: `bandedChecks` is the only way
19
- * they are meant to be reached, and a row reachable past its band is a row
18
+ * resolution helpers) stay internal on purpose: `scopedChecks` is the only way
19
+ * they are meant to be reached, and a row reachable past its scope is a row
20
20
  * `doctor` can report and `setup` cannot fix.
21
21
  */
22
22
  export { checkKeStore } from "./checkKeStore.js";
23
23
  export { checkNodeVersion } from "./checkNodeVersion.js";
24
24
  export { checkPackageRefs } from "./checkPackageRefs.js";
25
25
  export { checkPragmaVersion } from "./checkPragmaVersion.js";
26
- export { bandedChecks } from "./targetHealth.js";
26
+ export { scopedChecks } from "./targetHealth.js";
27
27
  //# sourceMappingURL=index.js.map
@@ -0,0 +1,38 @@
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) {
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("-"))
24
+ return false;
25
+ const [rawMajor, rawMinor] = version.split(".");
26
+ const major = Number(rawMajor);
27
+ const minor = Number(rawMinor);
28
+ if (!Number.isInteger(major) || !Number.isInteger(minor))
29
+ return false;
30
+ if (major < 22)
31
+ return false;
32
+ if (major === 22)
33
+ return minor >= 18;
34
+ if (major === 23)
35
+ return minor >= 6;
36
+ return true;
37
+ }
38
+ //# sourceMappingURL=isSupportedNodeVersion.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"isSupportedNodeVersion.js","sourceRoot":"","sources":["../../../../../src/capabilities/doctor/checks/isSupportedNodeVersion.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,UAAU,sBAAsB,CAAC,OAAe;IACpD,8EAA8E;IAC9E,0EAA0E;IAC1E,uEAAuE;IACvE,4EAA4E;IAC5E,IAAI,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC;QAAE,OAAO,KAAK,CAAC;IAExC,MAAM,CAAC,QAAQ,EAAE,QAAQ,CAAC,GAAG,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAChD,MAAM,KAAK,GAAG,MAAM,CAAC,QAAQ,CAAC,CAAC;IAC/B,MAAM,KAAK,GAAG,MAAM,CAAC,QAAQ,CAAC,CAAC;IAE/B,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC;IACvE,IAAI,KAAK,GAAG,EAAE;QAAE,OAAO,KAAK,CAAC;IAC7B,IAAI,KAAK,KAAK,EAAE;QAAE,OAAO,KAAK,IAAI,EAAE,CAAC;IACrC,IAAI,KAAK,KAAK,EAAE;QAAE,OAAO,KAAK,IAAI,CAAC,CAAC;IACpC,OAAO,IAAI,CAAC;AACd,CAAC"}
@@ -1,7 +1,7 @@
1
1
  /**
2
- * The banded doctor rows, derived from the setup target table.
2
+ * The scoped doctor rows, derived from the setup target table.
3
3
  *
4
- * `doctor` and `setup` are the SAME LIST. Every banded row here carries a
4
+ * `doctor` and `setup` are the SAME LIST. Every scoped row here carries a
5
5
  * target id verbatim — `completions`, not "Shell completions"; `mcp`, not "MCP
6
6
  * configured" — because the row name IS the fix command's argument, and a user
7
7
  * who reads `✗ mcp` beside `fix: pragma setup mcp` can see that they are the
@@ -11,23 +11,32 @@
11
11
  *
12
12
  * Every row's detection comes from the target table's own `detect`, so the two
13
13
  * surfaces cannot disagree about what they looked at, and every `fix:` is
14
- * derived from the row's id and band rather than authored per check.
14
+ * derived from the row's id and scope rather than authored per check.
15
15
  *
16
- * Doctor's UNBANDED environment checks (Node version, the CLI's own version,
16
+ * Doctor's UNSCOPED environment checks (Node version, the CLI's own version,
17
17
  * pack refs, the store) diagnose things setup cannot install and stay outside
18
18
  * this list.
19
+ *
20
+ * ONE scoped row is not a target: `harnesses`, the per-scope inventory. It is an
21
+ * inventory of the machine the targets are measured against, not a target that
22
+ * can be set up, so it deliberately carries no `fix:` and can never be `fail`
23
+ * or `available` — see {@link inventoryChecks}. The bijection between the other
24
+ * rows and the setup table is untouched: `harnesses` is not a `TargetId`, and
25
+ * nothing derives a command from it.
19
26
  */
20
27
  import { MCP_SERVER_NAME } from "../../../constants.js";
21
28
  import { detectionFailure, detectTargets, resolveRoots, } from "../../setup/buildPlan.js";
22
- import { lspEditorNames, lspSkipReason, mcpGroupState, skillsSkipReason, } from "../../setup/operations/index.js";
29
+ import { lspEditorNames, lspSkipReason, mcpGroupState, mcpWriteState, skillsSkipReason, skillsSkipRemedy, } from "../../setup/operations/index.js";
23
30
  import { shortenPath, TARGET_IDS } from "../../setup/plan.js";
31
+ import { MCP_NO_LOCATION } from "../../setup/targets.js";
24
32
  import { checkShellCompletions } from "./checkShellCompletions.js";
33
+ import { harnessInventory, inventoryHealth, } from "./harnessInventory.js";
25
34
  import { commandResolves } from "./mcpCommand.js";
26
35
  /** The `config` row: the global config file exists and parses. */
27
36
  const configHealth = (d, roots) => {
28
37
  const path = shortenPath(d.path, roots);
29
38
  if (!d.exists)
30
- return { status: "available", detail: `${path} not created` };
39
+ return { status: "available", detail: `${path} does not exist yet` };
31
40
  if (!d.parses) {
32
41
  return { status: "fail", detail: `${path} cannot be parsed as JSON` };
33
42
  }
@@ -40,8 +49,8 @@ const lspHealth = (d) => {
40
49
  }
41
50
  const editors = lspEditorNames(d).join(", ");
42
51
  return d.state === "installed"
43
- ? { status: "pass", detail: `installed (${editors})` }
44
- : { status: "available", detail: `not installed (${editors})` };
52
+ ? { status: "pass", detail: `installed in ${editors}` }
53
+ : { status: "available", detail: `not installed in ${editors}` };
45
54
  };
46
55
  /**
47
56
  * The `mcp` row: one sub-item per file, each saying whether the entry is
@@ -49,19 +58,16 @@ const lspHealth = (d) => {
49
58
  * one for presence, one for command resolution — and the second judged every
50
59
  * server in the file, so a foreign server's dead command failed this CLI's row.
51
60
  */
52
- async function mcpHealth(d, band, cwd, roots) {
61
+ async function mcpHealth(d, scope, cwd, roots) {
53
62
  if (d.groups.length === 0) {
54
- return {
55
- status: "skip",
56
- detail: "no harness config location in this band",
57
- };
63
+ return { status: "skip", detail: MCP_NO_LOCATION[scope] };
58
64
  }
59
65
  const resolves = await commandResolves(MCP_SERVER_NAME, cwd);
60
66
  const items = d.groups.map((group) => {
61
67
  const state = mcpGroupState(d, group.path);
62
68
  const label = shortenPath(group.path, roots);
63
69
  if (state === "absent") {
64
- return { label, status: "available", detail: "not configured" };
70
+ return { label, status: "available", detail: "not registered" };
65
71
  }
66
72
  if (state === "drifted") {
67
73
  return { label, status: "fail", detail: "entry differs from current" };
@@ -70,8 +76,8 @@ async function mcpHealth(d, band, cwd, roots) {
70
76
  label,
71
77
  status: resolves ? "pass" : "fail",
72
78
  detail: resolves
73
- ? `entry current, \`${MCP_SERVER_NAME}\` resolves`
74
- : `entry current, \`${MCP_SERVER_NAME}\` is not on PATH`,
79
+ ? `registered, and \`${MCP_SERVER_NAME}\` is on PATH`
80
+ : `registered, but \`${MCP_SERVER_NAME}\` is not on PATH`,
75
81
  };
76
82
  });
77
83
  const configured = items.filter((item) => item.status === "pass").length;
@@ -79,33 +85,51 @@ async function mcpHealth(d, band, cwd, roots) {
79
85
  if (failing > 0) {
80
86
  return {
81
87
  status: "fail",
82
- detail: `${failing} of ${items.length} need attention`,
88
+ detail: `${failing} of ${items.length} config files need attention`,
83
89
  items,
84
90
  };
85
91
  }
86
92
  if (configured === 0) {
87
- // Project-band absence is not a failure: the band is opt-in, so a
88
- // repository with no checked-in MCP config is a healthy repository.
89
- return band === "project"
90
- ? { status: "skip", detail: "not configured for this project (opt-in)" }
93
+ // Absence in the LOCAL PROJECT is not a failure: registering there is
94
+ // opt-in, so a repository with no checked-in MCP config is healthy. The
95
+ // line says so rather than leaving "(opt-in)" to be decoded.
96
+ return scope === "project"
97
+ ? {
98
+ status: "skip",
99
+ detail: "not registered for this project — per-project registration is opt-in",
100
+ }
91
101
  : {
92
102
  status: "available",
93
- detail: `${items.length} not configured`,
103
+ detail: `not registered in any of ${items.length} config files`,
94
104
  items,
95
105
  };
96
106
  }
97
107
  return {
98
108
  status: configured === items.length ? "pass" : "available",
99
- detail: `${configured} of ${items.length} current`,
109
+ detail: `registered in ${configured} of ${items.length} config files`,
100
110
  items,
101
111
  };
102
112
  }
103
113
  /** The `skills` row: every expected link present, lstat-verified, none stale. */
104
- function skillsHealth(d, band, roots) {
105
- if (!d.available) {
114
+ function skillsHealth(d, scope, roots) {
115
+ const short = shortenPath(d.sourceRoot, roots);
116
+ // Stale links this scope owns are work even when the root holds no skill, so
117
+ // the row is only a skip once there is nothing left to reconcile either.
118
+ const orphans = d.rootExists ? d.orphans.length : 0;
119
+ if (!d.available && orphans === 0) {
120
+ // The skip carries a remedy — `setup`'s row and this row are the same
121
+ // finding, and the dead-end version of it ("no skills installed", no next
122
+ // step) was reported identically by both surfaces.
106
123
  return {
107
124
  status: "skip",
108
- detail: skillsSkipReason(shortenPath(d.sourceRoot, roots), band),
125
+ detail: skillsSkipReason(short, scope, d.rootExists),
126
+ remedy: skillsSkipRemedy(short, scope),
127
+ };
128
+ }
129
+ if (orphans > 0) {
130
+ return {
131
+ status: "available",
132
+ detail: `${orphans} link${orphans === 1 ? "" : "s"} point at a skill that is gone`,
109
133
  };
110
134
  }
111
135
  const stale = d.actions.filter((a) => a.action === "replaced").length;
@@ -120,6 +144,19 @@ function skillsHealth(d, band, roots) {
120
144
  // and reporting the two the same way made this row say "links current" where
121
145
  // no link exists at all. Setup will never clear such a path — it is not this
122
146
  // command's to delete — so the row carries the only remedy that settles it.
147
+ // A symlink pointing outside every root pragma owns is `skipped` so setup
148
+ // will never delete it — which would make this row claim "links current" over
149
+ // a path where the user's own link shadows a shipped skill. `skipped` also
150
+ // covers an already-correct link (owned) and a real directory (blocked), so
151
+ // the foreign case is what is left.
152
+ const foreign = d.actions.filter((a) => a.action === "skipped" && !a.owned && !a.blocked).length;
153
+ if (foreign > 0) {
154
+ return {
155
+ status: "available",
156
+ detail: `${foreign} of ${d.actions.length} link paths hold a symlink pragma does not own`,
157
+ remedy: "Move or delete the symlink at that path, then link the skills again.",
158
+ };
159
+ }
123
160
  const blocked = d.actions.filter((a) => a.blocked).length;
124
161
  if (blocked > 0) {
125
162
  return {
@@ -143,7 +180,7 @@ function skillsHealth(d, band, roots) {
143
180
  */
144
181
  async function healthOf(row, rt, roots) {
145
182
  // A detection that threw is reported as ITS OWN failing row. Letting the
146
- // rejection escape took the whole banded section of the report with it, so a
183
+ // rejection escape took the whole scoped section of the report with it, so a
147
184
  // single unreadable config file left the user with no rows at all — the one
148
185
  // moment the report is most worth having.
149
186
  const failure = detectionFailure(row);
@@ -163,43 +200,150 @@ async function healthOf(row, rt, roots) {
163
200
  case "lsp":
164
201
  return lspHealth(row.detection);
165
202
  case "mcp":
166
- return mcpHealth(row.detection, row.band, rt.cwd, roots);
203
+ return mcpHealth(row.detection, row.scope, rt.cwd, roots);
167
204
  default:
168
- return skillsHealth(row.detection, row.band, roots);
205
+ return skillsHealth(row.detection, row.scope, roots);
169
206
  }
170
207
  }
208
+ /** The scoped inventory row's name — a LISTING, not a setup target id. */
209
+ export const INVENTORY_CHECK = "harnesses";
210
+ /**
211
+ * The `harnesses` row for one scope: what this machine has, and where pragma
212
+ * stands in each.
213
+ *
214
+ * It is the one scoped row that is NOT a target, and it earns that by never
215
+ * competing with the ones that are. Its status is only `pass` or `skip`, so it
216
+ * adds no failure and no `available` to the tally, and it derives no `fix:` —
217
+ * the `mcp` and `skills` rows beside it already name every command a harness
218
+ * needs, and repeating them here would be the same finding seen twice.
219
+ *
220
+ * The registry is the only thing detection could not supply: `detectHarnesses`
221
+ * filters to hits, so "Windsurf is not on this machine" was unrepresentable
222
+ * until the universe came from somewhere. Everything else — which harnesses
223
+ * share which file, and whether each one's own entry in it is current — is read
224
+ * straight off the `mcp` detection this report already ran. The projection to
225
+ * {@link InventoryGroup} is deliberate: `McpDetection` holds
226
+ * `writeMcpConfigTargets`/`removeMcpConfigFrom` as live functions, and a
227
+ * `mutates: false` command should be unable to reach a writer, not merely
228
+ * trusted not to.
229
+ *
230
+ * This is also where the harness↔`mcpKey` association is REJOINED, because it
231
+ * is the one place that holds both halves: `TargetGroup` records the names
232
+ * sharing a file and, separately, one write per distinct `mcpKey`, and the
233
+ * registry is what says which key a given harness writes. Without that join the
234
+ * only state available per harness is the file's aggregate — and a
235
+ * `.vscode/mcp.json` where VS Code's `servers` entry is current while Cline's
236
+ * `mcpServers` is absent aggregates to `drifted`, which would report BOTH
237
+ * harnesses as drifted when neither one is.
238
+ *
239
+ * @param rt - The per-invocation runtime (read for `--verbose`).
240
+ * @param rows - Every detected row, both scopes, as `scopedChecks` has them.
241
+ * @param roots - The roots every path renders relative to.
242
+ * @returns One inventory {@link CheckResult} per scope, global then project.
243
+ * @note Impure — dynamically imports the harness registry.
244
+ */
245
+ async function inventoryChecks(rt, rows, roots) {
246
+ const { harnesses, isHarnessInBand } = await import("@canonical/harnesses");
247
+ const verbose = rt.globalFlags.verbose;
248
+ const scopes = ["global", "project"];
249
+ // The key each harness NAME writes under — the half of the association
250
+ // `groupConfigTargets` consumed and did not record. Names are the only
251
+ // identity a group carries, and the registry's are unique.
252
+ const keyByName = new Map(harnesses.map((h) => [h.name, h.mcpKey]));
253
+ return scopes.map((scope) => {
254
+ // `isHarnessInBand(harness.scope, scope, scope)` — the scope asked for as
255
+ // its OWN selection ("band" is the harnesses package's name for it) — is
256
+ // exactly "does this harness have a config location here", which is the
257
+ // question `detectMcp` already answers for that scope. Asking it under the
258
+ // `both` selection would instead answer "who writes here when both scopes
259
+ // run", and report every dual-scope harness as having no global location.
260
+ const registry = harnesses.map((harness) => ({
261
+ id: harness.id,
262
+ name: harness.name,
263
+ inScope: isHarnessInBand(harness.scope, scope, scope),
264
+ }));
265
+ const mcpRow = rows.find((row) => row.target.id === "mcp" && row.scope === scope);
266
+ if (mcpRow === undefined || detectionFailure(mcpRow) !== undefined) {
267
+ return {
268
+ name: INVENTORY_CHECK,
269
+ status: "skip",
270
+ detail: "harness detection did not settle — see the `mcp` row",
271
+ scope,
272
+ };
273
+ }
274
+ const detection = mcpRow.detection;
275
+ const groups = detection.groups.map((group) => ({
276
+ path: shortenPath(group.path, roots),
277
+ harnesses: group.harnessNames.map((name) => {
278
+ // The harness's OWN write in this file. A name the registry does not
279
+ // know (or a key no write carries — impossible while the group was
280
+ // built from the same registry) falls back to the file's aggregate:
281
+ // strictly no worse than the state before this join existed.
282
+ const write = group.writes.find((w) => w.mcpKey === keyByName.get(name));
283
+ return {
284
+ name,
285
+ state: write === undefined
286
+ ? mcpGroupState(detection, group.path)
287
+ : mcpWriteState(detection, write),
288
+ };
289
+ }),
290
+ }));
291
+ const health = inventoryHealth(harnessInventory(registry, groups, scope), verbose);
292
+ return {
293
+ name: INVENTORY_CHECK,
294
+ status: health.status,
295
+ detail: health.detail,
296
+ scope,
297
+ ...(health.items.length === 0 ? {} : { items: health.items }),
298
+ };
299
+ });
300
+ }
171
301
  /**
172
- * The command that repairs a row — derived from its id and band, never
302
+ * The command that repairs a row — derived from its id and scope, never
173
303
  * authored. This is the bijection made mechanical: a row exists because a
174
304
  * target exists, and its fix is that target's own invocation.
175
305
  */
176
- export const fixCommandFor = (id, band, bin) => `${bin} setup ${id}${band === "project" ? " --local" : ""}`;
306
+ export const fixCommandFor = (id, scope, bin) => `${bin} setup ${id}${scope === "project" ? " --local" : ""}`;
177
307
  /**
178
- * Run every banded check: the target table, both bands, in table order.
308
+ * Run every scoped check: the target table, both scopes, in table order, then
309
+ * each scope's harness inventory.
310
+ *
311
+ * The inventory rows come LAST within the array, and the renderer partitions by
312
+ * scope while preserving order, so each scope's section ends with the listing of
313
+ * what that scope actually holds — the targets first, then the machine they were
314
+ * measured against. They ride on the SAME detection pass: no target is probed a
315
+ * second time to produce them.
179
316
  *
180
317
  * @param rt - The per-invocation runtime.
181
318
  * @param bin - The binary name the `fix:` lines are derived from.
182
- * @returns One {@link CheckResult} per (target, band) the machine can hold.
319
+ * @returns One {@link CheckResult} per (target, scope), plus one per scope.
183
320
  * @note Impure — every target's detection reads the real filesystem.
184
321
  */
185
- export async function bandedChecks(rt, bin) {
322
+ export async function scopedChecks(rt, bin) {
186
323
  const roots = await resolveRoots(rt);
187
324
  const detected = await detectTargets(rt, [...TARGET_IDS], "both");
188
- return Promise.all(detected.map(async (row) => {
189
- const health = await healthOf(row, rt, roots);
190
- const needsFix = health.status === "fail" || health.status === "available";
191
- return {
192
- name: row.target.id,
193
- status: health.status,
194
- detail: health.detail,
195
- band: row.band,
196
- ...(health.items === undefined ? {} : { items: health.items }),
197
- ...(needsFix
198
- ? {
199
- remedy: health.remedy ?? fixCommandFor(row.target.id, row.band, bin),
200
- }
201
- : {}),
202
- };
203
- }));
325
+ const [targetRows, inventoryRows] = await Promise.all([
326
+ Promise.all(detected.map(async (row) => {
327
+ const health = await healthOf(row, rt, roots);
328
+ const needsFix = health.status === "fail" || health.status === "available";
329
+ // A `skip` gets no DERIVED fix — re-running the target's own setup command
330
+ // would reproduce the skip. But a skip that AUTHORED a remedy has found a
331
+ // real next step on this machine (fill the scope's skill root, say), and
332
+ // dropping it is what made the skip a dead end on both surfaces.
333
+ const remedy = needsFix
334
+ ? (health.remedy ?? fixCommandFor(row.target.id, row.scope, bin))
335
+ : health.remedy;
336
+ return {
337
+ name: row.target.id,
338
+ status: health.status,
339
+ detail: health.detail,
340
+ scope: row.scope,
341
+ ...(health.items === undefined ? {} : { items: health.items }),
342
+ ...(remedy === undefined ? {} : { remedy }),
343
+ };
344
+ })),
345
+ inventoryChecks(rt, detected, roots),
346
+ ]);
347
+ return [...targetRows, ...inventoryRows];
204
348
  }
205
349
  //# sourceMappingURL=targetHealth.js.map