@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
@@ -19,16 +19,16 @@ import {
19
19
  } from "./plan.js";
20
20
  import {
21
21
  type AnyTarget,
22
- supportsBand,
22
+ supportsScope,
23
23
  TARGETS,
24
24
  type TargetDraft,
25
25
  } from "./targets.js";
26
- import type { ScopeBand, ScopeSelection } from "./types.js";
26
+ import type { Scope, ScopeSelection } from "./types.js";
27
27
 
28
28
  /** One detection, kept beside the row that produced it (and can read it back). */
29
29
  export interface DetectedRow {
30
30
  readonly target: AnyTarget;
31
- readonly band: ScopeBand;
31
+ readonly scope: Scope;
32
32
  readonly detection: never;
33
33
  /**
34
34
  * Why this row's detection did not settle, when it threw. `detection` is then
@@ -48,8 +48,8 @@ export interface DetectedPlan {
48
48
  readonly detected: readonly DetectedRow[];
49
49
  }
50
50
 
51
- /** The bands a `--scope` selection runs, global before project. */
52
- export const bandsForScope = (scope: ScopeSelection): readonly ScopeBand[] =>
51
+ /** The scopes a `--scope` selection runs, global before project. */
52
+ export const scopesForSelection = (scope: ScopeSelection): readonly Scope[] =>
53
53
  scope === "both" ? ["global", "project"] : [scope];
54
54
 
55
55
  /**
@@ -72,7 +72,7 @@ const messageOf = (reason: unknown): string =>
72
72
  reason instanceof Error ? reason.message : String(reason);
73
73
 
74
74
  /**
75
- * Detect every requested target in every band the scope runs.
75
+ * Detect every requested target in every scope the scope runs.
76
76
  *
77
77
  * Detections are started together and SETTLED INDEPENDENTLY: they are
78
78
  * independent reads, and a run-all that probed five targets serially paid for
@@ -82,13 +82,13 @@ const messageOf = (reason: unknown): string =>
82
82
  * unreadable config, a permission-denied directory — takes only its own row
83
83
  * down. Awaiting them as one `Promise.all` meant the first rejection prevented
84
84
  * a run from being built at all: none of the other targets ran, and doctor
85
- * rendered no banded rows whatsoever. The failed row carries its cause and is
85
+ * rendered no scoped rows whatsoever. The failed row carries its cause and is
86
86
  * reported as a row, which is the same shape a failed COMPOSE already has.
87
87
  *
88
88
  * @param rt - The per-invocation runtime.
89
89
  * @param ids - The targets to plan (all five for the run-all).
90
- * @param scope - The resolved band selection.
91
- * @returns One entry per (target, band) the scope actually runs.
90
+ * @param scope - The resolved scope selection.
91
+ * @returns One entry per (target, scope) the scope actually runs.
92
92
  * @note Impure — every target's `detect` reads the real filesystem.
93
93
  */
94
94
  export async function detectTargets(
@@ -97,25 +97,25 @@ export async function detectTargets(
97
97
  scope: ScopeSelection,
98
98
  ): Promise<DetectedRow[]> {
99
99
  const wanted = TARGETS.filter((target) => ids.includes(target.id));
100
- const pairs = bandsForScope(scope).flatMap((band) =>
100
+ const pairs = scopesForSelection(scope).flatMap((scope) =>
101
101
  wanted
102
- .filter((target) => supportsBand(target, band))
102
+ .filter((target) => supportsScope(target, scope))
103
103
  .map((target) => ({
104
104
  target,
105
- band,
105
+ scope,
106
106
  })),
107
107
  );
108
108
  const settled = await Promise.allSettled(
109
- pairs.map(({ target, band }) => target.detect(rt, band)),
109
+ pairs.map(({ target, scope }) => target.detect(rt, scope)),
110
110
  );
111
- return pairs.map(({ target, band }, index) => {
111
+ return pairs.map(({ target, scope }, index) => {
112
112
  const outcome = settled[index] as PromiseSettledResult<unknown>;
113
113
  if (outcome.status === "fulfilled") {
114
- return { target, band, detection: outcome.value as never };
114
+ return { target, scope, detection: outcome.value as never };
115
115
  }
116
116
  return {
117
117
  target,
118
- band,
118
+ scope,
119
119
  detection: undefined as never,
120
120
  failure: messageOf(outcome.reason),
121
121
  };
@@ -123,22 +123,26 @@ export async function detectTargets(
123
123
  }
124
124
 
125
125
  /**
126
- * The dim row a target gets when the SELECTED band cannot hold it — `setup
127
- * --local` still shows `completions`, saying why it is not in this run. A band
128
- * merely filters the run-all: the user asked for a band, not for that target, so
126
+ * The dim row a target gets when the SELECTED scope cannot hold it — `setup
127
+ * --local` still shows `completions`, saying why it is not in this run. A scope
128
+ * merely filters the run-all: the user asked for a scope, not for that target, so
129
129
  * nothing errors, but nothing is silently absent either. (Asking for the
130
- * impossible band on the SUB-VERB is a different thing entirely — a typed
130
+ * impossible scope on the SUB-VERB is a different thing entirely — a typed
131
131
  * contradiction, and a usage error.)
132
+ *
133
+ * The reason names the flag that WOULD reach the target, so the row is not a
134
+ * dead end: a reader who wanted `completions` under `--local` is told in the
135
+ * same line that `--global` is where it lives.
132
136
  */
133
- const outOfBandRow = (target: AnyTarget, band: ScopeBand): PlanRow => ({
137
+ const outOfScopeRow = (target: AnyTarget, scope: Scope): PlanRow => ({
134
138
  target: target.id,
135
- band,
139
+ scope,
136
140
  action: "skip",
137
- detail: "not in this band",
141
+ detail: "not part of this scope",
138
142
  reason:
139
- band === "project"
140
- ? "user-level only — it has no project band"
141
- : "project-level only — it has no global band",
143
+ scope === "project"
144
+ ? "this one is global only — run it with `--global`"
145
+ : "this one is per-project only — run it with `--local`",
142
146
  selected: false,
143
147
  });
144
148
 
@@ -151,11 +155,11 @@ const outOfBandRow = (target: AnyTarget, band: ScopeBand): PlanRow => ({
151
155
  */
152
156
  const failedDetectionRow = (
153
157
  target: AnyTarget,
154
- band: ScopeBand,
158
+ scope: Scope,
155
159
  failure: string,
156
160
  ): PlanRow => ({
157
161
  target: target.id,
158
- band,
162
+ scope,
159
163
  action: "skip",
160
164
  detail: "detection did not complete",
161
165
  reason: failure,
@@ -179,51 +183,51 @@ export function draftFor(
179
183
  removal: boolean,
180
184
  ): TargetDraft {
181
185
  return removal
182
- ? hit.target.removalPlan(hit.detection, hit.band, roots)
183
- : hit.target.plan(hit.detection, hit.band, roots);
186
+ ? hit.target.removalPlan(hit.detection, hit.scope, roots)
187
+ : hit.target.plan(hit.detection, hit.scope, roots);
184
188
  }
185
189
 
186
190
  /**
187
191
  * Project detections into plan rows.
188
192
  *
189
- * @param scope - The resolved band selection (named in the header).
193
+ * @param selection - The resolved scope selection (named in the header).
190
194
  * @param roots - The two named roots every path renders relative to.
191
- * @param detected - The detections, in table order per band.
192
- * @param ids - The targets that were REQUESTED, so an out-of-band one still
195
+ * @param detected - The detections, in table order per scope.
196
+ * @param ids - The targets that were REQUESTED, so an out-of-scope one still
193
197
  * gets a visible row.
194
198
  * @param removal - Build the removal plan (`--undo`) instead of the forward one.
195
199
  * @returns The plan.
196
200
  */
197
201
  export function buildPlan(
198
- scope: ScopeSelection,
202
+ selection: ScopeSelection,
199
203
  roots: SetupPlan["roots"],
200
204
  detected: readonly DetectedRow[],
201
205
  ids: readonly TargetId[],
202
206
  removal = false,
203
207
  ): SetupPlan {
204
208
  const rows: PlanRow[] = [];
205
- for (const band of bandsForScope(scope)) {
209
+ for (const scope of scopesForSelection(selection)) {
206
210
  for (const target of TARGETS) {
207
211
  if (!ids.includes(target.id)) continue;
208
212
  const hit = detected.find(
209
- (d) => d.target.id === target.id && d.band === band,
213
+ (d) => d.target.id === target.id && d.scope === scope,
210
214
  );
211
215
  if (hit === undefined) {
212
- // Only worth a line when the target has no row in ANY selected band —
216
+ // Only worth a line when the target has no row in ANY selected scope —
213
217
  // under `--scope both` a global-only target already appears above.
214
218
  const elsewhere = detected.some((d) => d.target.id === target.id);
215
- if (!elsewhere) rows.push(outOfBandRow(target, band));
219
+ if (!elsewhere) rows.push(outOfScopeRow(target, scope));
216
220
  continue;
217
221
  }
218
222
  const failure = detectionFailure(hit);
219
223
  if (failure !== undefined) {
220
- rows.push(failedDetectionRow(target, band, failure));
224
+ rows.push(failedDetectionRow(target, scope, failure));
221
225
  continue;
222
226
  }
223
227
  const draft = draftFor(hit, roots, removal);
224
228
  rows.push({
225
229
  target: target.id,
226
- band,
230
+ scope,
227
231
  action: draft.action,
228
232
  detail: draft.detail,
229
233
  ...(draft.reason === undefined ? {} : { reason: draft.reason }),
@@ -232,5 +236,5 @@ export function buildPlan(
232
236
  });
233
237
  }
234
238
  }
235
- return { scope, roots, rows };
239
+ return { scope: selection, roots, rows };
236
240
  }
@@ -9,4 +9,4 @@ export type {
9
9
  TargetId,
10
10
  } from "./plan.js";
11
11
  export { setupModule } from "./setup.verb.js";
12
- export type { ScopeBand, ScopeSelection } from "./types.js";
12
+ export type { Scope, ScopeSelection } from "./types.js";
@@ -20,8 +20,8 @@
20
20
  * who forgot `--yes` reads the hint on the very output they are capturing.
21
21
  *
22
22
  * An MCP tool call is programmatic consent: `transport === "mcp"` resolves as if
23
- * `--yes` were passed. The band default applies there too — the tool writes the
24
- * global band unless told otherwise.
23
+ * `--yes` were passed. The scope default applies there too — the tool writes the
24
+ * global scope unless told otherwise.
25
25
  */
26
26
 
27
27
  import type { InteractionMode } from "@canonical/summon-core/projection";
@@ -13,7 +13,7 @@
13
13
  *
14
14
  * The detection types are part of the surface, not an implementation leak: a
15
15
  * detection is the value passed from the read half to the compose half and on
16
- * to the plan rows, so `doctor`'s banded checks name the same records `setup`
16
+ * to the plan rows, so `doctor`'s scoped checks name the same records `setup`
17
17
  * acts on. The presentation helpers travel with them for the same reason — a
18
18
  * skip reason or an uninstall remedy is derived from one detection, and
19
19
  * deriving it twice is how the two surfaces start disagreeing about the same
@@ -54,6 +54,7 @@ export {
54
54
  lspEditorNames,
55
55
  lspSkipReason,
56
56
  lspUninstallRemedy,
57
+ ownedLspEditors,
57
58
  } from "./setupLsp.js";
58
59
  export type { McpDetection } from "./setupMcp.js";
59
60
  export {
@@ -61,6 +62,7 @@ export {
61
62
  composeMcpRemoval,
62
63
  detectMcp,
63
64
  mcpGroupState,
65
+ mcpWriteState,
64
66
  ownedMcpGroups,
65
67
  selectedGroups,
66
68
  } from "./setupMcp.js";
@@ -71,4 +73,7 @@ export {
71
73
  detectSkills,
72
74
  ownedSkillLinks,
73
75
  skillsSkipReason,
76
+ skillsSkipRemedy,
77
+ staleSkillLinks,
78
+ withinRoot,
74
79
  } from "./setupSkills.js";
@@ -5,7 +5,7 @@
5
5
  * gate, live progress, colours, and the shared cancel fixes by construction.
6
6
  *
7
7
  * The shape mirrors `create`: detection runs FOR REAL up front (once per
8
- * target/band, in `detectTargets`), then a PURE `generate` composes only the
8
+ * target/scope, in `detectTargets`), then a PURE `generate` composes only the
9
9
  * effects for the SELECTED rows. `generate` must stay re-interpretable —
10
10
  * `execute` invokes it more than once (the confirm-gate preview and the build) —
11
11
  * so it is composed from combinators and does no reads of its own.
@@ -55,7 +55,7 @@ import {
55
55
  type TargetId,
56
56
  withRows,
57
57
  } from "../plan.js";
58
- import type { ScopeBand, ScopeSelection, SetupMode } from "../types.js";
58
+ import type { Scope, ScopeSelection, SetupMode } from "../types.js";
59
59
 
60
60
  /** The answer key the run-all's row multiselect writes. */
61
61
  const ROWS_ANSWER = "targets";
@@ -72,9 +72,26 @@ const CHILD_ANSWER: Partial<Record<TargetId, string>> = {
72
72
  lsp: LSP_EDITORS_ANSWER,
73
73
  };
74
74
 
75
- /** A row's identity in an answer bag: band-qualified, so `both` stays unambiguous. */
76
- const rowKey = (band: ScopeBand, target: TargetId): string =>
77
- `${band}:${target}`;
75
+ /** A row's identity in an answer bag: scope-qualified, so `both` stays unambiguous. */
76
+ const rowKey = (scope: Scope, target: TargetId): string => `${scope}:${target}`;
77
+
78
+ /**
79
+ * One live row-progress event: a plan row starting, finishing, or failing,
80
+ * reported WHILE the composed task is being interpreted. `label` is the row's
81
+ * progress sentence — the same `target detail — note` shape
82
+ * `renderProgressLine` prints — so the listener (the Ink wizard's step view)
83
+ * shows the run in the plan's own vocabulary, not the effect transcript's.
84
+ *
85
+ * Structurally identical to summon-core's `StepReport` ON PURPOSE: the wizard
86
+ * branch forwards these events verbatim, and this module must not import
87
+ * summon-core values (it stays behind the verb's lazy import).
88
+ */
89
+ export interface RowEvent {
90
+ /** The row's stable identity — `scope:target`. */
91
+ readonly key: string;
92
+ readonly label: string;
93
+ readonly status: "start" | "done" | "failed";
94
+ }
78
95
 
79
96
  /**
80
97
  * A ready-to-run setup invocation: the plan as detected, the synthesized
@@ -86,6 +103,15 @@ export interface SetupRun {
86
103
  readonly generator: GeneratorDefinition;
87
104
  /** The plan with the wizard's selection and the run's outcomes applied. */
88
105
  applied(answers: Record<string, unknown>): SetupPlan;
106
+ /**
107
+ * Register THE live row listener (the Ink wizard's step adapter). The
108
+ * composed rows emit through it while they are interpreted — which means
109
+ * EVERY interpretation fires it, the confirm gate's honest preview included;
110
+ * the receiving session drops reports outside its executing phase, exactly
111
+ * as the {@link OutcomeSink} relies on last-drive-wins for its records. No
112
+ * listener registered (a `--yes` run, `--dry-run`, MCP) costs nothing.
113
+ */
114
+ setRowListener(listener: (event: RowEvent) => void): void;
89
115
  }
90
116
 
91
117
  /**
@@ -144,25 +170,50 @@ const rowPragmaError = (error: TaskError): PragmaError =>
144
170
  * throw bypasses the trampoline's recovery frames — which is why the row bodies
145
171
  * raise via `checkExecOk`/`failPragma`, never a bare throw. Interruption bypasses
146
172
  * recovery by interpreter invariant, so Ctrl-C still stops the whole run.
173
+ *
174
+ * Each row is also BRACKETED with {@link RowEvent}s — `start` as its body is
175
+ * entered, `done`/`failed` as it settles — emitted from the continuations,
176
+ * which run at interpretation time. Like the sink writes beside them, the
177
+ * emits capture nothing IN the task: the composition stays pure and
178
+ * re-interpretable, and each drive simply narrates itself as it runs.
147
179
  */
148
180
  const runRowsIsolated = (
149
181
  sink: OutcomeSink,
150
- rows: readonly { key: string; task: Task<void> }[],
182
+ rows: readonly {
183
+ key: string;
184
+ label: string;
185
+ doneLabel: string;
186
+ task: Task<void>;
187
+ }[],
188
+ emit: (event: RowEvent) => void,
151
189
  ): Task<void> =>
152
190
  flatMap(pure(undefined), (): Task<void> => {
153
191
  sink.clear();
154
192
  return sequence_(
155
- rows.map(({ key, task }) =>
156
- recover(task, (error) =>
157
- flatMap(
158
- warn(
159
- `The ${key.split(":")[1]} step did not complete — continuing with the remaining targets.`,
160
- ),
161
- () => {
162
- sink.record(key, error);
193
+ rows.map(({ key, label, doneLabel, task }) =>
194
+ recover(
195
+ flatMap(pure(undefined), () => {
196
+ emit({ key, label, status: "start" });
197
+ return flatMap(task, () => {
198
+ emit({ key, label: doneLabel, status: "done" });
163
199
  return pure(undefined);
164
- },
165
- ),
200
+ });
201
+ }),
202
+ (error) =>
203
+ flatMap(
204
+ warn(
205
+ `The ${key.split(":")[1]} step did not complete — continuing with the remaining targets.`,
206
+ ),
207
+ () => {
208
+ sink.record(key, error);
209
+ emit({
210
+ key,
211
+ label: `${label} — ${rowPragmaError(error).message}`,
212
+ status: "failed",
213
+ });
214
+ return pure(undefined);
215
+ },
216
+ ),
166
217
  ),
167
218
  ),
168
219
  );
@@ -198,7 +249,7 @@ const buildRowsPrompt = (plan: SetupPlan): PromptDefinition => {
198
249
  .filter((row) => row.action !== "skip")
199
250
  .map((row) => ({
200
251
  label: `${row.target} — ${row.detail}${row.action === "none" ? " (already configured)" : ""}`,
201
- value: rowKey(row.band, row.target),
252
+ value: rowKey(row.scope, row.target),
202
253
  }));
203
254
  return {
204
255
  name: ROWS_ANSWER,
@@ -207,27 +258,27 @@ const buildRowsPrompt = (plan: SetupPlan): PromptDefinition => {
207
258
  choices,
208
259
  default: plan.rows
209
260
  .filter((row) => row.selected)
210
- .map((row) => rowKey(row.band, row.target)),
261
+ .map((row) => rowKey(row.scope, row.target)),
211
262
  };
212
263
  };
213
264
 
214
- /** The opt-in "customize which files" gate — defaults to false. */
215
- const buildCustomizePrompt = (
216
- when?: PromptDefinition["when"],
217
- ): PromptDefinition => ({
218
- name: "customize",
219
- type: "confirm",
220
- message: `Customize which files ${BIN_NAME} configures?`,
221
- default: false,
222
- when,
223
- });
224
-
225
265
  /**
226
266
  * The per-file MCP multiselect — one row per deduplicated config file, across
227
- * every band the run covers. An already-current file is DEFAULT-DESELECTED; a
228
- * file that is absent or drifted stays selected. Retained unchanged from the
229
- * landed opt-in narrowing: it is row-level CHILD selection, which is exactly
230
- * what the plan's child rows are.
267
+ * every scope the run covers. An already-current file is DEFAULT-DESELECTED; a
268
+ * file that is absent or drifted stays selected. It is row-level CHILD
269
+ * selection, which is exactly what the plan's child rows are.
270
+ *
271
+ * It used to sit behind a meta-question — "Customize which files pragma
272
+ * configures?", answered no by default. That gate was read in exactly ONE place
273
+ * in the repo (this prompt's `when`) and nowhere else: not by `generate`, not by
274
+ * `applied`, not by any plan, recap, renderer or JSON projection. Answering no
275
+ * meant the child prompt was never asked, its key was absent from the answer
276
+ * bag, `readList` returned `undefined`, and the compose bodies read that as
277
+ * "all" — so everything detected was configured anyway. A meta-question in front
278
+ * of a direct question is one more thing to answer and no more control; the
279
+ * choices below already carry the detected state per child (`— unchanged`,
280
+ * `— add`, `— update`), which is the answer the meta-question was standing in
281
+ * front of.
231
282
  */
232
283
  const buildChildPrompt = (
233
284
  plan: SetupPlan,
@@ -251,7 +302,7 @@ const buildChildPrompt = (
251
302
  };
252
303
  };
253
304
 
254
- /** Every child row a target contributes, across the bands in the plan. */
305
+ /** Every child row a target contributes, across the scopes in the plan. */
255
306
  const childrenOf = (
256
307
  plan: SetupPlan,
257
308
  target: TargetId,
@@ -268,7 +319,7 @@ const isChosen = (
268
319
  row.action !== "skip" &&
269
320
  (chosen === undefined
270
321
  ? row.selected
271
- : chosen.includes(rowKey(row.band, row.target)));
322
+ : chosen.includes(rowKey(row.scope, row.target)));
272
323
 
273
324
  /**
274
325
  * What a row DID, as a past-tense word. The row's detail says what and where;
@@ -320,7 +371,7 @@ function childNote(
320
371
  *
321
372
  * @param rt - The per-invocation runtime.
322
373
  * @param mode - The entry point: the run-all, or one target.
323
- * @param scope - The resolved band selection.
374
+ * @param scope - The resolved scope selection.
324
375
  * @param removal - Compose the removal (`--undo`) instead of the install.
325
376
  * @returns The plan, the generator, and the answers-to-plan projection.
326
377
  * @note Impure — runs every selected target's real detection.
@@ -350,35 +401,51 @@ export async function buildSetupRun(
350
401
  ] as const
351
402
  ).filter(([target]) => !removal && childrenOf(plan, target).length > 1);
352
403
 
353
- if (narrowable.length > 0) {
354
- const anyChosen = (answers: Record<string, unknown>): boolean => {
404
+ for (const [target, message] of narrowable) {
405
+ // Asked whenever the row it narrows is in the run — one direct question,
406
+ // no meta-question in front of it. Removing a prompt cannot break the
407
+ // non-interactive paths: `autoPrompt`/`mcpPrompt` return each question's
408
+ // default, so a smaller prompt list means a smaller answer bag, and the
409
+ // compose bodies already read an absent child answer as "all". (The
410
+ // asymmetry is the reason this direction is safe: ADDING a defaultless
411
+ // prompt would hard-fail with MISSING_REQUIRED_ANSWER.)
412
+ const rowChosen = (answers: Record<string, unknown>): boolean => {
355
413
  const chosen = readList(answers, ROWS_ANSWER);
356
- return narrowable.some(([target]) =>
357
- plan.rows.some((row) => row.target === target && isChosen(row, chosen)),
414
+ return plan.rows.some(
415
+ (row) => row.target === target && isChosen(row, chosen),
358
416
  );
359
417
  };
360
- prompts.push(buildCustomizePrompt(anyChosen));
361
- for (const [target, message] of narrowable) {
362
- const rowChosen = (answers: Record<string, unknown>): boolean => {
363
- const chosen = readList(answers, ROWS_ANSWER);
364
- return plan.rows.some(
365
- (row) => row.target === target && isChosen(row, chosen),
366
- );
367
- };
368
- prompts.push(
369
- buildChildPrompt(
370
- plan,
371
- target,
372
- message,
373
- (answers) => answers.customize === true && rowChosen(answers),
374
- ),
375
- );
376
- }
418
+ prompts.push(buildChildPrompt(plan, target, message, rowChosen));
377
419
  }
378
420
 
379
421
  /** The detection behind a row, for composing and for re-reading its draft. */
380
422
  const detectionFor = (row: PlanRow): DetectedRow | undefined =>
381
- detected.find((d) => d.target.id === row.target && d.band === row.band);
423
+ detected.find((d) => d.target.id === row.target && d.scope === row.scope);
424
+
425
+ /**
426
+ * The success note a row's completion carries — ONE derivation for the
427
+ * recap's outcome and the live row event's `done` label, so the sentence a
428
+ * watcher saw land is the sentence the recap repeats. A removal never
429
+ * borrows the forward child summary: its children are the files the entry is
430
+ * being taken OUT of, and counting them as `1 updated` described the
431
+ * opposite of what the run had just done.
432
+ */
433
+ const noteFor = (
434
+ row: PlanRow,
435
+ answers: Record<string, unknown>,
436
+ ): string | undefined => {
437
+ if (removal) return ACTION_NOTES[row.action];
438
+ const childKey = CHILD_ANSWER[row.target];
439
+ const kept =
440
+ childKey === undefined ? undefined : readList(answers, childKey);
441
+ return childNote(row, kept) ?? ACTION_NOTES[row.action];
442
+ };
443
+
444
+ /** The live row listener, when a wizard is watching. See {@link SetupRun}. */
445
+ let rowListener: ((event: RowEvent) => void) | undefined;
446
+
447
+ /** The id column width — the recap's own rule, so the columns agree. */
448
+ const idWidth = Math.max(...plan.rows.map((row) => row.target.length));
382
449
 
383
450
  const generator: GeneratorDefinition = {
384
451
  meta: buildMeta(
@@ -399,9 +466,21 @@ export async function buildSetupRun(
399
466
  const task = removal
400
467
  ? hit.target.composeRemoval(hit.detection)
401
468
  : hit.target.compose(hit.detection, children);
402
- return [{ key: rowKey(row.band, row.target), task }];
469
+ // The row's progress sentence — the `target detail — note` shape
470
+ // `renderProgressLine` prints — so the live view and the recap say the
471
+ // same thing about the same row.
472
+ const label = `${row.target.padEnd(idWidth)} ${row.detail}`;
473
+ const note = noteFor(row, answers);
474
+ return [
475
+ {
476
+ key: rowKey(row.scope, row.target),
477
+ label,
478
+ doneLabel: note === undefined ? label : `${label} — ${note}`,
479
+ task,
480
+ },
481
+ ];
403
482
  });
404
- return runRowsIsolated(sink, tasks);
483
+ return runRowsIsolated(sink, tasks, (event) => rowListener?.(event));
405
484
  },
406
485
  };
407
486
 
@@ -432,7 +511,7 @@ export async function buildSetupRun(
432
511
  // Convergence is a real result: one quiet line per row, zero writes.
433
512
  if (row.action === "none") return { status: "noop", note: "unchanged" };
434
513
  if (!isChosen(row, chosen)) return undefined;
435
- const error = sink.get(rowKey(row.band, row.target));
514
+ const error = sink.get(rowKey(row.scope, row.target));
436
515
  if (error !== undefined) {
437
516
  const pragma = rowPragmaError(error);
438
517
  return {
@@ -444,15 +523,7 @@ export async function buildSetupRun(
444
523
  };
445
524
  }
446
525
  if (!isActionable(row.action)) return { status: "noop" };
447
- // A removal never borrows the forward child summary: its children are the
448
- // files the entry is being taken OUT of, and counting them as "1 updated"
449
- // described the opposite of what the run had just done.
450
- const childKey = CHILD_ANSWER[row.target];
451
- const kept =
452
- childKey === undefined ? undefined : readList(answers, childKey);
453
- const note = removal
454
- ? ACTION_NOTES[row.action]
455
- : (childNote(row, kept) ?? ACTION_NOTES[row.action]);
526
+ const note = noteFor(row, answers);
456
527
  return {
457
528
  status: removal ? "removed" : "done",
458
529
  ...(note === undefined ? {} : { note }),
@@ -462,6 +533,9 @@ export async function buildSetupRun(
462
533
  return {
463
534
  plan,
464
535
  generator,
536
+ setRowListener: (listener) => {
537
+ rowListener = listener;
538
+ },
465
539
  applied: (answers) => {
466
540
  const chosen = readList(answers, ROWS_ANSWER);
467
541
  return withRows(