@canonical/pragma-cli 0.35.0 → 0.36.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 (230) 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 +16 -15
  13. package/dist/pragma.conf.js +236 -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/colophon/colophon.render.js +1 -1
  18. package/dist/src/capabilities/colophon/colophon.render.js.map +1 -1
  19. package/dist/src/capabilities/create/mount.js +4 -3
  20. package/dist/src/capabilities/create/mount.js.map +1 -1
  21. package/dist/src/capabilities/doctor/checks/checkPackageRefs.js +60 -4
  22. package/dist/src/capabilities/doctor/checks/checkPackageRefs.js.map +1 -1
  23. package/dist/src/capabilities/doctor/checks/checkShellCompletions.js +8 -8
  24. package/dist/src/capabilities/doctor/checks/checkShellCompletions.js.map +1 -1
  25. package/dist/src/capabilities/doctor/checks/harnessInventory.js +195 -0
  26. package/dist/src/capabilities/doctor/checks/harnessInventory.js.map +1 -0
  27. package/dist/src/capabilities/doctor/checks/index.js +6 -6
  28. package/dist/src/capabilities/doctor/checks/targetHealth.js +194 -50
  29. package/dist/src/capabilities/doctor/checks/targetHealth.js.map +1 -1
  30. package/dist/src/capabilities/doctor/doctor.render.js +43 -64
  31. package/dist/src/capabilities/doctor/doctor.render.js.map +1 -1
  32. package/dist/src/capabilities/doctor/doctor.verb.js +6 -2
  33. package/dist/src/capabilities/doctor/doctor.verb.js.map +1 -1
  34. package/dist/src/capabilities/doctor/runChecks.js +14 -11
  35. package/dist/src/capabilities/doctor/runChecks.js.map +1 -1
  36. package/dist/src/capabilities/graph/query.render.js +2 -2
  37. package/dist/src/capabilities/graph/query.render.js.map +1 -1
  38. package/dist/src/capabilities/info/collectInfo.js +8 -2
  39. package/dist/src/capabilities/info/collectInfo.js.map +1 -1
  40. package/dist/src/capabilities/info/info.render.js +6 -2
  41. package/dist/src/capabilities/info/info.render.js.map +1 -1
  42. package/dist/src/capabilities/prompt/prompt.render.js +2 -2
  43. package/dist/src/capabilities/prompt/prompt.render.js.map +1 -1
  44. package/dist/src/capabilities/setup/buildPlan.js +43 -39
  45. package/dist/src/capabilities/setup/buildPlan.js.map +1 -1
  46. package/dist/src/capabilities/setup/mode.js +2 -2
  47. package/dist/src/capabilities/setup/operations/index.js +4 -4
  48. package/dist/src/capabilities/setup/operations/index.js.map +1 -1
  49. package/dist/src/capabilities/setup/operations/setupGenerator.js +92 -45
  50. package/dist/src/capabilities/setup/operations/setupGenerator.js.map +1 -1
  51. package/dist/src/capabilities/setup/operations/setupLsp.js +88 -36
  52. package/dist/src/capabilities/setup/operations/setupLsp.js.map +1 -1
  53. package/dist/src/capabilities/setup/operations/setupMcp.js +97 -51
  54. package/dist/src/capabilities/setup/operations/setupMcp.js.map +1 -1
  55. package/dist/src/capabilities/setup/operations/setupSkills.js +325 -47
  56. package/dist/src/capabilities/setup/operations/setupSkills.js.map +1 -1
  57. package/dist/src/capabilities/setup/plan.js.map +1 -1
  58. package/dist/src/capabilities/setup/plan.render.js +144 -66
  59. package/dist/src/capabilities/setup/plan.render.js.map +1 -1
  60. package/dist/src/capabilities/setup/setup.render.js +1 -1
  61. package/dist/src/capabilities/setup/setup.verb.js +86 -33
  62. package/dist/src/capabilities/setup/setup.verb.js.map +1 -1
  63. package/dist/src/capabilities/setup/targets.js +110 -45
  64. package/dist/src/capabilities/setup/targets.js.map +1 -1
  65. package/dist/src/capabilities/shared/index.js +6 -5
  66. package/dist/src/capabilities/shared/index.js.map +1 -1
  67. package/dist/src/capabilities/shared/packageManager.js +300 -54
  68. package/dist/src/capabilities/shared/packageManager.js.map +1 -1
  69. package/dist/src/capabilities/skill/discover.js +97 -11
  70. package/dist/src/capabilities/skill/discover.js.map +1 -1
  71. package/dist/src/capabilities/skill/render.js +2 -2
  72. package/dist/src/capabilities/skill/render.js.map +1 -1
  73. package/dist/src/capabilities/sources/installSkills.js +145 -3
  74. package/dist/src/capabilities/sources/installSkills.js.map +1 -1
  75. package/dist/src/capabilities/sources/runUpdate.js +68 -2
  76. package/dist/src/capabilities/sources/runUpdate.js.map +1 -1
  77. package/dist/src/capabilities/sources/update.verb.js +3 -3
  78. package/dist/src/capabilities/sources/update.verb.js.map +1 -1
  79. package/dist/src/capabilities/upgrade/runUpgrade.js +43 -7
  80. package/dist/src/capabilities/upgrade/runUpgrade.js.map +1 -1
  81. package/dist/src/capabilities/upgrade/upgrade.render.js +10 -2
  82. package/dist/src/capabilities/upgrade/upgrade.render.js.map +1 -1
  83. package/dist/src/constants.js +6 -1
  84. package/dist/src/constants.js.map +1 -1
  85. package/dist/src/kernel/index.js +11 -2
  86. package/dist/src/kernel/index.js.map +1 -1
  87. package/dist/src/kernel/interactivity.js +68 -0
  88. package/dist/src/kernel/interactivity.js.map +1 -0
  89. package/dist/src/kernel/packs/renderPack.js +3 -3
  90. package/dist/src/kernel/packs/renderPack.js.map +1 -1
  91. package/dist/src/kernel/packs/resolveEntity.js +63 -12
  92. package/dist/src/kernel/packs/resolveEntity.js.map +1 -1
  93. package/dist/src/kernel/packs/runBodies.js +57 -4
  94. package/dist/src/kernel/packs/runBodies.js.map +1 -1
  95. package/dist/src/kernel/packs/schema.js +54 -2
  96. package/dist/src/kernel/packs/schema.js.map +1 -1
  97. package/dist/src/kernel/packs/sparql/applyFilters.js +104 -5
  98. package/dist/src/kernel/packs/sparql/applyFilters.js.map +1 -1
  99. package/dist/src/kernel/packs/sparql/buildLookupQuery.js +306 -43
  100. package/dist/src/kernel/packs/sparql/buildLookupQuery.js.map +1 -1
  101. package/dist/src/kernel/packs/types.js.map +1 -1
  102. package/dist/src/kernel/project/cli/buildProgram.js +1 -1
  103. package/dist/src/kernel/project/cli/buildProgram.js.map +1 -1
  104. package/dist/src/kernel/project/cli/dispatch.js +23 -23
  105. package/dist/src/kernel/project/cli/dispatch.js.map +1 -1
  106. package/dist/src/kernel/project/cli/globalFlags.js +4 -1
  107. package/dist/src/kernel/project/cli/globalFlags.js.map +1 -1
  108. package/dist/src/kernel/project/cli/helpFormat.js +1 -2
  109. package/dist/src/kernel/project/cli/helpFormat.js.map +1 -1
  110. package/dist/src/kernel/project/cli/rootHelp.js +34 -4
  111. package/dist/src/kernel/project/cli/rootHelp.js.map +1 -1
  112. package/dist/src/kernel/project/mcp/registerVerb.js +16 -1
  113. package/dist/src/kernel/project/mcp/registerVerb.js.map +1 -1
  114. package/dist/src/kernel/render/renderers.js +1 -1
  115. package/dist/src/kernel/render/style.js +29 -9
  116. package/dist/src/kernel/render/style.js.map +1 -1
  117. package/dist/src/kernel/render/vocabulary.js +174 -0
  118. package/dist/src/kernel/render/vocabulary.js.map +1 -0
  119. package/dist/src/kernel/spec/emitReference.js +1 -1
  120. package/dist/src/kernel/spec/emitReference.js.map +1 -1
  121. package/package.json +16 -15
  122. package/pragma.conf.ts +241 -34
  123. package/src/bin.ts +8 -1
  124. package/src/capabilities/block.parity.test.ts +25 -11
  125. package/src/capabilities/block.shipped.exec.test.ts +282 -0
  126. package/src/capabilities/block.tierRank.exec.test.ts +300 -0
  127. package/src/capabilities/colophon/colophon.render.ts +1 -2
  128. package/src/capabilities/colophon/colophon.test.ts +2 -2
  129. package/src/capabilities/create/createGrammar.test.ts +1 -1
  130. package/src/capabilities/create/mount.ts +3 -3
  131. package/src/capabilities/distribution.parity.test.ts +4 -0
  132. package/src/capabilities/doctor/checks/checkPackageRefs.ts +69 -4
  133. package/src/capabilities/doctor/checks/checkShellCompletions.test.ts +6 -4
  134. package/src/capabilities/doctor/checks/checkShellCompletions.ts +8 -8
  135. package/src/capabilities/doctor/checks/harnessInventory.test.ts +219 -0
  136. package/src/capabilities/doctor/checks/harnessInventory.ts +298 -0
  137. package/src/capabilities/doctor/checks/index.ts +6 -6
  138. package/src/capabilities/doctor/checks/targetHealth.ts +229 -59
  139. package/src/capabilities/doctor/doctor.render.test.ts +108 -32
  140. package/src/capabilities/doctor/doctor.render.ts +56 -80
  141. package/src/capabilities/doctor/doctor.test.ts +247 -35
  142. package/src/capabilities/doctor/doctor.verb.ts +7 -2
  143. package/src/capabilities/doctor/runChecks.ts +14 -11
  144. package/src/capabilities/doctor/types.ts +15 -24
  145. package/src/capabilities/emptyRecovery.test.ts +10 -2
  146. package/src/capabilities/graph/query.render.ts +2 -2
  147. package/src/capabilities/info/collectInfo.ts +8 -1
  148. package/src/capabilities/info/info.render.ts +6 -2
  149. package/src/capabilities/info/info.test.ts +37 -1
  150. package/src/capabilities/info/types.ts +10 -2
  151. package/src/capabilities/prompt/prompt.render.ts +2 -2
  152. package/src/capabilities/renderVocabularySync.test.ts +82 -0
  153. package/src/capabilities/scopeTypeSync.test.ts +29 -15
  154. package/src/capabilities/setup/buildPlan.ts +45 -41
  155. package/src/capabilities/setup/index.ts +1 -1
  156. package/src/capabilities/setup/mode.ts +2 -2
  157. package/src/capabilities/setup/operations/index.ts +6 -1
  158. package/src/capabilities/setup/operations/setupGenerator.ts +144 -70
  159. package/src/capabilities/setup/operations/setupLsp.ts +129 -37
  160. package/src/capabilities/setup/operations/setupMcp.ts +132 -61
  161. package/src/capabilities/setup/operations/setupSkills.ts +392 -58
  162. package/src/capabilities/setup/plan.render.ts +171 -76
  163. package/src/capabilities/setup/plan.ts +17 -12
  164. package/src/capabilities/setup/setup.mcp.correctness.test.ts +9 -9
  165. package/src/capabilities/setup/setup.render.test.ts +50 -36
  166. package/src/capabilities/setup/setup.render.ts +1 -1
  167. package/src/capabilities/setup/setup.test.ts +881 -50
  168. package/src/capabilities/setup/setup.verb.ts +91 -33
  169. package/src/capabilities/setup/targets.ts +120 -52
  170. package/src/capabilities/setup/types.ts +10 -18
  171. package/src/capabilities/shared/index.ts +17 -6
  172. package/src/capabilities/shared/packageManager.test.ts +421 -6
  173. package/src/capabilities/shared/packageManager.ts +394 -59
  174. package/src/capabilities/skill/bundledSkills.test.ts +347 -0
  175. package/src/capabilities/skill/discover.ts +100 -11
  176. package/src/capabilities/skill/render.ts +2 -2
  177. package/src/capabilities/skill/skill.test.ts +17 -4
  178. package/src/capabilities/sources/installSkills.ts +181 -3
  179. package/src/capabilities/sources/runUpdate.ts +78 -2
  180. package/src/capabilities/sources/sources.test.ts +228 -0
  181. package/src/capabilities/sources/update.verb.ts +3 -3
  182. package/src/capabilities/surface.test.ts +2 -2
  183. package/src/capabilities/upgrade/runUpgrade.ts +45 -6
  184. package/src/capabilities/upgrade/types.ts +10 -2
  185. package/src/capabilities/upgrade/upgrade.render.ts +9 -2
  186. package/src/capabilities/upgrade/upgrade.test.ts +72 -0
  187. package/src/constants.ts +6 -1
  188. package/src/identity.test.ts +18 -5
  189. package/src/kernel/completion/entitySource.test.ts +15 -2
  190. package/src/kernel/completion/safety.test.ts +16 -9
  191. package/src/kernel/config/defaults.test.ts +2 -1
  192. package/src/kernel/index.ts +11 -2
  193. package/src/kernel/interactivity.test.ts +76 -0
  194. package/src/kernel/interactivity.ts +69 -0
  195. package/src/kernel/packs/compile.test.ts +92 -0
  196. package/src/kernel/packs/renderPack.ts +3 -3
  197. package/src/kernel/packs/resolveEntity.test.ts +75 -14
  198. package/src/kernel/packs/resolveEntity.ts +88 -24
  199. package/src/kernel/packs/runBodies.ts +84 -19
  200. package/src/kernel/packs/schema.ts +60 -4
  201. package/src/kernel/packs/sparql/applyFilters.ts +125 -4
  202. package/src/kernel/packs/sparql/buildLookupQuery.test.ts +290 -0
  203. package/src/kernel/packs/sparql/buildLookupQuery.ts +323 -45
  204. package/src/kernel/packs/toolDescription.test.ts +35 -1
  205. package/src/kernel/packs/types.ts +125 -2
  206. package/src/kernel/project/cli/buildProgram.test.ts +73 -2
  207. package/src/kernel/project/cli/buildProgram.ts +8 -1
  208. package/src/kernel/project/cli/dispatch.test.ts +5 -4
  209. package/src/kernel/project/cli/dispatch.ts +25 -24
  210. package/src/kernel/project/cli/globalFlags.ts +4 -1
  211. package/src/kernel/project/cli/helpFormat.ts +1 -2
  212. package/src/kernel/project/cli/rootHelp.ts +36 -3
  213. package/src/kernel/project/mcp/registerVerb.ts +20 -1
  214. package/src/kernel/render/renderers.ts +1 -1
  215. package/src/kernel/render/style.ts +32 -9
  216. package/src/kernel/render/vocabulary.ts +276 -0
  217. package/src/kernel/spec/emitReference.ts +1 -1
  218. package/src/kernel/spec/types.ts +35 -6
  219. package/src/testing/PARITY_GAPS.ts +9 -3
  220. package/src/testing/behavioral/docExamples.test.ts +4 -2
  221. package/src/testing/behavioral/errorMatrix.mcp.test.ts +110 -7
  222. package/src/testing/behavioral/journeys.firstInstall.test.ts +7 -2
  223. package/src/testing/behavioral/journeys.packageStories.test.ts +6 -3
  224. package/src/testing/eval/__snapshots__/eval.test.ts.snap +26 -1
  225. package/src/testing/eval/cases/readNouns.ts +245 -9
  226. package/src/testing/fixtures/blockGraph.ts +28 -13
  227. package/src/testing/fixtures/graph/canonical.ts +95 -12
  228. package/dist/src/capabilities/shared/bands.js +0 -16
  229. package/dist/src/capabilities/shared/bands.js.map +0 -1
  230. package/src/capabilities/shared/bands.ts +0 -16
