@canonical/pragma-cli 0.32.0 → 0.34.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 +72 -0
  2. package/dist/pragma +0 -0
  3. package/package.json +19 -15
  4. package/pragma.conf.ts +664 -0
  5. package/src/bin.ts +15 -6
  6. package/src/capabilities/block.parity.test.ts +250 -0
  7. package/src/capabilities/capabilities/capabilities.render.ts +3 -2
  8. package/src/capabilities/capabilities/capabilities.test.ts +10 -1
  9. package/src/capabilities/capabilities/capabilities.verb.ts +32 -14
  10. package/src/capabilities/capabilities/catalog.ts +20 -8
  11. package/src/capabilities/capabilities/hints.ts +21 -36
  12. package/src/capabilities/capabilities/types.ts +2 -0
  13. package/src/capabilities/colophon/collectColophon.ts +34 -19
  14. package/src/capabilities/colophon/colophon.render.test.ts +31 -4
  15. package/src/capabilities/colophon/colophon.render.ts +13 -4
  16. package/src/capabilities/colophon/colophon.test.ts +3 -3
  17. package/src/capabilities/colophon/colophon.verb.ts +11 -4
  18. package/src/capabilities/colophon/markdownTerminal.ts +33 -14
  19. package/src/capabilities/colophon/types.ts +16 -7
  20. package/src/capabilities/config/collectConfigShow.test.ts +57 -0
  21. package/src/capabilities/config/collectConfigShow.ts +41 -4
  22. package/src/capabilities/config/fields.ts +10 -108
  23. package/src/capabilities/config/set.test.ts +6 -6
  24. package/src/capabilities/config/set.verb.ts +12 -8
  25. package/src/capabilities/config/show.render.test.ts +137 -0
  26. package/src/capabilities/config/show.render.ts +57 -16
  27. package/src/capabilities/config/show.verb.ts +9 -8
  28. package/src/capabilities/config/types.ts +8 -0
  29. package/src/capabilities/create/byteEquality.subprocess.test.ts +1 -1
  30. package/src/capabilities/create/byteEquality.test.ts +82 -101
  31. package/src/capabilities/create/compiledCreate.subprocess.test.ts +53 -16
  32. package/src/capabilities/create/constants.ts +75 -0
  33. package/src/capabilities/create/create.mcp.test.ts +2 -2
  34. package/src/capabilities/create/create.test.ts +192 -16
  35. package/src/capabilities/create/create.verb.ts +208 -79
  36. package/src/capabilities/create/index.ts +2 -10
  37. package/src/capabilities/create/pickGenerator.ts +34 -28
  38. package/src/capabilities/create/templates.embedded.generated.ts +4 -29
  39. package/src/capabilities/create/types.ts +10 -6
  40. package/src/capabilities/{standard/parity.test.ts → distribution.parity.test.ts} +24 -14
  41. package/src/capabilities/distribution.test.ts +120 -0
  42. package/src/capabilities/distribution.ts +59 -0
  43. package/src/capabilities/doctor/checks/checkConfigFile.ts +6 -5
  44. package/src/capabilities/doctor/checks/checkKeStore.ts +2 -2
  45. package/src/capabilities/doctor/checks/checkMcpCommands.ts +6 -0
  46. package/src/capabilities/doctor/checks/checkMcpConfigured.ts +15 -8
  47. package/src/capabilities/doctor/checks/checkPackageRefs.ts +79 -45
  48. package/src/capabilities/doctor/checks/checkPragmaVersion.ts +3 -2
  49. package/src/capabilities/doctor/checks/checkShellCompletions.test.ts +61 -11
  50. package/src/capabilities/doctor/checks/checkShellCompletions.ts +63 -23
  51. package/src/capabilities/doctor/checks/checkSkillsSymlinked.ts +3 -2
  52. package/src/capabilities/doctor/checks/deriveBand.test.ts +42 -0
  53. package/src/capabilities/doctor/checks/deriveBand.ts +28 -0
  54. package/src/capabilities/doctor/doctor.render.test.ts +170 -0
  55. package/src/capabilities/doctor/doctor.render.ts +117 -34
  56. package/src/capabilities/doctor/doctor.test.ts +68 -9
  57. package/src/capabilities/doctor/doctor.verb.ts +6 -2
  58. package/src/capabilities/doctor/runChecks.ts +33 -9
  59. package/src/capabilities/doctor/types.ts +16 -1
  60. package/src/capabilities/emptyRecovery.test.ts +32 -15
  61. package/src/capabilities/graph/inspect.verb.ts +3 -2
  62. package/src/capabilities/graph/query.verb.ts +3 -2
  63. package/src/capabilities/graph/runQuery.ts +8 -5
  64. package/src/capabilities/index.ts +22 -18
  65. package/src/capabilities/info/collectInfo.ts +10 -6
  66. package/src/capabilities/info/info.render.ts +3 -2
  67. package/src/capabilities/info/info.verb.ts +3 -2
  68. package/src/capabilities/lazy.test.ts +93 -2
  69. package/src/capabilities/ontology/index.ts +10 -3
  70. package/src/capabilities/ontology/ontology.test.ts +157 -5
  71. package/src/capabilities/ontology/verbs.ts +203 -104
  72. package/src/capabilities/prompt/prompt.render.test.ts +94 -0
  73. package/src/capabilities/prompt/prompt.render.ts +23 -6
  74. package/src/capabilities/prompt/prompt.test.ts +68 -1
  75. package/src/capabilities/prompt/verbs.ts +24 -11
  76. package/src/capabilities/reference.test.ts +32 -0
  77. package/src/capabilities/resources/provider.ts +58 -25
  78. package/src/capabilities/resources/resources.test.ts +176 -21
  79. package/src/capabilities/scopeTypeSync.test.ts +37 -0
  80. package/src/capabilities/setup/index.ts +7 -1
  81. package/src/capabilities/setup/operations/setupCompletions.ts +49 -16
  82. package/src/capabilities/setup/operations/setupGenerator.ts +166 -63
  83. package/src/capabilities/setup/operations/setupLsp.ts +56 -12
  84. package/src/capabilities/setup/operations/setupMcp.ts +216 -39
  85. package/src/capabilities/setup/operations/setupSkills.ts +10 -4
  86. package/src/capabilities/setup/setup.render.test.ts +109 -0
  87. package/src/capabilities/setup/setup.render.ts +61 -14
  88. package/src/capabilities/setup/setup.test.ts +352 -1
  89. package/src/capabilities/setup/setup.verb.ts +68 -9
  90. package/src/capabilities/setup/shell.ts +29 -3
  91. package/src/capabilities/setup/types.ts +59 -2
  92. package/src/capabilities/shared/bands.ts +16 -0
  93. package/src/capabilities/skill/discover.ts +11 -5
  94. package/src/capabilities/skill/render.ts +4 -5
  95. package/src/capabilities/skill/verbs.ts +7 -7
  96. package/src/capabilities/sources/collectStatus.ts +69 -49
  97. package/src/capabilities/sources/index.ts +1 -1
  98. package/src/capabilities/sources/installSkills.ts +2 -2
  99. package/src/capabilities/sources/runUpdate.ts +146 -98
  100. package/src/capabilities/sources/sources.test.ts +264 -84
  101. package/src/capabilities/sources/status.render.test.ts +94 -0
  102. package/src/capabilities/sources/status.render.ts +55 -33
  103. package/src/capabilities/sources/status.verb.ts +15 -8
  104. package/src/capabilities/sources/types.ts +18 -25
  105. package/src/capabilities/sources/update.render.ts +9 -2
  106. package/src/capabilities/sources/update.verb.ts +12 -23
  107. package/src/capabilities/surface.test.ts +65 -17
  108. package/src/capabilities/tier.parity.test.ts +144 -0
  109. package/src/capabilities/upgrade/upgrade.verb.ts +7 -3
  110. package/src/constants.test.ts +9 -7
  111. package/src/constants.ts +49 -23
  112. package/src/identity.test.ts +333 -0
  113. package/src/index.ts +2 -2
  114. package/src/kernel/completion/__snapshots__/emitScripts.test.ts.snap +18 -16
  115. package/src/kernel/completion/complete.test.ts +3 -5
  116. package/src/kernel/completion/emitScripts.test.ts +1 -1
  117. package/src/kernel/completion/emitScripts.ts +4 -2
  118. package/src/kernel/completion/entitySource.test.ts +185 -41
  119. package/src/kernel/completion/entitySource.ts +131 -113
  120. package/src/kernel/completion/model.test.ts +3 -3
  121. package/src/kernel/completion/model.ts +11 -1
  122. package/src/kernel/completion/parse.test.ts +2 -4
  123. package/src/kernel/completion/safety.test.ts +82 -36
  124. package/src/kernel/completion/shellDrive.test.ts +592 -0
  125. package/src/kernel/completion/templates/fish.ts +28 -6
  126. package/src/kernel/completion/types.ts +3 -3
  127. package/src/kernel/config/__snapshots__/firstRun.test.ts.snap +1 -1
  128. package/src/kernel/config/defaults.test.ts +85 -0
  129. package/src/kernel/config/defaults.ts +12 -23
  130. package/src/kernel/config/evaluateProjectConfig.ts +2 -3
  131. package/src/kernel/config/findProjectConfig.ts +4 -3
  132. package/src/kernel/config/firstRun.ts +12 -5
  133. package/src/kernel/config/globalConfig.ts +2 -2
  134. package/src/kernel/config/index.ts +4 -5
  135. package/src/kernel/config/paths.ts +17 -17
  136. package/src/kernel/config/readConfig.test.ts +242 -6
  137. package/src/kernel/config/readConfig.ts +8 -8
  138. package/src/kernel/config/schema.test.ts +50 -0
  139. package/src/kernel/config/schema.ts +97 -10
  140. package/src/kernel/config/types.ts +73 -19
  141. package/src/kernel/copy.test.ts +453 -0
  142. package/src/kernel/error/PragmaError.test.ts +5 -5
  143. package/src/kernel/error/constants.ts +3 -2
  144. package/src/kernel/error/index.ts +1 -1
  145. package/src/kernel/error/recovery.test.ts +22 -45
  146. package/src/kernel/error/recovery.ts +22 -27
  147. package/src/kernel/error/renderError.ts +1 -1
  148. package/src/kernel/error/types.ts +5 -4
  149. package/src/kernel/packs/collect.test.ts +195 -11
  150. package/src/kernel/packs/collect.ts +213 -58
  151. package/src/kernel/packs/compile.test.ts +163 -7
  152. package/src/kernel/packs/compile.ts +15 -12
  153. package/src/kernel/packs/disclosure.test.ts +5 -2
  154. package/src/kernel/packs/graphql/fetchGraphqlLookup.ts +11 -10
  155. package/src/kernel/packs/graphqlSource.test.ts +6 -3
  156. package/src/kernel/packs/iri.ts +5 -7
  157. package/src/kernel/packs/renderPack.ts +5 -3
  158. package/src/kernel/packs/resolveEntity.ts +21 -17
  159. package/src/kernel/packs/runBodies.ts +15 -14
  160. package/src/kernel/packs/schema.ts +96 -10
  161. package/src/kernel/packs/sparql/runSelect.test.ts +104 -10
  162. package/src/kernel/packs/sparql/runSelect.ts +73 -34
  163. package/src/kernel/packs/toolDescription.test.ts +10 -2
  164. package/src/kernel/packs/types.ts +39 -4
  165. package/src/kernel/packs/uniqueness.ts +4 -4
  166. package/src/kernel/project/cli/buildProgram.test.ts +60 -11
  167. package/src/kernel/project/cli/buildProgram.ts +11 -0
  168. package/src/kernel/project/cli/dispatch.test.ts +67 -0
  169. package/src/kernel/project/cli/dispatch.ts +34 -12
  170. package/src/kernel/project/cli/globalFlags.test.ts +12 -4
  171. package/src/kernel/project/cli/globalFlags.ts +14 -14
  172. package/src/kernel/project/cli/helpFormat.ts +1 -1
  173. package/src/kernel/project/cli/rootHelp.ts +109 -107
  174. package/src/kernel/project/mcp/buildServer.ts +11 -6
  175. package/src/kernel/project/mcp/instructions.ts +12 -7
  176. package/src/kernel/project/mcp/mcpError.ts +52 -0
  177. package/src/kernel/project/mcp/parity.test.ts +39 -1
  178. package/src/kernel/project/mcp/prompts/prompts.test.ts +128 -5
  179. package/src/kernel/project/mcp/prompts/provider.ts +51 -7
  180. package/src/kernel/project/mcp/prompts/source.ts +68 -67
  181. package/src/kernel/project/mcp/registerVerb.ts +25 -6
  182. package/src/kernel/project/mcp/serve.ts +11 -3
  183. package/src/kernel/project/mcp/toolDescriptions.test.ts +6 -2
  184. package/src/kernel/render/disclosure.ts +4 -2
  185. package/src/kernel/render/formatters.ts +5 -4
  186. package/src/kernel/render/prefixes.test.ts +36 -0
  187. package/src/kernel/render/prefixes.ts +40 -13
  188. package/src/kernel/render/renderers.test.ts +35 -1
  189. package/src/kernel/render/renderers.ts +25 -7
  190. package/src/kernel/render/style.test.ts +31 -0
  191. package/src/kernel/render/style.ts +74 -0
  192. package/src/kernel/runtime/graphpack/build.ts +57 -3
  193. package/src/kernel/runtime/graphpack/buildIndex.test.ts +149 -0
  194. package/src/kernel/runtime/graphpack/buildIndex.ts +173 -50
  195. package/src/kernel/runtime/graphpack/embedded/pack.generated.ts +5 -8
  196. package/src/kernel/runtime/graphpack/embedded/pack.index.generated.ts +3 -4
  197. package/src/kernel/runtime/graphpack/embedded/pack.stories.generated.ts +3 -0
  198. package/src/kernel/runtime/graphpack/embedded.ts +37 -11
  199. package/src/kernel/runtime/graphpack/graphpack.test.ts +274 -6
  200. package/src/kernel/runtime/graphpack/hash.ts +3 -2
  201. package/src/kernel/runtime/graphpack/index.ts +1 -1
  202. package/src/kernel/runtime/graphpack/manifest.ts +114 -12
  203. package/src/kernel/runtime/graphpack/read.ts +32 -22
  204. package/src/kernel/runtime/graphpack/schemas.ts +74 -0
  205. package/src/kernel/runtime/graphpack/stories.ts +84 -0
  206. package/src/kernel/runtime/graphpack/types.ts +72 -45
  207. package/src/kernel/runtime/loadSession.ts +4 -12
  208. package/src/kernel/runtime/paths.ts +64 -15
  209. package/src/kernel/runtime/probe.test.ts +8 -4
  210. package/src/kernel/runtime/readEntity.ts +6 -5
  211. package/src/kernel/runtime/refs/gitOps.ts +1 -19
  212. package/src/kernel/runtime/refs/parseRef.ts +7 -7
  213. package/src/kernel/runtime/refs/resolve.ts +141 -65
  214. package/src/kernel/runtime/resolveSources.test.ts +63 -24
  215. package/src/kernel/runtime/resolveSources.ts +36 -25
  216. package/src/kernel/runtime/storeReadiness.ts +62 -0
  217. package/src/kernel/runtime/storeless.test.ts +1 -1
  218. package/src/kernel/runtime/types.ts +1 -1
  219. package/src/kernel/runtime/wasmEmbed.test.ts +25 -20
  220. package/src/kernel/spec/emitReference.test.ts +196 -0
  221. package/src/kernel/spec/emitReference.ts +624 -0
  222. package/src/kernel/spec/emitSurface.ts +13 -7
  223. package/src/kernel/spec/index.ts +2 -0
  224. package/src/kernel/spec/surfaceConformance.test.ts +2 -2
  225. package/src/kernel/spec/surfaceConformance.ts +8 -0
  226. package/src/kernel/spec/types.ts +29 -9
  227. package/src/kernel/spec/validate.ts +2 -1
  228. package/src/kernel/vocabulary.test.ts +164 -0
  229. package/src/kernel/vocabulary.ts +146 -0
  230. package/src/testing/PARITY_GAPS.ts +9 -3
  231. package/src/testing/behavioral/README.md +3 -3
  232. package/src/testing/behavioral/autoLlm.e2e.test.ts +12 -2
  233. package/src/testing/behavioral/completion.test.ts +21 -8
  234. package/src/testing/behavioral/disclosure.test.ts +12 -0
  235. package/src/testing/behavioral/docExamples.test.ts +283 -0
  236. package/src/testing/behavioral/errorMatrix.mcp.test.ts +4 -3
  237. package/src/testing/behavioral/errorMatrix.storeless.test.ts +3 -2
  238. package/src/testing/behavioral/exitCodes.e2e.test.ts +6 -5
  239. package/src/testing/behavioral/harness.foundation.test.ts +14 -5
  240. package/src/testing/behavioral/journeys.cli.test.ts +116 -73
  241. package/src/testing/behavioral/journeys.defaultPack.test.ts +477 -0
  242. package/src/testing/behavioral/journeys.firstInstall.test.ts +167 -0
  243. package/src/testing/behavioral/journeys.interactiveTty.e2e.test.ts +66 -0
  244. package/src/testing/behavioral/journeys.livePack.e2e.test.ts +129 -0
  245. package/src/testing/behavioral/journeys.mcpLifecycle.mcp.test.ts +179 -0
  246. package/src/testing/behavioral/journeys.packageStories.test.ts +188 -0
  247. package/src/testing/behavioral/liveReadSurface.ts +4 -3
  248. package/src/testing/behavioral/mcpSurface.test.ts +10 -5
  249. package/src/testing/behavioral/staleCommands.test.ts +86 -0
  250. package/src/testing/eval/__snapshots__/eval.test.ts.snap +9 -9
  251. package/src/testing/eval/cases/assembly.ts +16 -9
  252. package/src/testing/eval/cases/effectful.ts +31 -16
  253. package/src/testing/eval/cases/readNouns.ts +23 -18
  254. package/src/testing/eval/cases/stable.ts +5 -1
  255. package/src/testing/eval/eval.test.ts +7 -1
  256. package/src/testing/fixtures/fixtureCapability.ts +201 -1
  257. package/src/testing/fixtures/graph/canonical.ts +6 -3
  258. package/src/testing/fixtures/graph/defaultPack.ts +179 -0
  259. package/src/testing/fixtures/graph/owlExport.ts +97 -0
  260. package/src/testing/helpers/extractSnippets.ts +29 -0
  261. package/src/testing/helpers/fixtureGraph.ts +33 -16
  262. package/src/testing/helpers/golden.ts +1 -1
  263. package/src/testing/helpers/packRuntime.ts +2 -3
  264. package/src/testing/helpers/projectCli.ts +2 -1
  265. package/src/testing/helpers/projectMcp.ts +6 -0
  266. package/src/testing/perf/budgets.test.ts +4 -0
  267. package/src/testing/perf/budgets.ts +22 -7
  268. package/src/testing/perf/globalSetup.ts +121 -6
  269. package/src/types.ts +2 -2
  270. package/src/capabilities/block/blockList.verb.ts +0 -150
  271. package/src/capabilities/block/index.ts +0 -25
  272. package/src/capabilities/block/pack.ts +0 -155
  273. package/src/capabilities/block/parity.test.ts +0 -180
  274. package/src/capabilities/block/tierChain.ts +0 -78
  275. package/src/capabilities/colophon/pragmaColophon.ts +0 -66
  276. package/src/capabilities/config/field.test.ts +0 -188
  277. package/src/capabilities/modifier/index.ts +0 -15
  278. package/src/capabilities/modifier/pack.ts +0 -64
  279. package/src/capabilities/standard/index.ts +0 -18
  280. package/src/capabilities/standard/pack.ts +0 -128
  281. package/src/capabilities/tier/index.ts +0 -19
  282. package/src/capabilities/tier/lookup.render.ts +0 -34
  283. package/src/capabilities/tier/lookup.test.ts +0 -68
  284. package/src/capabilities/tier/lookup.verb.ts +0 -44
  285. package/src/capabilities/tier/pack.ts +0 -28
  286. package/src/capabilities/tier/runLookup.ts +0 -59
  287. package/src/capabilities/token/addConfig.render.ts +0 -27
  288. package/src/capabilities/token/addConfig.test.ts +0 -62
  289. package/src/capabilities/token/addConfig.verb.ts +0 -40
  290. package/src/capabilities/token/index.ts +0 -19
  291. package/src/capabilities/token/pack.ts +0 -58
  292. package/src/capabilities/token/runAddConfig.ts +0 -95
  293. package/src/kernel/completion/bashDrive.test.ts +0 -73
  294. package/src/kernel/runtime/graphpack/embedded/sample.ttl +0 -39
  295. package/src/kernel/runtime/lock.ts +0 -90
