@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
@@ -9,18 +9,18 @@
9
9
  * only the self-verb is an MCP tool. `destructive: false` keeps MCP from
10
10
  * advertising the tool as destructive.
11
11
  *
12
- * THE BAND DEFAULT IS GLOBAL. `--scope` used to default to `both`, and `both`
13
- * runs each harness's own default band — which is `project` for most of the
12
+ * THE SCOPE DEFAULT IS GLOBAL. `--scope` used to default to `both`, and `both`
13
+ * runs each harness's own default scope — which is `project` for most of the
14
14
  * registry — so a bare `setup` scattered `.mcp.json`, `.gemini/settings.json`
15
15
  * and `opencode.json` into whatever directory you happened to be standing in.
16
- * The user band is what a machine-level installer configures; the project band
16
+ * The user scope is what a machine-level installer configures; the project scope
17
17
  * is a deliberate, checked-in, team-shared choice, and is now chosen with
18
18
  * `--local` rather than assumed.
19
19
  *
20
- * Every sub-verb registers the scope flags UNIFORMLY. That is what lets the band
20
+ * Every sub-verb registers the scope flags UNIFORMLY. That is what lets the scope
21
21
  * answer come from the table instead of per-verb flag wiring — previously
22
22
  * `setup completions --local` died as an unknown option, while `setup mcp
23
- * --local` worked, for no reason a user could see. Asking a target for a band it
23
+ * --local` worked, for no reason a user could see. Asking a target for a scope it
24
24
  * cannot honour is a usage error (exit 2) with the corrected command; asking the
25
25
  * RUN-ALL for such a band merely filters, and the out-of-band target still shows
26
26
  * as a named row.
@@ -43,20 +43,25 @@ import type {
43
43
  } from "../../kernel/spec/index.js";
44
44
  import { resolveSetupMode, setupModeInput } from "./mode.js";
45
45
  import {
46
+ isActionable,
46
47
  planExitFailed,
47
48
  planTally,
48
49
  type SetupPlan,
49
50
  TARGET_IDS,
50
51
  } from "./plan.js";
51
- import { renderProgressLine, renderRecap } from "./plan.render.js";
52
+ import {
53
+ renderDetectionSummary,
54
+ renderProgressLine,
55
+ renderRecap,
56
+ } from "./plan.render.js";
52
57
  import { renderDryRun, setupFormatters } from "./setup.render.js";
53
- import type { ScopeBand, ScopeSelection, SetupMode } from "./types.js";
58
+ import type { Scope, ScopeSelection, SetupMode } from "./types.js";
54
59
 
55
60
  /**
56
61
  * The `--scope` flag + its `--global`/`--local` boolean sugars. Registered by
57
- * EVERY setup verb, including the single-band ones: a flag that exists on some
58
- * sub-verbs and not others is a grammar the user has to memorise, and the band
59
- * a single-band target can honour is a question the target table answers.
62
+ * EVERY setup verb, including the single-scope ones: a flag that exists on some
63
+ * sub-verbs and not others is a grammar the user has to memorise, and the scope
64
+ * a single-scope target can honour is a question the target table answers.
60
65
  *
61
66
  * `--global` is a synonym of the default; it stays for scripts and for symmetry
62
67
  * with `--local`.
@@ -65,19 +70,19 @@ const SCOPE_PARAMS: readonly ParamSpec[] = [
65
70
  {
66
71
  kind: "enum",
67
72
  name: "scope",
68
- doc: "Which config band(s) to configure: global (the default), project, or both.",
73
+ doc: "Where to configure: global (your home directory, the default), project (this repository), or both.",
69
74
  values: ["project", "global", "both"],
70
75
  default: "global",
71
76
  },
72
77
  {
73
78
  kind: "boolean",
74
79
  name: "global",
75
- doc: "Shorthand for --scope global (configure the user/home band).",
80
+ doc: "Shorthand for --scope global — configure your home directory.",
76
81
  },
77
82
  {
78
83
  kind: "boolean",
79
84
  name: "local",
80
- doc: "Shorthand for --scope project (configure the per-project band).",
85
+ doc: "Shorthand for --scope project — configure this project only.",
81
86
  },
82
87
  ];
83
88
 
@@ -96,34 +101,35 @@ function resolveScope(params: Record<string, unknown>): ScopeSelection {
96
101
  }
97
102
 
98
103
  /**
99
- * Reject a sub-verb asked for a band its target cannot hold.
104
+ * Reject a sub-verb asked for a scope its target cannot hold.
100
105
  *
101
106
  * This is a usage error, not a skip: the user typed a contradiction, and
102
- * correcting it is exactly what exit 2 means. The message names the band the
107
+ * correcting it is exactly what exit 2 means. The message names the scope the
103
108
  * target DOES have and prints the command that works, so the correction is one
104
109
  * paste rather than one manual page.
105
110
  *
106
111
  * @param mode - The sub-verb's target, or `all`.
107
112
  * @param scope - The resolved selection.
108
- * @throws PragmaError INVALID_INPUT when the target has no such band.
113
+ * @throws PragmaError INVALID_INPUT when the target has no such scope.
109
114
  */
110
- async function assertBandIsPossible(
115
+ async function assertScopeIsPossible(
111
116
  mode: SetupMode,
112
117
  scope: ScopeSelection,
113
118
  ): Promise<void> {
114
119
  if (mode === "all" || scope === "both") return;
115
120
  const { findTarget } = await import("./targets.js");
116
121
  const target = findTarget(mode);
117
- if (target === undefined || target.bands.includes(scope as ScopeBand)) return;
118
- const only = target.bands[0] as ScopeBand;
119
- const level = only === "global" ? "user-level" : "project-level";
122
+ if (target === undefined || target.scopes.includes(scope as Scope)) return;
123
+ const only = target.scopes[0] as Scope;
124
+ const level = only === "global" ? "global only" : "per-project only";
125
+ const asked = scope === "global" ? "globally" : "per project";
120
126
  const corrected =
121
127
  only === "global"
122
128
  ? `${BIN_NAME} setup ${mode}`
123
129
  : `${BIN_NAME} setup ${mode} --local`;
124
130
  throw new PragmaError({
125
131
  code: "INVALID_INPUT",
126
- message: `${mode} is ${level} — it has no ${scope} band.`,
132
+ message: `${mode} is ${level} — it cannot be configured ${asked}.`,
127
133
  recovery: { message: `Run: ${corrected}` },
128
134
  });
129
135
  }
@@ -169,7 +175,7 @@ const SUB_CAPABILITY = {
169
175
  *
170
176
  * @param mode - Which entry point (`all` or one target).
171
177
  * @param rt - The per-invocation runtime (mutated: `rt.exec` and `rt.planData`).
172
- * @param scope - The resolved band selection.
178
+ * @param scope - The resolved scope selection.
173
179
  * @returns The `Task<SetupPlan>` the dispatcher/MCP handler interprets.
174
180
  * @note Impure — runs every target's real detection before returning a Task.
175
181
  */
@@ -178,7 +184,7 @@ async function runSetup(
178
184
  rt: PragmaRuntime,
179
185
  scope: ScopeSelection,
180
186
  ): Promise<Task<SetupPlan>> {
181
- await assertBandIsPossible(mode, scope);
187
+ await assertScopeIsPossible(mode, scope);
182
188
 
183
189
  // Lazy dynamic imports (lazy-React discipline): summon-core's barrel is
184
190
  // React-free, and the non-TTY branch picks autoPrompt/mcpPrompt (never mounts
@@ -203,6 +209,28 @@ async function runSetup(
203
209
  // to know which of the two it is.
204
210
  const previewing = interactionMode === "batch-dry-run";
205
211
 
212
+ // A CONVERGED machine has no question to ask. Every row is `none` or `skip`,
213
+ // so nothing composes an effect — and the wizard's confirm gate then mounted
214
+ // Ink to render summon-core's "No operations planned." above a "Proceed?"
215
+ // over an empty tally. For a sub-verb, whose prompt list is empty, that
216
+ // contentless gate was the FIRST AND ONLY thing the user ever saw. The
217
+ // explanation existed the whole time (each row's `none` detail, one child row
218
+ // per file/editor marked `unchanged`) and was dropped at the render seam.
219
+ //
220
+ // So: report the plan instead of asking a question with no content. There is
221
+ // no `preview: true` here — the run really did complete. `applied({})` marks
222
+ // each `none` row `{status:"noop", note:"unchanged"}`, `wasApplied` is then
223
+ // true, and `setupFormatters.plain` renders the recap. That also retires a
224
+ // second defect: a non-interactive converged run used to print the misleading
225
+ // "Nothing was applied. Run again with --yes to apply." over an all-`none`
226
+ // plan. A run whose every DETECTION threw is not converged in this sense —
227
+ // its rows are skips that report `failed` — so it falls through to the normal
228
+ // path, which is the one that raises.
229
+ if (!previewing && run.plan.rows.every((row) => !isActionable(row.action))) {
230
+ const converged = run.applied({});
231
+ if (!planExitFailed(converged)) return pure<SetupPlan>(converged);
232
+ }
233
+
206
234
  // Adaptation (b): a non-interactive run without `--yes` previews rather than
207
235
  // mutating. It is the same plan the dry-run prints, plus one hint line, at
208
236
  // exit 0 — the invocation completed a real read-only unit of work.
@@ -210,6 +238,23 @@ async function runSetup(
210
238
  return pure<SetupPlan>({ ...run.plan, preview: true });
211
239
  }
212
240
 
241
+ // Say what was DETECTED before asking anything about it. The data has been
242
+ // sitting in `run.plan` since before the prompt list was built — `inkPrompt`
243
+ // mounts no React until the first Prompt effect — so this is a rendering gap,
244
+ // not an ordering one, and closing it is nearly free.
245
+ //
246
+ // It MUST be emitted here, before `execute` is driven: the summary and the
247
+ // Ink frame both write to stderr, and only a line written first lands in
248
+ // scrollback ABOVE the frame. `rt.report` is the non-Ink seam — `--quiet`
249
+ // silences it and it is a no-op over MCP — so no React is loaded to print it.
250
+ // A preview says nothing here: the plan table IS its output.
251
+ if (!previewing) {
252
+ const summary = renderDetectionSummary(run.plan, {
253
+ verbose: rt.globalFlags.verbose,
254
+ });
255
+ if (summary !== undefined) rt.report?.(summary);
256
+ }
257
+
213
258
  const { transport, yes, signal, abort } = rt.interaction ?? {
214
259
  isTTY: false,
215
260
  transport: "cli" as const,
@@ -226,6 +271,19 @@ async function runSetup(
226
271
  signal,
227
272
  onCancel: abort,
228
273
  });
274
+ // Live progress in the PLAN's units, not the interpreter's: the wizard's
275
+ // default execution view is the per-effect transcript — right for `create`,
276
+ // whose unit of work IS the file, and wrong here, where one unit is one
277
+ // configured target and the transcript renders eighteen symlink rows for
278
+ // the row the plan called `skills link 9 skills → 2 folders`. Each row
279
+ // event carries the SAME sentence `renderProgressLine` prints, so the
280
+ // watcher and the recap reader see one dialect. `--verbose` withholds the
281
+ // listener, which falls the wizard back to the full effect transcript —
282
+ // the same detected-only-unless-verbose rule the detection summary and
283
+ // doctor already follow.
284
+ if (rt.globalFlags.verbose !== true) {
285
+ run.setRowListener((event) => session.reportStep(event));
286
+ }
229
287
  rt.exec = {
230
288
  promptHandler: session.promptHandler,
231
289
  onEffectStart: session.onEffectStart,
@@ -345,24 +403,24 @@ function setupVerb(
345
403
 
346
404
  const setupAllVerb = setupVerb(
347
405
  ["setup"],
348
- "Configure the global config, completions, the LSP, MCP, and skills.",
406
+ "Set up your config file, TAB completion, the editor extension, MCP, and skills.",
349
407
  "all",
350
408
  SELF_CAPABILITY,
351
409
  {
352
- doc: "Plans every target in the selected band, then applies the ones you keep. The user/home band is the default; the scope option chooses the per-project band, or both. A run with no attended terminal prints the plan and applies nothing unless the run is confirmed.",
410
+ doc: "Shows what each target needs, then applies the ones you keep. Everything is configured in your home directory by default; the scope option moves the run to this project alone, or covers both. Without an attended terminal the plan is printed and nothing is written unless the run is explicitly confirmed.",
353
411
  examples: [
354
412
  { cmd: `${BIN_NAME} setup` },
355
413
  {
356
414
  cmd: `${BIN_NAME} setup --dry-run`,
357
- note: "print the plan, write nothing",
415
+ note: "show the plan, write nothing",
358
416
  },
359
417
  {
360
418
  cmd: `${BIN_NAME} setup --local`,
361
- note: "configure the project band instead",
419
+ note: "configure this project instead of your home directory",
362
420
  },
363
421
  {
364
422
  cmd: `${BIN_NAME} setup mcp`,
365
- note: "just the MCP server registration",
423
+ note: "only register the MCP server",
366
424
  },
367
425
  ],
368
426
  },
@@ -370,35 +428,35 @@ const setupAllVerb = setupVerb(
370
428
 
371
429
  const configVerb = setupVerb(
372
430
  ["setup", "config"],
373
- "Create the global config file with its defaults.",
431
+ "Create your global config file, filled in with the defaults.",
374
432
  "config",
375
433
  SUB_CAPABILITY,
376
434
  );
377
435
 
378
436
  const mcpVerb = setupVerb(
379
437
  ["setup", "mcp"],
380
- `Register the ${BIN_NAME} MCP server in detected AI harnesses.`,
438
+ `Register the ${BIN_NAME} MCP server with the AI harnesses on this machine.`,
381
439
  "mcp",
382
440
  SUB_CAPABILITY,
383
441
  );
384
442
 
385
443
  const completionsVerb = setupVerb(
386
444
  ["setup", "completions"],
387
- "Install the shell-completion script for your shell.",
445
+ "Install TAB completion for the shell you are running.",
388
446
  "completions",
389
447
  SUB_CAPABILITY,
390
448
  );
391
449
 
392
450
  const skillsVerb = setupVerb(
393
451
  ["setup", "skills"],
394
- "Symlink discovered skills into each AI harness.",
452
+ "Link the skills you have installed into every AI harness that reads them.",
395
453
  "skills",
396
454
  SUB_CAPABILITY,
397
455
  );
398
456
 
399
457
  const lspVerb = setupVerb(
400
458
  ["setup", "lsp"],
401
- "Ensure the Terrazzo LSP VS Code extension is installed.",
459
+ "Install the Terrazzo design-token extension into your VS Code-family editors.",
402
460
  "lsp",
403
461
  SUB_CAPABILITY,
404
462
  );
@@ -1,14 +1,14 @@
1
1
  /**
2
2
  * THE target table — the one list `setup` and `doctor` both read.
3
3
  *
4
- * Five rows, each with a band column. This table drives the `setup <target>`
4
+ * Five rows, each with a scope column. This table drives the `setup <target>`
5
5
  * sub-verbs, the wizard's choices, the `--dry-run` plan, the progress lines, the
6
6
  * recap, the doctor rows, and each doctor row's `fix:` line. Adding a sixth
7
7
  * target is one row here — not a new sub-verb, a new check, a new recap branch
8
8
  * and a new fix string that four separate files have to agree about.
9
9
  *
10
10
  * A row owns: its identity (`id` is BOTH the sub-verb argument and the doctor
11
- * row name, so `mcp` and `setup mcp` visibly share a token), which bands it can
11
+ * row name, so `mcp` and `setup mcp` visibly share a token), which scopes it can
12
12
  * be installed into, how to DETECT its current state, how to PLAN a run from
13
13
  * that detection, and how to COMPOSE the forward and removal effects. Detection
14
14
  * runs once per invocation and feeds every reader, so setup and doctor cannot
@@ -47,7 +47,7 @@ import {
47
47
  type LspDetection,
48
48
  lspEditorNames,
49
49
  lspSkipReason,
50
- lspUninstallRemedy,
50
+ ownedLspEditors,
51
51
  } from "./operations/setupLsp.js";
52
52
  import {
53
53
  composeMcp,
@@ -65,6 +65,8 @@ import {
65
65
  ownedSkillLinks,
66
66
  type SkillsDetection,
67
67
  skillsSkipReason,
68
+ skillsSkipRemedy,
69
+ staleSkillLinks,
68
70
  } from "./operations/setupSkills.js";
69
71
  import {
70
72
  type PlanAction,
@@ -73,7 +75,7 @@ import {
73
75
  shortenPath,
74
76
  type TargetId,
75
77
  } from "./plan.js";
76
- import type { ScopeBand } from "./types.js";
78
+ import type { Scope } from "./types.js";
77
79
 
78
80
  /** What a target contributes to one plan row, before selection is applied. */
79
81
  export interface TargetDraft {
@@ -98,13 +100,13 @@ export interface TargetDefinition<D> {
98
100
  readonly id: TargetId;
99
101
  /** Human title, used where a sentence needs one; the id is the row name. */
100
102
  readonly title: string;
101
- readonly bands: readonly ScopeBand[];
102
- /** Real reads, up front, once per (target, band) per invocation. */
103
- detect(rt: PragmaRuntime, band: ScopeBand): Promise<D>;
103
+ readonly scopes: readonly Scope[];
104
+ /** Real reads, up front, once per (target, scope) per invocation. */
105
+ detect(rt: PragmaRuntime, scope: Scope): Promise<D>;
104
106
  /** What a forward run would do. */
105
- plan(detection: D, band: ScopeBand, roots: Roots): TargetDraft;
107
+ plan(detection: D, scope: Scope, roots: Roots): TargetDraft;
106
108
  /** What a removal would do — composed from what detection says we own. */
107
- removalPlan(detection: D, band: ScopeBand, roots: Roots): TargetDraft;
109
+ removalPlan(detection: D, scope: Scope, roots: Roots): TargetDraft;
108
110
  /**
109
111
  * The forward effects. `chosen` is the row's per-child selection (MCP file
110
112
  * paths); a row without children ignores it.
@@ -128,21 +130,21 @@ const defineTarget = <D>(target: TargetDefinition<D>): AnyTarget =>
128
130
  const configTarget = defineTarget<ConfigDetection>({
129
131
  id: "config",
130
132
  title: "Global configuration",
131
- bands: ["global"],
133
+ scopes: ["global"],
132
134
  detect: () => detectConfigFile(),
133
- plan: (d, _band, roots) => {
135
+ plan: (d, _scope, roots) => {
134
136
  const path = shortenPath(d.path, roots);
135
137
  if (!d.exists) return { action: "install", detail: path };
136
138
  return { action: "none", detail: `${path} — present` };
137
139
  },
138
- removalPlan: (d, _band, roots) => {
140
+ removalPlan: (d, _scope, roots) => {
139
141
  const path = shortenPath(d.path, roots);
140
142
  if (!d.exists) return { action: "none", detail: `${path} — absent` };
141
143
  if (!d.isSeed) {
142
144
  return {
143
145
  action: "skip",
144
146
  detail: path,
145
- reason: "kept — it holds your settings",
147
+ reason: `it holds your own settings, so ${path} stays`,
146
148
  };
147
149
  }
148
150
  return { action: "remove", detail: path };
@@ -154,9 +156,9 @@ const configTarget = defineTarget<ConfigDetection>({
154
156
  const completionsTarget = defineTarget<CompletionsDetection>({
155
157
  id: "completions",
156
158
  title: "Shell completions",
157
- bands: ["global"],
159
+ scopes: ["global"],
158
160
  detect: (rt) => detectCompletions(rt.cwd),
159
- plan: (d, _band, roots) => {
161
+ plan: (d, _scope, roots) => {
160
162
  // Never guess a shell. Installing for the wrong one is invisible until the
161
163
  // user presses TAB and nothing happens, so an unresolved shell is a named
162
164
  // skip whose remedy is the one action that DOES settle it.
@@ -187,13 +189,18 @@ const completionsTarget = defineTarget<CompletionsDetection>({
187
189
  };
188
190
  }
189
191
  const where = `${d.shell} → ${shortenPath(d.path, roots)}`;
190
- if (d.state === "installed") return { action: "none", detail: where };
192
+ // A no-op row says what it FOUND, not just where it looked: `no change`
193
+ // beside a bare path leaves the reader to guess whether the script is
194
+ // there and current or missing and unreachable.
195
+ if (d.state === "installed") {
196
+ return { action: "none", detail: `${where} — already up to date` };
197
+ }
191
198
  return {
192
199
  action: d.state === "stale" ? "update" : "install",
193
200
  detail: where,
194
201
  };
195
202
  },
196
- removalPlan: (d, _band, roots) => {
203
+ removalPlan: (d, _scope, roots) => {
197
204
  if (d.path === null || d.state === "absent") {
198
205
  return { action: "none", detail: "no script installed" };
199
206
  }
@@ -206,7 +213,7 @@ const completionsTarget = defineTarget<CompletionsDetection>({
206
213
  const lspTarget = defineTarget<LspDetection>({
207
214
  id: "lsp",
208
215
  title: "Terrazzo LSP extension",
209
- bands: ["global"],
216
+ scopes: ["global"],
210
217
  detect: (rt) => detectLsp(rt.cwd),
211
218
  plan: (d) => {
212
219
  if (d.state === "unknown") {
@@ -225,32 +232,71 @@ const lspTarget = defineTarget<LspDetection>({
225
232
  label: `${e.editor.cli} — ${e.editor.name}`,
226
233
  action: e.installed ? ("unchanged" as const) : ("add" as const),
227
234
  }));
235
+ // The detail names WHAT is being placed and WHERE — never the row's own
236
+ // state, which the action column already carries. `installed (VSCodium)`
237
+ // beside a child row reading `codium — VSCodium (unchanged)` said the
238
+ // editor's name twice and "nothing happens here" twice.
228
239
  if (d.state === "installed") {
229
240
  return {
230
241
  action: "none",
231
- detail: `installed (${lspEditorNames(d).join(", ")})`,
242
+ detail: `Terrazzo extension in ${lspEditorNames(d).join(", ")}`,
232
243
  children,
233
244
  };
234
245
  }
235
246
  const pending = d.editors.filter((e) => !e.installed);
236
247
  return {
237
248
  action: "install",
238
- detail: `${pending.length} ${pending.length === 1 ? "editor" : "editors"}`,
249
+ detail: `Terrazzo extension → ${pending
250
+ .map((e) => e.editor.name)
251
+ .join(", ")}`,
239
252
  children,
240
253
  };
241
254
  },
242
- removalPlan: (d) => ({
243
- action: "skip",
244
- detail: "not removed",
245
- reason: "an extension install cannot be reversed from here",
246
- remedy:
247
- lspUninstallRemedy(d) ??
248
- "no editor CLI is on PATH — uninstall it from your editor",
249
- }),
255
+ removalPlan: (d) => {
256
+ // No editor CLI on PATH is the SAME named skip the forward plan reports:
257
+ // there is nothing on this machine to run an uninstall with.
258
+ if (d.state === "unknown") {
259
+ return {
260
+ action: "skip",
261
+ detail: lspSkipReason(d),
262
+ reason: lspSkipReason(d),
263
+ remedy: LSP_SKIP_REMEDY,
264
+ };
265
+ }
266
+ // Removal is composed from what detection says we OWN — a copy that is
267
+ // present, whatever its version. The forward plan's version gate would let
268
+ // a dead pre-0.8.3 copy this command installed survive its own `--undo`.
269
+ const owned = ownedLspEditors(d);
270
+ if (owned.length === 0) {
271
+ return { action: "none", detail: "no editor carries the extension" };
272
+ }
273
+ return {
274
+ action: "remove",
275
+ detail: `Terrazzo extension from ${owned
276
+ .map((e) => e.editor.name)
277
+ .join(", ")}`,
278
+ children: owned.map((e) => ({
279
+ key: e.editor.cli,
280
+ label: `${e.editor.cli} — ${e.editor.name}`,
281
+ action: "update" as const,
282
+ })),
283
+ };
284
+ },
250
285
  compose: (d, chosen) => composeLsp(d, chosen),
251
286
  composeRemoval: (d) => composeLspRemoval(d),
252
287
  });
253
288
 
289
+ /**
290
+ * Why the `mcp` row has nothing to write in a scope — authored once, because
291
+ * `setup`'s row and `doctor`'s row are the same finding and must not word it
292
+ * differently. Neither sentence says "scope": the two scopes are `global` and
293
+ * `local project`, after the `--global` / `--local` flags that select them.
294
+ */
295
+ export const MCP_NO_LOCATION: Record<Scope, string> = {
296
+ global: "no AI harness on this machine keeps a global config",
297
+ project: "no AI harness in this project keeps a per-project config",
298
+ };
299
+
254
300
  /** One MCP file as a plan child: its path, the harnesses sharing it, its state. */
255
301
  const mcpChild = (
256
302
  d: McpDetection,
@@ -262,7 +308,7 @@ const mcpChild = (
262
308
  key: group.path,
263
309
  label: shortenPath(group.path, roots),
264
310
  action:
265
- state === "configured"
311
+ state === "registered"
266
312
  ? "unchanged"
267
313
  : state === "drifted"
268
314
  ? "update"
@@ -273,35 +319,34 @@ const mcpChild = (
273
319
  const mcpTarget = defineTarget<McpDetection>({
274
320
  id: "mcp",
275
321
  title: "MCP server registration",
276
- bands: ["global", "project"],
277
- detect: (rt, band) => detectMcp(rt, band),
278
- plan: (d, band, roots) => {
322
+ scopes: ["global", "project"],
323
+ detect: (rt, scope) => detectMcp(rt, scope),
324
+ plan: (d, scope, roots) => {
279
325
  if (d.groups.length === 0) {
280
326
  return {
281
327
  action: "skip",
282
- detail: "no harness config location in this band",
283
- reason:
284
- band === "project"
285
- ? "no AI harness in this project writes a per-repository config"
286
- : "no detected AI harness declares a user-level config location",
328
+ detail: MCP_NO_LOCATION[scope],
329
+ reason: MCP_NO_LOCATION[scope],
287
330
  };
288
331
  }
289
332
  const children = d.groups.map((group) => mcpChild(d, group, roots));
290
333
  const pending = children.filter((c) => c.action !== "unchanged");
291
334
  return {
292
335
  action: pending.length === 0 ? "none" : "update",
293
- detail: `${d.groups.length} ${d.groups.length === 1 ? "file" : "files"}`,
336
+ detail: `${d.groups.length} ${
337
+ d.groups.length === 1 ? "config file" : "config files"
338
+ }`,
294
339
  children,
295
340
  };
296
341
  },
297
- removalPlan: (d, _band, roots) => {
342
+ removalPlan: (d, _scope, roots) => {
298
343
  const owned = ownedMcpGroups(d);
299
344
  if (owned.length === 0) {
300
345
  return { action: "none", detail: "no entry to remove" };
301
346
  }
302
347
  return {
303
348
  action: "remove",
304
- detail: `${owned.length} ${owned.length === 1 ? "file" : "files"}`,
349
+ detail: `${owned.length} ${owned.length === 1 ? "config file" : "config files"}`,
305
350
  children: owned.map((group) => ({
306
351
  key: group.path,
307
352
  label: shortenPath(group.path, roots),
@@ -317,19 +362,42 @@ const mcpTarget = defineTarget<McpDetection>({
317
362
  const skillsTarget = defineTarget<SkillsDetection>({
318
363
  id: "skills",
319
364
  title: "Skill symlinks",
320
- bands: ["global", "project"],
321
- detect: (rt, band) => detectSkills(rt, band),
322
- plan: (d, band, roots) => {
323
- if (!d.available) {
324
- const reason = skillsSkipReason(shortenPath(d.sourceRoot, roots), band);
325
- return { action: "skip", detail: reason, reason };
365
+ scopes: ["global", "project"],
366
+ detect: (rt, scope) => detectSkills(rt, scope),
367
+ plan: (d, scope, roots) => {
368
+ // A forward run RECONCILES: stale links this scope owns are its work too, so
369
+ // an orphan-only tree is actionable rather than a row that plans `none` and
370
+ // is then deselected into doing nothing. `staleSkillLinks` carries the
371
+ // absent-root safety gate, so a machine whose source root does not exist
372
+ // contributes no sweep and the row falls back to the honest skip.
373
+ const stale = staleSkillLinks(d);
374
+ if (!d.available && stale.length === 0) {
375
+ const short = shortenPath(d.sourceRoot, roots);
376
+ const reason = skillsSkipReason(short, scope, d.rootExists);
377
+ // A skip with no remedy is a dead end — this one names the command that
378
+ // fills the scope's source root, exactly as the completions and lsp skips
379
+ // beside it name theirs.
380
+ return {
381
+ action: "skip",
382
+ detail: reason,
383
+ reason,
384
+ remedy: skillsSkipRemedy(short, scope),
385
+ };
326
386
  }
327
387
  const dirs = d.targets.map((t) => shortenPath(t.dir, roots)).join(", ");
328
- const where = `${d.skillCount} ${d.skillCount === 1 ? "skill" : "skills"} → ${d.targets.length} ${d.targets.length === 1 ? "dir" : "dirs"} (${dirs})`;
388
+ const where = `${d.skillCount} ${d.skillCount === 1 ? "skill" : "skills"} → ${d.targets.length} ${d.targets.length === 1 ? "folder" : "folders"} (${dirs})`;
389
+ const detail =
390
+ stale.length === 0
391
+ ? where
392
+ : `${where}, ${stale.length} stale ${stale.length === 1 ? "link" : "links"} to remove`;
329
393
  const pending = d.actions.filter((a) => a.action !== "skipped");
330
- return { action: pending.length === 0 ? "none" : "link", detail: where };
394
+ if (pending.length > 0) return { action: "link", detail };
395
+ // Nothing to link, but something to retire: `update` is the table's word
396
+ // for "this row has work that is not a fresh install".
397
+ if (stale.length > 0) return { action: "update", detail };
398
+ return { action: "none", detail };
331
399
  },
332
- removalPlan: (d, _band, roots) => {
400
+ removalPlan: (d, _scope, roots) => {
333
401
  const owned = ownedSkillLinks(d);
334
402
  if (owned.length === 0) {
335
403
  return { action: "none", detail: "no link to remove" };
@@ -362,6 +430,6 @@ export const TARGETS: readonly AnyTarget[] = [
362
430
  export const findTarget = (id: string): AnyTarget | undefined =>
363
431
  TARGETS.find((target) => target.id === id);
364
432
 
365
- /** Whether a target can be installed into a band. */
366
- export const supportsBand = (target: AnyTarget, band: ScopeBand): boolean =>
367
- target.bands.includes(band);
433
+ /** Whether a target can be installed into a scope. */
434
+ export const supportsScope = (target: AnyTarget, scope: Scope): boolean =>
435
+ target.scopes.includes(scope);
@@ -15,31 +15,23 @@ export type SetupMode =
15
15
  | "mcp"
16
16
  | "skills";
17
17
 
18
- /**
19
- * One of the two config bands: the per-user/home `global` band or the per-repo
20
- * `project` band. Mirrors `@canonical/harnesses`' `ScopeBand` structurally, but
21
- * is redeclared here so this statically-reachable type module never pulls the
22
- * harnesses runtime into the fast-path module graph (the lazy-dispatch invariant).
23
- */
24
- export type ScopeBand = "project" | "global";
25
-
26
- /**
27
- * The resolved `--scope` selection: which config band(s) a run touches.
28
- * `global` is the DEFAULT — the user/home band a machine-level installer
29
- * configures; `project` is the opt-in per-repository band; `both` is the
30
- * explicit "run each band in one invocation". Structurally mirrors the
31
- * harnesses `ScopeSelection`.
32
- */
33
- export type ScopeSelection = "project" | "global" | "both";
18
+ // The scope types are part of the rendering vocabulary — the one module that
19
+ // owns both the states and the words for them. Re-exported here so setup's
20
+ // type surface stays complete; the definitions (and the structural pin against
21
+ // `@canonical/harnesses`) live with the vocabulary.
22
+ export type {
23
+ Scope,
24
+ ScopeSelection,
25
+ } from "../../kernel/render/vocabulary.js";
34
26
 
35
27
  /**
36
28
  * The prior state of an MCP target file, read up front by `detectMcp`:
37
- * `absent` (no pragma entry yet), `configured` (a matching pragma entry already
29
+ * `absent` (no pragma entry yet), `registered` (a matching pragma entry already
38
30
  * present in every write — a re-run skips it), or `drifted` (a pragma entry
39
31
  * exists but differs, so a write updates it). Mirrors the skills step's
40
32
  * created/skipped/replaced idempotency at the file grain.
41
33
  */
42
- export type McpTargetState = "absent" | "configured" | "drifted";
34
+ export type McpTargetState = "absent" | "registered" | "drifted";
43
35
 
44
36
  /**
45
37
  * The prior on-disk state of the shell-completion script, read up front by