@@ -13,85 +13,107 @@
13
13
  * a piped run renders byte-for-byte plain.
14
14
  */
15
15
 
16
- import chalk from "chalk";
17
16
  import { BIN_NAME } from "../../constants.js";
18
17
  import { defaultStyle, type RenderStyle } from "../../kernel/render/style.js";
19
- import { BAND_LABELS } from "../shared/index.js";
20
18
  import {
19
+ NOT_RUN_GLYPH,
20
+ OUTCOME_GLYPHS,
21
+ outcomeRemedyWord,
22
+ SCOPE_LABELS,
23
+ SCOPE_PHRASES,
24
+ } from "../../kernel/render/vocabulary.js";
25
+ import {
26
+ type PlanAction,
21
27
  type PlanChildRow,
22
28
  type PlanRow,
23
29
  planTally,
24
30
  type SetupPlan,
25
31
  shortenPath,
26
32
  } from "./plan.js";
27
- import type { ScopeBand } from "./types.js";
28
-
29
- /** The glyph for a row's outcome. */
30
- const GLYPHS = {
31
- done: "✓",
32
- noop: "✓",
33
- removed: "✓",
34
- kept: "○",
35
- skipped: "○",
36
- failed: "✗",
37
- } as const;
33
+ import type { Scope } from "./types.js";
34
+
35
+ /** The header's scope phrase: `global`, `local project`, or both of them. */
36
+ const scopePhrase = (scope: SetupPlan["scope"]): string => SCOPE_PHRASES[scope];
38
37
 