@@ -0,0 +1,170 @@
1
+ /**
2
+ * Render goldens for `pragma doctor` — the band-grouped report AND the plain-path
3
+ * color gate (F1).
4
+ *
5
+ * The banded Global/Project grouping (`partitionByBand` → sections) is pinned for
6
+ * both the plain and llm formatters, locking the unified Global/Project
7
+ * vocabulary (the report no longer says MACHINE/PROJECT). Separately, the plain
8
+ * formatter tints ONLY on a color-capable TTY: `supports-color` reports a
9
+ * non-zero `chalk.level` off a TTY under `GITHUB_ACTIONS` / `FORCE_COLOR`, so the
10
+ * `isTTY` gate (via the shared style seam) keeps ANSI out of
11
+ * `doctor --format plain | tee`. `beforeEach` forces color OFF so the banded
12
+ * goldens stay deterministic; the color tests opt a level back in explicitly.
13
+ */
14
+
15
+ import chalk from "chalk";
16
+ import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest";
17
+ import { doctorFormatters } from "./doctor.render.js";
18
+ import type { DoctorData } from "./types.js";
19
+
20
+ /** A fixture spanning all three groups: environment (no band), global, project. */
21
+ const BANDED_DATA: DoctorData = {
22
+ checks: [
23
+ { name: "Node version", status: "pass", detail: "v20 detected" },
24
+ {
25
+ name: "Shell completions",
26
+ status: "pass",
27
+ detail: "zsh up to date and resolving",
28
+ band: "global",
29
+ },
30
+ {
31
+ name: "MCP configured",
32
+ status: "fail",
33
+ detail: "detected Windsurf but pragma not configured",
34
+ remedy: "pragma setup mcp",
35
+ band: "global",
36
+ },
37
+ {
38
+ name: "Skills symlinked",
39
+ status: "skip",
40
+ detail: "no AI harnesses detected",
41
+ band: "project",
42
+ },
43
+ ],
44
+ passed: 2,
45
+ failed: 1,
46
+ skipped: 1,
47
+ };
48
+
49
+ /** A fixture spanning pass/fail/skip, sub-items, and a remedy — every color path. */
50
+ const COLOR_DATA: DoctorData = {
51
+ checks: [
52
+ { name: "Node version", status: "pass", detail: "v24" },
53
+ {
54
+ name: "pack refs",
55
+ status: "fail",
56
+ detail: "packs are configured but the store has not been built",
57
+ items: [
58
+ { label: "core", status: "fail", detail: "not built" },
59
+ { label: "ui", status: "fail", detail: "not built" },
60
+ ],
61
+ remedy: "pragma sources update",
62
+ },
63
+ { name: "Skills symlinked", status: "skip", detail: "no harness" },
64
+ ],
65
+ passed: 1,
66
+ failed: 1,
67
+ skipped: 1,
68
+ };
69
+
70
+ /** Run `body` with stdout's `isTTY` forced to `value`, then restore it. */
71
+ function withStdoutTty(value: boolean | undefined, body: () => void): void {
72
+ const stream = process.stdout as { isTTY?: boolean };
73
+ const saved = stream.isTTY;
74
+ stream.isTTY = value;
75
+ try {
76
+ body();
77
+ } finally {
78
+ stream.isTTY = saved;
79
+ }
80
+ }
81
+
82
+ let prevLevel: number;
83
+ beforeAll(() => {
84
+ prevLevel = chalk.level;
85
+ });
86
+ beforeEach(() => {
87
+ chalk.level = 0; // color-free ⇒ deterministic text (color tests opt back in)
88
+ });
89
+ afterAll(() => {
90
+ chalk.level = prevLevel;
91
+ });
92
+
93
+ describe("doctor render — banded plain report", () => {
94
+ it("groups checks under Global then Project headers (unified labels)", () => {
95
+ const out = doctorFormatters.plain(BANDED_DATA);
96
+ const lines = out.split("\n");
97
+ // The two banded section headers use the unified Global/Project vocabulary.
98
+ expect(lines).toContain("Global");
99
+ expect(lines).toContain("Project");
100
+ // The superseded MACHINE/PROJECT labels are gone.
101
+ expect(out).not.toContain("MACHINE");
102
+ expect(out).not.toMatch(/^PROJECT$/m);
103
+ });
104
+
105
+ it("orders environment → Global → Project, banding each check correctly", () => {
106
+ const out = doctorFormatters.plain(BANDED_DATA);
107
+ // Environment check leads with no header; the two global-band checks sit
108
+ // under Global; the project-band check sits under Project.
109
+ const at = (needle: string): number => out.indexOf(needle);
110
+ expect(at("Node version")).toBeGreaterThanOrEqual(0);
111
+ expect(at("Node version")).toBeLessThan(at("Global"));
112
+ expect(at("Global")).toBeLessThan(at("Shell completions"));
113
+ expect(at("MCP configured")).toBeLessThan(at("Project"));
114
+ expect(at("Project")).toBeLessThan(at("Skills symlinked"));
115
+ // A failing banded check keeps its inline remedy under its band.
116
+ expect(out).toContain("fix: pragma setup mcp");
117
+ // The tally closes the report.
118
+ expect(out).toContain("2 passed");
119
+ });
120
+ });
121
+
122
+ describe("doctor render — banded llm report", () => {
123
+ it("groups checks under ### Global then ### Project headers", () => {
124
+ const out = doctorFormatters.llm(BANDED_DATA);
125
+ expect(out).toContain("### Global");
126
+ expect(out).toContain("### Project");
127
+ expect(out).not.toContain("### Machine");
128
+ const at = (needle: string): number => out.indexOf(needle);
129
+ // Environment leads (no section header), then the two banded sections.
130
+ expect(at("Node version")).toBeLessThan(at("### Global"));
131
+ expect(at("### Global")).toBeLessThan(at("Shell completions"));
132
+ expect(at("Shell completions")).toBeLessThan(at("### Project"));
133
+ expect(at("### Project")).toBeLessThan(at("Skills symlinked"));
134
+ expect(out).toContain("_2 passed, 1 failed, 1 skipped_");
135
+ });
136
+ });
137
+
138
+ describe("doctor render — json", () => {
139
+ it("is the exact DoctorData round-trip", () => {
140
+ expect(JSON.parse(doctorFormatters.json(BANDED_DATA))).toEqual(BANDED_DATA);
141
+ });
142
+ });
143
+
144
+ describe("doctor render — piped output is ANSI-free (F1)", () => {
145
+ it("plain emits ZERO ANSI off a TTY even when chalk reports color (CI/FORCE_COLOR)", () => {
146
+ chalk.level = 3;
147
+ withStdoutTty(undefined, () => {
148
+ const out = doctorFormatters.plain(COLOR_DATA);
149
+ // biome-ignore lint/suspicious/noControlCharactersInRegex: asserting NO ESC byte survives
150
+ expect(out).not.toMatch(/\x1b\[/);
151
+ // The structural, color-free content still renders (glyphs, names, remedy).
152
+ expect(out).toContain("pragma doctor");
153
+ expect(out).toContain("✓ Node version");
154
+ expect(out).toContain("✗ pack refs");
155
+ expect(out).toContain("○ Skills symlinked");
156
+ expect(out).toContain("↳ fix: pragma sources update");
157
+ expect(out).toContain(" 1 passed · 1 failed · 1 skipped");
158
+ });
159
+ });
160
+ });
161
+
162
+ describe("doctor render — color ON (attended TTY)", () => {
163
+ it("plain tints the output on a color-capable TTY", () => {
164
+ chalk.level = 1;
165
+ withStdoutTty(true, () => {
166
+ // biome-ignore lint/suspicious/noControlCharactersInRegex: asserting the literal ESC byte is the point
167
+ expect(doctorFormatters.plain(COLOR_DATA)).toMatch(/\x1b\[/);
168
+ });
169
+ });
170
+ });
@@ -1,35 +1,75 @@
1
1
  /**
2
- * Formatters for `pragma doctor` — plain (chalk), llm (Markdown), json.
2
+ * Formatters for `pragma doctor` — plain (chalk on a TTY), llm (Markdown), json.
3
3
  *
4
4
  * Ported from the old shell's `doctor/formatters/doctor.ts`, retargeted at the
5
- * kernel `Formatters` contract.
5
+ * kernel `Formatters` contract. The plain path is colored ONLY on a color-capable
6
+ * TTY: it consults the shared {@link defaultStyle} seam (stdout `isTTY` AND a
7
+ * non-zero chalk level), so a piped / redirected / CI run — where `supports-color`
8
+ * can report a level with no TTY (`GITHUB_ACTIONS`, `FORCE_COLOR`) — renders the
9
+ * plain form byte-for-byte instead of leaking ANSI into `doctor --format plain`.
10
+ * Glyphs are plain constants tinted at render time; nothing is baked at load.
6
11
  */
7
12
 
8
13
  import chalk from "chalk";
14
+ import { BIN_NAME } from "../../constants.js";
15
+ import { defaultStyle, type RenderStyle } from "../../kernel/render/style.js";
9
16
  import type { Formatters } from "../../kernel/spec/types.js";
17
+ import { BAND_LABELS } from "../shared/bands.js";
10
18
  import type { CheckResult, CheckStatus, DoctorData } from "./types.js";
11
19
 
12
- /** Status icons for pass/fail/skip results (checks and sub-items). */
13
- const STATUS_ICONS: Record<CheckStatus, string> = {
14
- pass: chalk.green("✓"),
15
- fail: chalk.red("✗"),
16
- skip: chalk.yellow("○"),
20
+ /** Uncolored status glyphs — tinted at render time (never baked at module load). */
21
+ const STATUS_GLYPHS: Record<CheckStatus, string> = {
22
+ pass: "✓",
23
+ fail: "✗",
24
+ skip: "○",
17
25
  };
18
26
 
19
- const SUB_BULLET = chalk.dim("·");
20
- const FIX_ARROW = chalk.cyan("↳");
27
+ const SUB_BULLET = "·";
28
+ const FIX_ARROW = "↳";
21
29
  const INDENT = " "; // aligns sub-lines under the check name
22
30
 
31
+ /** The TTY styler plus `red` — the one tint {@link RenderStyle} does not carry. */
32
+ interface DoctorStyle extends RenderStyle {
33
+ red(text: string): string;
34
+ }
35
+
36
+ /**
37
+ * Build the doctor styler for this process's stdout: the shared TTY seam, plus a
38
+ * `red` gated on the SAME decision, so a fail tint appears only on a color TTY.
39
+ *
40
+ * @returns A colorizing styler on a color-capable TTY, else the identity styler.
41
+ * @note Impure — {@link defaultStyle} reads `process.stdout.isTTY` + chalk level.
42
+ */
43
+ function doctorStyle(): DoctorStyle {
44
+ const style = defaultStyle();
45
+ return {
46
+ ...style,
47
+ red: style.enabled ? (text) => chalk.red(text) : (text) => text,
48
+ };
49
+ }
50
+
51
+ /** Tint a status glyph by meaning — green pass, red fail, yellow skip. */
52
+ function paintGlyph(status: CheckStatus, style: DoctorStyle): string {
53
+ const glyph = STATUS_GLYPHS[status];
54
+ if (status === "pass") return style.green(glyph);
55
+ if (status === "fail") return style.red(glyph);
56
+ return style.yellow(glyph);
57
+ }
58
+
23
59
  /**
24
60
  * Render one check as terminal lines: a headline row (icon, aligned name,
25
61
  * detail), an optional indented breakdown of sub-items, and — for failures — an
26
62
  * inline remedial instruction.
27
63
  */
28
- function formatCheckPlain(check: CheckResult, nameWidth: number): string[] {
64
+ function formatCheckPlain(
65
+ check: CheckResult,
66
+ nameWidth: number,
67
+ style: DoctorStyle,
68
+ ): string[] {
29
69
  const name = check.name.padEnd(nameWidth);
30
- const label = check.status === "fail" ? chalk.red(name) : chalk.bold(name);
70
+ const label = check.status === "fail" ? style.red(name) : style.bold(name);
31
71
  const lines = [
32
- ` ${STATUS_ICONS[check.status]} ${label} ${chalk.dim(check.detail)}`,
72
+ ` ${paintGlyph(check.status, style)} ${label} ${style.dim(check.detail)}`,
33
73
  ];
34
74
 
35
75
  if (check.items && check.items.length > 0) {
@@ -38,55 +78,86 @@ function formatCheckPlain(check: CheckResult, nameWidth: number): string[] {
38
78
  for (const item of check.items) {
39
79
  const icon = anyStatus
40
80
  ? item.status
41
- ? `${STATUS_ICONS[item.status]} `
81
+ ? `${paintGlyph(item.status, style)} `
42
82
  : " "
43
83
  : "";
44
84
  const itemLabel = item.detail ? item.label.padEnd(itemWidth) : item.label;
45
- const detail = item.detail ? ` ${chalk.dim(item.detail)}` : "";
46
- lines.push(`${INDENT}${SUB_BULLET} ${icon}${itemLabel}${detail}`);
85
+ const detail = item.detail ? ` ${style.dim(item.detail)}` : "";
86
+ lines.push(
87
+ `${INDENT}${style.dim(SUB_BULLET)} ${icon}${itemLabel}${detail}`,
88
+ );
47
89
  }
48
90
  }
49
91
 
50
92
  if (check.status === "fail" && check.remedy) {
51
- lines.push(`${INDENT}${FIX_ARROW} ${chalk.cyan("fix:")} ${check.remedy}`);
93
+ lines.push(
94
+ `${INDENT}${style.cyan(FIX_ARROW)} ${style.cyan("fix:")} ${check.remedy}`,
95
+ );
52
96
  }
53
97
 
54
98
  return lines;
55
99
  }
56
100
 
57
101
  /** Render the pass/fail/skip tally, coloring non-zero fail/skip. */
58
- function formatSummary(data: DoctorData): string {
59
- const parts = [chalk.green(`${data.passed} passed`)];
102
+ function formatSummary(data: DoctorData, style: DoctorStyle): string {
103
+ const parts = [style.green(`${data.passed} passed`)];
60
104
  parts.push(
61
105
  data.failed > 0
62
- ? chalk.red(`${data.failed} failed`)
63
- : chalk.dim(`${data.failed} failed`),
106
+ ? style.red(`${data.failed} failed`)
107
+ : style.dim(`${data.failed} failed`),
64
108
  );
65
109
  parts.push(
66
110
  data.skipped > 0
67
- ? chalk.yellow(`${data.skipped} skipped`)
68
- : chalk.dim(`${data.skipped} skipped`),
111
+ ? style.yellow(`${data.skipped} skipped`)
112
+ : style.dim(`${data.skipped} skipped`),
69
113
  );
70
- return ` ${parts.join(chalk.dim(" · "))}`;
114
+ return ` ${parts.join(style.dim(" · "))}`;
115
+ }
116
+
117
+ /**
118
+ * Partition checks into ordered bands: environment (no band), then the global
119
+ * and project bands. Declaration order is preserved within each band, so the
120
+ * report stays deterministic.
121
+ */
122
+ function partitionByBand(checks: readonly CheckResult[]): {
123
+ environment: CheckResult[];
124
+ machine: CheckResult[];
125
+ project: CheckResult[];
126
+ } {
127
+ return {
128
+ environment: checks.filter((c) => c.band === undefined),
129
+ machine: checks.filter((c) => c.band === "global"),
130
+ project: checks.filter((c) => c.band === "project"),
131
+ };
71
132
  }
72
133
 
73
134
  export const doctorFormatters: Formatters<DoctorData> = {
74
135
  plain(data) {
136
+ const style = doctorStyle();
75
137
  const nameWidth = Math.max(...data.checks.map((c) => c.name.length), 0);
76
- const lines: string[] = [chalk.bold("pragma doctor"), ""];
77
- for (const check of data.checks) {
78
- lines.push(...formatCheckPlain(check, nameWidth));
79
- }
80
- lines.push("", formatSummary(data));
138
+ const lines: string[] = [style.bold(`${BIN_NAME} doctor`), ""];
139
+ const { environment, machine, project } = partitionByBand(data.checks);
140
+ const section = (heading: string, checks: CheckResult[]): void => {
141
+ if (checks.length === 0) return;
142
+ if (heading) lines.push(style.bold(heading));
143
+ for (const check of checks)
144
+ lines.push(...formatCheckPlain(check, nameWidth, style));
145
+ lines.push("");
146
+ };
147
+ // Environment checks lead (no header); the global then project bands are the
148
+ // two banded sections before the tally.
149
+ section("", environment);
150
+ section(BAND_LABELS.global, machine);
151
+ section(BAND_LABELS.project, project);
152
+ lines.push(formatSummary(data, style));
81
153
  return lines.join("\n");
82
154
  },
83
155
 
84
156
  llm(data) {
85
- const lines: string[] = ["## Doctor", ""];
86
- for (const check of data.checks) {
157
+ const renderCheck = (check: CheckResult): string[] => {
87
158
  const icon =
88
159
  check.status === "pass" ? "✓" : check.status === "fail" ? "✗" : "○";
89
- lines.push(`- ${icon} **${check.name}**: ${check.detail}`);
160
+ const out = [`- ${icon} **${check.name}**: ${check.detail}`];
90
161
  for (const item of check.items ?? []) {
91
162
  const itemIcon =
92
163
  item.status === "pass"
@@ -97,12 +168,24 @@ export const doctorFormatters: Formatters<DoctorData> = {
97
168
  ? "○ "
98
169
  : "";
99
170
  const detail = item.detail ? `: ${item.detail}` : "";
100
- lines.push(` - ${itemIcon}${item.label}${detail}`);
171
+ out.push(` - ${itemIcon}${item.label}${detail}`);
101
172
  }
102
173
  if (check.status === "fail" && check.remedy) {
103
- lines.push(` - _fix:_ \`${check.remedy}\``);
174
+ out.push(` - _fix:_ \`${check.remedy}\``);
104
175
  }
105
- }
176
+ return out;
177
+ };
178
+
179
+ const lines: string[] = ["## Doctor", ""];
180
+ const { environment, machine, project } = partitionByBand(data.checks);
181
+ const section = (heading: string, checks: CheckResult[]): void => {
182
+ if (checks.length === 0) return;
183
+ if (heading) lines.push(`### ${heading}`, "");
184
+ for (const check of checks) lines.push(...renderCheck(check));
185
+ };
186
+ section("", environment);
187
+ section(BAND_LABELS.global, machine);
188
+ section(BAND_LABELS.project, project);
106
189
  lines.push(
107
190
  "",
108
191
  `_${data.passed} passed, ${data.failed} failed, ${data.skipped} skipped_`,
@@ -8,7 +8,7 @@
8
8
  * the MCP read-only envelope.
9
9
  */
10
10
 
11
- import { mkdtempSync, rmSync } from "node:fs";
11
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
12
12
  import { tmpdir } from "node:os";
13
13
  import { join } from "node:path";
14
14
  import { afterEach, beforeEach, describe, expect, it } from "vitest";
@@ -28,6 +28,7 @@ import {
28
28
  } from "../../testing/fixtures/graph/canonical.js";
29
29
  import { bootFixtureRuntime } from "../../testing/helpers/fixtureGraph.js";
30
30
  import { projectMcp } from "../../testing/helpers/projectMcp.js";
31
+ import { checkMcpConfigured } from "./checks/checkMcpConfigured.js";
31
32
  import { doctorModule } from "./index.js";
32
33
  import { runChecks } from "./runChecks.js";
33
34
  import type { DoctorData } from "./types.js";
@@ -50,17 +51,24 @@ const tmp = (prefix: string): string => {
50
51
 
51
52
  let prevHome: string | undefined;
52
53
  let prevXdg: string | undefined;
54
+ let prevPath: string | undefined;
53
55
  beforeEach(() => {
54
56
  prevHome = process.env.HOME;
55
57
  prevXdg = process.env.XDG_CONFIG_HOME;
58
+ prevPath = process.env.PATH;
56
59
  // Empty HOME/XDG so `~`-based harness signals, rc files, and the global config
57
60
  // are all absent — the harness/config/completion checks become deterministic.
58
61
  process.env.HOME = tmp("pragma-doctor-home-");
59
62
  process.env.XDG_CONFIG_HOME = tmp("pragma-doctor-xdg-");
63
+ // Point PATH at an empty dir so no harness `process` signal fires off the
64
+ // ambient PATH (e.g. a `claude`/`codex` binary on the CI/dev host) — keeping
65
+ // "no harnesses detected" deterministic regardless of what is installed.
66
+ process.env.PATH = tmp("pragma-doctor-path-");
60
67
  });
61
68
  afterEach(() => {
62
69
  process.env.HOME = prevHome;
63
70
  process.env.XDG_CONFIG_HOME = prevXdg;
71
+ process.env.PATH = prevPath;
64
72
  for (const dir of roots) rmSync(dir, { recursive: true, force: true });
65
73
  roots.length = 0;
66
74
  });
@@ -71,10 +79,9 @@ const defaultLayers: ConfigLayers = {
71
79
  tier: "default",
72
80
  channel: "default",
73
81
  detail: "default",
74
- packages: "default",
82
+ packs: "default",
75
83
  stories: "default",
76
84
  prefixes: "default",
77
- prompts: "default",
78
85
  },
79
86
  global: { path: "", exists: false },
80
87
  project: { exists: false },
@@ -114,12 +121,12 @@ describe("doctor — shape & spread", () => {
114
121
  }
115
122
  // Deterministic under the isolated env.
116
123
  expect(byName(data, "Node version")?.status).toBe("pass");
117
- // The 3 default-config packages are configured but not locked here → an
118
- // attributable fail listing them, with the sources-update remedy.
119
- const pkgRefs = byName(data, "package refs");
120
- expect(pkgRefs?.status).toBe("fail");
121
- expect(pkgRefs?.remedy).toBe("pragma sources update");
122
- expect(pkgRefs?.items?.length).toBe(3);
124
+ // Nothing built, but the packs are the distribution's own — so the embedded
125
+ // snapshot answers reads and the check passes, naming what it is reading.
126
+ const pkgRefs = byName(data, "pack refs");
127
+ expect(pkgRefs?.status).toBe("pass");
128
+ expect(pkgRefs?.detail).toContain("embedded snapshot @ ");
129
+ expect(pkgRefs?.remedy).toBeUndefined();
123
130
  // No harnesses in an empty HOME/cwd — attributable fail + skips.
124
131
  expect(byName(data, "MCP configured")?.status).toBe("fail");
125
132
  expect(byName(data, "Skills symlinked")?.status).toBe("skip");
@@ -129,6 +136,36 @@ describe("doctor — shape & spread", () => {
129
136
  });
130
137
  });
131
138
 
139
+ describe("doctor — the pack-refs check", () => {
140
+ it("a project with its OWN packs and nothing built is an attributable fail", async () => {
141
+ // The install that must never read healthy: `origins.packs` is "project",
142
+ // so the embedded snapshot is a DIFFERENT graph and every read throws
143
+ // STORE_UNAVAILABLE. Doctor has to say so and name the fix, rather than
144
+ // pass by listing packs it is not actually reading from.
145
+ const cwd = tmp("pragma-unbuilt-");
146
+ writeFileSync(
147
+ join(cwd, "pragma.config.ts"),
148
+ 'export default { packs: [{ name: "unbuilt", source: "file:///pragma-never-built" }] };\n',
149
+ );
150
+ const data = await runChecks(bootRuntime(FLAGS, cwd));
151
+ const pkgRefs = byName(data, "pack refs");
152
+ expect(pkgRefs?.status).toBe("fail");
153
+ expect(pkgRefs?.detail).toContain("the store has not been built");
154
+ expect(pkgRefs?.remedy).toBe("pragma sources update");
155
+ });
156
+
157
+ it("a project pointed at its own built pack passes, naming that pack", async () => {
158
+ const fixture = await bootFixtureRuntime({ ttl: CANONICAL_TTL });
159
+ const data = await runChecks(fixture.runtime);
160
+ const pkgRefs = byName(data, "pack refs");
161
+ expect(pkgRefs?.status).toBe("pass");
162
+ // The fixture pack's own provenance label, not the embedded snapshot's.
163
+ expect(pkgRefs?.detail).toContain("fixture");
164
+ expect(pkgRefs?.detail).not.toContain("embedded snapshot");
165
+ await fixture.dispose();
166
+ });
167
+ });
168
+
132
169
  describe("doctor — the store check", () => {
133
170
  it("a store that fails to boot is an attributable fail, not a crash", async () => {
134
171
  const data = await runChecks(throwingStoreRuntime(tmp("pragma-proj-")));
@@ -177,3 +214,25 @@ describe("doctor — dispatch & MCP", () => {
177
214
  expect((envelope.data as DoctorData).checks).toHaveLength(9);
178
215
  });
179
216
  });
217
+
218
+ describe("doctor — MCP checks band by detected harness scope, not check name", () => {
219
+ it("bands a configured GLOBAL-scope harness (Windsurf) as global", async () => {
220
+ // Windsurf is a global-only harness whose MCP config lives in the home band.
221
+ // The old static map tagged every "MCP configured" result PROJECT; the fix
222
+ // derives the band from the harness's real scope, so this reports `global`.
223
+ const cwd = tmp("pragma-doctor-proj-");
224
+ mkdirSync(join(cwd, ".windsurf"), { recursive: true }); // ⇒ Windsurf detected
225
+ const home = process.env.HOME ?? "";
226
+ const wsDir = join(home, ".codeium", "windsurf");
227
+ mkdirSync(wsDir, { recursive: true });
228
+ writeFileSync(
229
+ join(wsDir, "mcp_config.json"),
230
+ JSON.stringify({ mcpServers: { pragma: { command: "pragma" } } }),
231
+ );
232
+
233
+ const check = await checkMcpConfigured(cwd);
234
+ expect(check.status).toBe("pass");
235
+ expect(check.detail).toContain("Windsurf");
236
+ expect(check.band).toBe("global"); // NOT the old static "project"
237
+ });
238
+ });
@@ -12,6 +12,7 @@
12
12
  * `doctor-exit-zero-with-failures`.
13
13
  */
14
14
 
15
+ import { BIN_NAME } from "../../constants.js";
15
16
  import { asVerb } from "../../kernel/spec/asVerb.js";
16
17
  import type { CapabilityModule, VerbSpec } from "../../kernel/spec/types.js";
17
18
  import { doctorFormatters } from "./doctor.render.js";
@@ -24,8 +25,11 @@ const doctorVerb: VerbSpec<Record<string, unknown>, DoctorData> = {
24
25
  params: [],
25
26
  output: { formatters: doctorFormatters },
26
27
  examples: [
27
- { cmd: "pragma doctor" },
28
- { cmd: "pragma doctor --format json", note: "machine-readable checks" },
28
+ { cmd: `${BIN_NAME} doctor` },
29
+ {
30
+ cmd: `${BIN_NAME} doctor --format json`,
31
+ note: "machine-readable checks",
32
+ },
29
33
  ],
30
34
  capability: {
31
35
  needsStore: false,
@@ -8,6 +8,7 @@
8
8
  * neither the checks nor `@canonical/harnesses` land on the fast path.
9
9
  */
10
10
 
11
+ import { BIN_NAME } from "../../constants.js";
11
12
  import type { PragmaRuntime } from "../../kernel/runtime/types.js";
12
13
  import { checkConfigFile } from "./checks/checkConfigFile.js";
13
14
  import { checkKeStore } from "./checks/checkKeStore.js";
@@ -18,7 +19,31 @@ import { checkPackageRefs } from "./checks/checkPackageRefs.js";
18
19
  import { checkPragmaVersion } from "./checks/checkPragmaVersion.js";
19
20
  import { checkShellCompletions } from "./checks/checkShellCompletions.js";
20
21
  import { checkSkillsSymlinked } from "./checks/checkSkillsSymlinked.js";
21
- import type { CheckResult, DoctorData } from "./types.js";
22
+ import type { CheckResult, DoctorData, ScopeBand } from "./types.js";
23
+
24
+ /**
25
+ * The band a check statically concerns, by name — for checks whose band is
26
+ * fixed by construction: skills always symlink into the per-repo project dir
27
+ * (`skillsPath` is project-rooted), and completions install into the user/home
28
+ * shell dir. The MCP checks are NOT here: they detect harnesses across BOTH
29
+ * bands, so each derives its own band from what it found (see `deriveBand`) and
30
+ * sets it on its own result — {@link attachBand} preserves that.
31
+ */
32
+ const CHECK_BANDS: Record<string, ScopeBand> = {
33
+ "Skills symlinked": "project",
34
+ "Shell completions": "global",
35
+ };
36
+
37
+ /**
38
+ * Attach a band tag to a check result. A check that already set its own band
39
+ * (the harness-derived MCP checks) keeps it; otherwise the static
40
+ * {@link CHECK_BANDS} map fills one in by name, if defined.
41
+ */
42
+ function attachBand(result: CheckResult): CheckResult {
43
+ if (result.band !== undefined) return result;
44
+ const band = CHECK_BANDS[result.name];
45
+ return band ? { ...result, band } : result;
46
+ }
22
47
 
23
48
  /**
24
49
  * Each check paired with the display name used if it rejects. The fallback name
@@ -31,9 +56,9 @@ function buildChecks(
31
56
  ): readonly [string, Promise<CheckResult>][] {
32
57
  return [
33
58
  ["Node version", checkNodeVersion()],
34
- ["pragma version", checkPragmaVersion(rt)],
35
- ["pragma config", checkConfigFile(rt)],
36
- ["package refs", checkPackageRefs(rt)],
59
+ [`${BIN_NAME} version`, checkPragmaVersion(rt)],
60
+ [`${BIN_NAME} config`, checkConfigFile(rt)],
61
+ ["pack refs", checkPackageRefs(rt)],
37
62
  ["ke store", checkKeStore(rt)],
38
63
  ["Shell completions", checkShellCompletions(rt.cwd)],
39
64
  ["MCP configured", checkMcpConfigured(rt.cwd)],
@@ -53,16 +78,15 @@ export async function runChecks(rt: PragmaRuntime): Promise<DoctorData> {
53
78
  const checks: CheckResult[] = await Promise.all(
54
79
  buildChecks(rt).map(async ([name, promise]): Promise<CheckResult> => {
55
80
  try {
56
- return await promise;
81
+ return attachBand(await promise);
57
82
  } catch (error) {
58
83
  const reason = error instanceof Error ? error.message : String(error);
59
- return {
84
+ return attachBand({
60
85
  name,
61
86
  status: "fail",
62
87
  detail: `Check threw an unexpected error: ${reason}`,
63
- remedy:
64
- "Re-run `pragma doctor`; if it persists, report this as a bug.",
65
- };
88
+ remedy: `Re-run \`${BIN_NAME} doctor\`; if it persists, report this as a bug.`,
89
+ });
66
90
  }
67
91
  }),
68
92
  );
@@ -2,12 +2,19 @@
2
2
  * Data shapes for `pragma doctor` (ported verbatim from the old shell).
3
3
  */
4
4
 
5
+ /**
6
+ * One of the two config bands a check can concern. Mirrors the harnesses
7
+ * `ScopeBand` structurally but is redeclared here so this statically-reachable
8
+ * type module never pulls the harnesses runtime into the fast-path module graph.
9
+ */
10
+ export type ScopeBand = "project" | "global";
11
+
5
12
  /** Status of a doctor check or one of its sub-items. */
6
13
  export type CheckStatus = "pass" | "fail" | "skip";
7
14
 
8
15
  /**
9
16
  * A structured sub-item under a check — e.g. one resolved package under
10
- * `package refs`, or one unresolvable server under `MCP commands`. Lets the
17
+ * `pack refs`, or one unresolvable server under `MCP commands`. Lets the
11
18
  * formatter render an indented, aligned breakdown instead of cramming
12
19
  * everything into a single `detail` string.
13
20
  */
@@ -30,6 +37,14 @@ export interface CheckResult {
30
37
  readonly items?: readonly CheckItem[];
31
38
  /** Remedial instruction shown inline under a failing check. */
32
39
  readonly remedy?: string;
40
+ /**
41
+ * Which config band the check concerns, if any: `global` for the user/home
42
+ * level (shell completions), `project` for per-repo config (skills). The MCP
43
+ * checks derive their band from the harnesses they found. The renderer groups
44
+ * banded checks into Global/Project sections; environment checks (Node,
45
+ * versions, store) carry no band.
46
+ */
47
+ readonly band?: ScopeBand;
33
48
  }
34
49
 
35
50
  /** Aggregated results from all doctor checks. */