39
38
  /**
40
- * The marker for a row that carries NO outcome — one the user deselected, so it
41
- * was neither done nor skipped-for-cause. It must not be a ✓: a green check
42
- * against work that never ran is the same lie the old recap told when it
43
- * reported "Setup complete" over a target it had dropped.
39
+ * The header line. It names the scope and BOTH roots exactly once, which is what
40
+ * frees every row below to render its paths root-relative instead of repeating
41
+ * a 120-character prefix per line. The roots are LABELLED (`home:`, `project:`)
42
+ * because the `~` and `.` markers the rows use are only legible once something
43
+ * has said what they stand for.
44
44
  */
45
- const NOT_RUN_GLYPH = "·";
46
-
47
- /** The TTY styler plus `red` — the one tint {@link RenderStyle} does not carry. */
48
- interface PlanStyle extends RenderStyle {
49
- red(text: string): string;
45
+ function header(plan: SetupPlan, lead: string): string {
46
+ const project = shortenPath(plan.roots.project, {
47
+ global: plan.roots.global,
48
+ project: "",
49
+ });
50
+ return `${lead} — ${scopePhrase(plan.scope)} (home: ~ · project: ${project})`;
50
51
  }
51
52
 
52
53
  /**
53
- * Widen a {@link RenderStyle} with a `red` gated on the SAME colour decision, so
54
- * a failure tint appears only on a colour-capable TTY. Mirrors `doctor`'s own
55
- * styler, which is deliberate: a failed setup row and a failed doctor row are
56
- * the same finding seen twice, and they are tinted alike.
54
+ * The plain verb each action reads as. The middle column is a column of VERBS:
55
+ * what this row will do. It used to render the raw {@link PlanAction} token, so
56
+ * it read as a column of status codes — and `none` was the worst of them,
57
+ * standing equally for "already correct" and "nothing found here". The two are
58
+ * now told apart by the pair: the word says nothing will happen, the detail
59
+ * beside it says what was found.
57
60
  *
58
- * @param style - The shared styler.
59
- * @returns The styler plus `red`.
61
+ * `none` and `skip` are worded direction-neutrally on purpose. The SAME renderer
62
+ * draws the `--undo` plan, where a target-specific negation ("nothing to create"
63
+ * on the `config` row) would be plainly false — there the row is not creating
64
+ * anything, it is declining to delete.
65
+ *
66
+ * The token itself is untouched: `action` is the machine field `--format json`
67
+ * publishes, and this is the display layer over it.
60
68
  */
61
- const withRed = (style: RenderStyle): PlanStyle => ({
62
- ...style,
63
- red: style.enabled ? (text) => chalk.red(text) : (text) => text,
64
- });
69
+ const ACTION_WORDS: Record<PlanAction, string> = {
70
+ install: "install",
71
+ update: "update",
72
+ link: "link",
73
+ remove: "remove",
74
+ none: "no change",
75
+ skip: "nothing to do",
76
+ };
65
77
 
66
- /** The header's band phrase: `global band`, `project band`, or `both bands`. */
67
- const scopePhrase = (scope: SetupPlan["scope"]): string =>
68
- scope === "both" ? "both bands" : `${scope} band`;
78
+ /** The distinct actions a row's children carry (empty when it has none). */
79
+ const childActions = (row: PlanRow): Set<PlanChildRow["action"]> =>
80
+ new Set((row.children ?? []).map((child) => child.action));
69
81
 
70
82
  /**
71
- * The header line. It names the band and BOTH roots exactly once, which is what
72
- * frees every row below to render its paths root-relative instead of repeating
73
- * a 120-character prefix per line.
83
+ * The middle column.
84
+ *
85
+ * A row with children takes its word from THEM when they agree, because the
86
+ * row-level token can disagree with every one of its own files: registering the
87
+ * MCP server in three files that have never held it plans as `update` (the
88
+ * table's word for "this row has work that is not a fresh install"), and
89
+ * printing `update` over three `(add)` children described the opposite of what
90
+ * the run would do. Before this the cell held the row's DETAIL instead — `3
91
+ * files`, which is a count, not an action, and left the plan with no column
92
+ * that said what would happen.
74
93
  */
75
- function header(plan: SetupPlan, lead: string): string {
76
- const project = shortenPath(plan.roots.project, {
77
- global: plan.roots.global,
78
- project: "",
79
- });
80
- return `${lead} — ${scopePhrase(plan.scope)} (~ · project: ${project})`;
94
+ function actionCell(row: PlanRow): string {
95
+ const kinds = childActions(row);
96
+ if (row.action === "update" && kinds.size === 1 && kinds.has("add")) {
97
+ return ACTION_WORDS.install;
98
+ }
99
+ return ACTION_WORDS[row.action];
81
100
  }
82
101
 
83
- /** The middle column: the per-file count when a row has children, else its action. */
84
- const actionCell = (row: PlanRow): string =>
85
- row.children && row.children.length > 0 ? row.detail : row.action;
86
-
87
- /** One child rendered inline: `~/.claude.json (add)`. */
88
- const childCell = (child: PlanChildRow): string =>
89
- `${child.label} (${child.action})`;
102
+ /**
103
+ * One child rendered inline. The per-child action is printed only when the
104
+ * children DISAGREE — when they all do the same thing the row's own verb has
105
+ * just said it, and `lsp no change codium — VSCodium (unchanged)` said
106
+ * "nothing happens here" twice in two columns.
107
+ */
108
+ const childCell = (row: PlanRow, child: PlanChildRow): string =>
109
+ childActions(row).size === 1
110
+ ? child.label
111
+ : `${child.label} (${child.action})`;
90
112
 
91
113
  /** The right column: the children joined, or the row's own detail. */
92
114
  const detailCell = (row: PlanRow): string =>
93
115
  row.children && row.children.length > 0
94
- ? row.children.map(childCell).join(" · ")
116
+ ? row.children.map((child) => childCell(row, child)).join(" · ")
95
117
  : row.action === "skip"
96
118
  ? (row.reason ?? row.detail)
97
119
  : row.detail;
@@ -100,13 +122,13 @@ const detailCell = (row: PlanRow): string =>
100
122
  const widthOf = (rows: readonly PlanRow[], pick: (row: PlanRow) => string) =>
101
123
  Math.max(0, ...rows.map((row) => pick(row).length));
102
124
 
103
- /** Group rows by band, in the plan's own band order. */
104
- function byBand(plan: SetupPlan): [ScopeBand, PlanRow[]][] {
105
- const bands: ScopeBand[] = ["global", "project"];
106
- return bands
107
- .map((band): [ScopeBand, PlanRow[]] => [
108
- band,
109
- plan.rows.filter((row) => row.band === band),
125
+ /** Group rows by scope, in the plan's own scope order. */
126
+ function byScope(plan: SetupPlan): [Scope, PlanRow[]][] {
127
+ const scopes: Scope[] = ["global", "project"];
128
+ return scopes
129
+ .map((scope): [Scope, PlanRow[]] => [
130
+ scope,
131
+ plan.rows.filter((row) => row.scope === scope),
110
132
  ])
111
133
  .filter(([, rows]) => rows.length > 0);
112
134
  }
@@ -128,9 +150,9 @@ export function renderPlanTable(
128
150
  const idWidth = widthOf(plan.rows, (row) => row.target);
129
151
  const actionWidth = widthOf(plan.rows, actionCell);
130
152
  const lines = [style.bold(header(plan, options.lead)), ""];
131
- const groups = byBand(plan);
132
- for (const [band, rows] of groups) {
133
- if (plan.scope === "both") lines.push(style.bold(BAND_LABELS[band]));
153
+ const groups = byScope(plan);
154
+ for (const [scope, rows] of groups) {
155
+ if (plan.scope === "both") lines.push(style.bold(SCOPE_LABELS[scope]));
134
156
  for (const row of rows) {
135
157
  lines.push(
136
158
  ` ${row.target.padEnd(idWidth)} ${actionCell(row).padEnd(actionWidth)} ${style.dim(detailCell(row))}`,
@@ -141,6 +163,65 @@ export function renderPlanTable(
141
163
  return lines.join("\n");
142
164
  }
143
165
 
166
+ /**
167
+ * What was DETECTED, rendered before anything is asked.
168
+ *
169
+ * The gap this closes is a rendering one, not an ordering one: detection is
170
+ * complete before the prompt array is even constructed (`buildSetupRun` does
171
+ * resolveRoots → detectTargets → buildPlan, and only then builds `prompts`), and
172
+ * the Ink view mounts no React until the FIRST prompt effect. So the facts were
173
+ * sitting in `run.plan` the whole time with nothing rendering them — the wizard
174
+ * leaked detection only through prompt-label suffixes, and `renderPlanTable` is
175
+ * reached only by `--dry-run` or the non-interactive preview. A wizard run
176
+ * never rendered it at all, and the confirm gate shows an EFFECTS summary, not
177
+ * a detection one.
178
+ *
179
+ * It is a plain-string formatter on purpose. It rides `rt.report` — the non-Ink
180
+ * stderr seam, `--quiet`-aware and a no-op over MCP — because pragma contains no
181
+ * React and no Ink, and three PROTECTED tests exist to keep it that way.
182
+ *
183
+ * The default lists DETECTED rows only; `--verbose` lists everything. "Detected"
184
+ * is `action !== "skip"`, which is not a new partition: it is exactly the
185
+ * predicate the row multiselect already uses to build its choices, so the
186
+ * summary and the question that follows it describe the same set.
187
+ *
188
+ * @param plan - The plan as detected (no outcomes yet).
189
+ * @param options - `verbose` widens detected-only to the whole table.
190
+ * @param style - Injected for tests; defaults to the shared TTY seam.
191
+ * @returns The rendered block, or `undefined` when there is nothing to say.
192
+ */
193
+ export function renderDetectionSummary(
194
+ plan: SetupPlan,
195
+ options: { verbose?: boolean } = {},
196
+ style: RenderStyle = defaultStyle(),
197
+ ): string | undefined {
198
+ const shown = options.verbose
199
+ ? plan.rows
200
+ : plan.rows.filter((row) => row.action !== "skip");
201
+ if (shown.length === 0) return undefined;
202
+ const hidden = plan.rows.length - shown.length;
203
+ const idWidth = widthOf(shown, (row) => row.target);
204
+ const actionWidth = widthOf(shown, actionCell);
205
+ const lines = [style.bold(header(plan, "Detected")), ""];
206
+ for (const [scope, rows] of byScope({ ...plan, rows: shown })) {
207
+ if (plan.scope === "both") lines.push(style.bold(SCOPE_LABELS[scope]));
208
+ for (const row of rows) {
209
+ lines.push(
210
+ ` ${row.target.padEnd(idWidth)} ${actionCell(row).padEnd(actionWidth)} ${style.dim(detailCell(row))}`,
211
+ );
212
+ }
213
+ }
214
+ if (hidden > 0) {
215
+ lines.push(
216
+ "",
217
+ style.dim(
218
+ `${hidden} target${hidden === 1 ? "" : "s"} not detected here — --verbose lists them`,
219
+ ),
220
+ );
221
+ }
222
+ return lines.join("\n");
223
+ }
224
+
144
225
  /**
145
226
  * One progress line, emitted as a row's outcome lands. Same columns as the
146
227
  * recap, so a reader watching the run and a reader reading the recap afterwards
@@ -156,27 +237,30 @@ export function renderProgressLine(
156
237
  idWidth: number,
157
238
  style: RenderStyle = defaultStyle(),
158
239
  ): string {
159
- const painted = withRed(style);
160
240
  const outcome = row.outcome;
161
241
 
162
242
  // No outcome at all: the row was offered and left unselected. It is neither a
163
243
  // success nor a skip-for-cause, and painting it green would claim work that
164
244
  // never happened, so it gets a neutral marker and says plainly what it is.
165
245
  if (outcome === undefined) {
166
- return `${painted.dim(NOT_RUN_GLYPH)} ${row.target.padEnd(idWidth)} ${painted.dim(`${row.detail} — not selected`)}`;
246
+ return `${style.dim(NOT_RUN_GLYPH)} ${row.target.padEnd(idWidth)} ${style.dim(`${row.detail} — not selected`)}`;
167
247
  }
168
248
 
169
- const glyph = GLYPHS[outcome.status];
249
+ const glyph = OUTCOME_GLYPHS[outcome.status];
170
250
  const tinted =
171
251
  outcome.status === "failed"
172
- ? painted.red(glyph)
252
+ ? style.red(glyph)
173
253
  : outcome.status === "skipped" || outcome.status === "kept"
174
- ? painted.yellow(glyph)
175
- : painted.green(glyph);
254
+ ? style.yellow(glyph)
255
+ : style.green(glyph);
176
256
  const note = outcome.note;
177
257
  const body =
178
258
  outcome.status === "skipped"
179
- ? `skipped — ${row.reason ?? row.detail}`
259
+ ? // A colon, not a dash: the reasons carry their own em-dash clause
260
+ // ("no skills installed yet — pack skills arrive with …"), and two
261
+ // dashes in one line read as one run-on sentence rather than a label
262
+ // in front of a reason.
263
+ `skipped: ${row.reason ?? row.detail}`
180
264
  : note
181
265
  ? `${row.detail} — ${note}`
182
266
  : row.detail;
@@ -207,15 +291,19 @@ export function renderRecap(
207
291
  `${lead}: ${configured} of ${accountable} targets configured — ${scopePhrase(plan.scope)}`,
208
292
  ),
209
293
  ];
210
- for (const [band, rows] of byBand(plan)) {
211
- if (plan.scope === "both") lines.push(style.bold(` ${BAND_LABELS[band]}`));
294
+ for (const [scope, rows] of byScope(plan)) {
295
+ if (plan.scope === "both")
296
+ lines.push(style.bold(` ${SCOPE_LABELS[scope]}`));
212
297
  for (const row of rows) {
213
298
  lines.push(` ${renderProgressLine(row, idWidth, style)}`);
214
299
  const remedy = row.outcome?.remedy;
215
300
  if (remedy) lines.push(` ${style.dim(remedy)}`);
216
301
  }
217
302
  }
218
- lines.push("", style.dim(`verify anytime: ${BIN_NAME} doctor`));
303
+ lines.push(
304
+ "",
305
+ style.dim(`Check this again any time with \`${BIN_NAME} doctor\`.`),
306
+ );
219
307
  return lines.join("\n");
220
308
  }
221
309
 
@@ -227,15 +315,22 @@ export function renderPlanLlm(plan: SetupPlan, lead = "Setup"): string {
227
315
  // Before a run there is no outcome to report, so the row shows its ACTION
228
316
  // behind a neutral marker. Reusing the `noop` glyph here painted every row
229
317
  // of an unapplied plan — skips included — with a green check.
230
- const glyph = status === undefined ? NOT_RUN_GLYPH : GLYPHS[status];
318
+ const glyph = status === undefined ? NOT_RUN_GLYPH : OUTCOME_GLYPHS[status];
231
319
  const state = status ?? row.action;
232
320
  lines.push(
233
- `- ${glyph} **${row.target}** (${row.band}): ${state} — ${detailCell(row)}`,
321
+ `- ${glyph} **${row.target}** (${row.scope}): ${state} — ${detailCell(row)}`,
234
322
  );
235
323
  for (const child of row.children ?? []) {
236
- lines.push(` - ${childCell(child)}`);
324
+ lines.push(` - ${childCell(row, child)}`);
325
+ }
326
+ // `fix:` for a row that failed, `next:` for one that skipped — a skip is
327
+ // not a fault, and labelling its instruction as a repair is what teaches a
328
+ // reader to treat skips as failures.
329
+ if (row.outcome?.remedy) {
330
+ lines.push(
331
+ ` - _${outcomeRemedyWord(row.outcome.status)}:_ ${row.outcome.remedy}`,
332
+ );
237
333
  }
238
- if (row.outcome?.remedy) lines.push(` - _fix:_ ${row.outcome.remedy}`);
239
334
  }
240
335
  return lines.join("\n");
241
336
  }
@@ -18,7 +18,8 @@
18
18
  */
19
19
 
20
20
  import { isAbsolute, relative, sep } from "node:path";
21
- import type { ScopeBand, ScopeSelection } from "./types.js";
21
+ import type { OutcomeStatus } from "../../kernel/render/vocabulary.js";
22
+ import type { Scope, ScopeSelection } from "./types.js";
22
23
 
23
24
  /** The five setup targets, in table (and therefore display) order. */
24
25
  export const TARGET_IDS = [
@@ -45,14 +46,11 @@ export type PlanAction =
45
46
  | "none"
46
47
  | "skip";
47
48
 
48
- /** How one row ended. `skipped` is NOT a failure — see {@link planExitFailed}. */
49
- export type OutcomeStatus =
50
- | "done"
51
- | "noop"
52
- | "skipped"
53
- | "failed"
54
- | "removed"
55
- | "kept";
49
+ // How one row ended. Defined with the rendering vocabulary (one module owns
50
+ // the states and their glyphs); re-exported here because the plan is where
51
+ // every other consumer meets it. `skipped` is NOT a failure — see
52
+ // {@link planExitFailed}.
53
+ export type { OutcomeStatus } from "../../kernel/render/vocabulary.js";
56
54
 
57
55
  /** Per-file / per-link detail under a row (MCP files; skill link dirs). */
58
56
  export interface PlanChildRow {
@@ -62,7 +60,14 @@ export interface PlanChildRow {
62
60
  * from {@link label}, which is display text and may be re-worded freely.
63
61
  */
64
62
  readonly key: string;
65
- /** Display text, e.g. `~/.claude.json — Claude Code`. */
63
+ /**
64
+ * Display text as the producers actually emit it: the MCP row's children
65
+ * carry the file's shortened path alone (`~/.claude.json`), the LSP row's
66
+ * carry `cli — Editor Name` (`codium — VSCodium`). This doc once promised
67
+ * a path-plus-harness pairing (`~/.claude.json — Claude Code`) that no
68
+ * producer ever emitted; the harness names sharing an MCP file are the
69
+ * doctor inventory row's to report, not this label's.
70
+ */
66
71
  readonly label: string;
67
72
  readonly action: "add" | "update" | "unchanged" | "skip";
68
73
  /** REQUIRED when the action is `skip` — the named reason. */
@@ -82,10 +87,10 @@ export interface PlanOutcome {
82
87
  readonly remedy?: string;
83
88
  }
84
89
 
85
- /** One row of the plan: one target in one band. */
90
+ /** One row of the plan: one target in one scope. */
86
91
  export interface PlanRow {
87
92
  readonly target: TargetId;
88
- readonly band: ScopeBand;
93
+ readonly scope: Scope;
89
94
  readonly action: PlanAction;
90
95
  /** Right-hand column: what and where, rendered root-relative. */
91
96
  readonly detail: string;
@@ -74,7 +74,7 @@ afterEach(() => {
74
74
  roots.length = 0;
75
75
  });
76
76
 
77
- /** A project with Cursor detected — one project-band file, `.cursor/mcp.json`. */
77
+ /** A project with Cursor detected — one project-scope file, `.cursor/mcp.json`. */
78
78
  const cursorProject = (): { cwd: string; config: string } => {
79
79
  const cwd = tmp("pragma-mcpc-proj-");
80
80
  mkdirSync(join(cwd, ".cursor"), { recursive: true });
@@ -122,7 +122,7 @@ describe("installing twice never produces a second entry", () => {
122
122
  const { cwd, config } = cursorProject();
123
123
  await install(cwd);
124
124
  const detection = await detectMcp(bootRuntime(FLAGS, cwd), "project");
125
- expect(mcpGroupState(detection, config)).toBe("configured");
125
+ expect(mcpGroupState(detection, config)).toBe("registered");
126
126
  });
127
127
  });
128
128
 
@@ -278,7 +278,7 @@ describe("every drift shape converges in one write, then stays converged", () =>
278
278
  const repaired = readFileSync(config, "utf-8");
279
279
 
280
280
  const after = await detectMcp(bootRuntime(FLAGS, cwd), "project");
281
- expect(mcpGroupState(after, config)).toBe("configured");
281
+ expect(mcpGroupState(after, config)).toBe("registered");
282
282
 
283
283
  // A second run after the repair changes nothing.
284
284
  await install(cwd);
@@ -341,7 +341,7 @@ describe("every drift shape converges in one write, then stays converged", () =>
341
341
  expect(entry).toEqual({ command: "pragma", args: ["mcp", "serve"] });
342
342
  expect(entry).not.toHaveProperty("cwd");
343
343
  const after = await detectMcp(bootRuntime(FLAGS, cwd), "global");
344
- expect(mcpGroupState(after, config)).toBe("configured");
344
+ expect(mcpGroupState(after, config)).toBe("registered");
345
345
  });
346
346
 
347
347
  it("leaves extra keys on OUR entry alone — they are not ours to drop", async () => {
@@ -355,7 +355,7 @@ describe("every drift shape converges in one write, then stays converged", () =>
355
355
  writeFileSync(config, `${JSON.stringify(parsed, null, 2)}\n`);
356
356
 
357
357
  const detection = await detectMcp(bootRuntime(FLAGS, cwd), "project");
358
- expect(mcpGroupState(detection, config)).toBe("configured");
358
+ expect(mcpGroupState(detection, config)).toBe("registered");
359
359
 
360
360
  await install(cwd);
361
361
  expect(
@@ -363,8 +363,8 @@ describe("every drift shape converges in one write, then stays converged", () =>
363
363
  ).toBe(5000);
364
364
  });
365
365
 
366
- it("keeps the two bands of one harness independent", async () => {
367
- // Cursor has both a project file and a home file. Writing one band must
366
+ it("keeps the two scopes of one harness independent", async () => {
367
+ // Cursor has both a project file and a home file. Writing one scope must
368
368
  // not touch the other, and each converges on its own terms — the project
369
369
  // entry records a cwd, the global entry omits it.
370
370
  const cwd = tmp("pragma-mcpc-proj-");
@@ -380,13 +380,13 @@ describe("every drift shape converges in one write, then stays converged", () =>
380
380
  const projectBytes = readFileSync(projectConfig, "utf-8");
381
381
  expect(existsSync(globalConfig)).toBe(true);
382
382
 
383
- // Each band carries its own shape.
383
+ // Each scope carries its own shape.
384
384
  expect(JSON.parse(projectBytes).mcpServers.pragma.cwd).toBe(cwd);
385
385
  expect(
386
386
  JSON.parse(readFileSync(globalConfig, "utf-8")).mcpServers.pragma,
387
387
  ).not.toHaveProperty("cwd");
388
388
 
389
- // Undoing one band leaves the other intact.
389
+ // Undoing one scope leaves the other intact.
390
390
  await undo(cwd, GLOBAL);
391
391
  expect(readFileSync(projectConfig, "utf-8")).toBe(projectBytes);
392
392
  });