@canonical/pragma-cli 0.35.0 → 0.37.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (295) hide show
  1. package/README.md +2 -2
  2. package/bundled-skills/add-standard/SKILL.md +338 -0
  3. package/bundled-skills/adoption-a1-styles/SKILL.md +269 -0
  4. package/bundled-skills/adoption-a2-components/SKILL.md +291 -0
  5. package/bundled-skills/adoption-a3-forms/SKILL.md +260 -0
  6. package/bundled-skills/anatomy-author/ANATOMY_DSL_SPEC.md +406 -0
  7. package/bundled-skills/anatomy-author/SKILL.md +1226 -0
  8. package/bundled-skills/design-auditor/SKILL.md +455 -0
  9. package/bundled-skills/implement-component/SKILL.md +333 -0
  10. package/bundled-skills/specify-component/SKILL.md +331 -0
  11. package/bundled-skills/specify-pattern/SKILL.md +292 -0
  12. package/dist/package.json +24 -20
  13. package/dist/pragma.conf.js +266 -34
  14. package/dist/pragma.conf.js.map +1 -1
  15. package/dist/src/bin.js +1 -1
  16. package/dist/src/bin.js.map +1 -1
  17. package/dist/src/capabilities/capabilities/hints.js +1 -1
  18. package/dist/src/capabilities/capabilities/hints.js.map +1 -1
  19. package/dist/src/capabilities/colophon/colophon.render.js +1 -1
  20. package/dist/src/capabilities/colophon/colophon.render.js.map +1 -1
  21. package/dist/src/capabilities/create/create.verb.js +2 -1
  22. package/dist/src/capabilities/create/create.verb.js.map +1 -1
  23. package/dist/src/capabilities/create/createSurface.generated.js +23 -1
  24. package/dist/src/capabilities/create/createSurface.generated.js.map +1 -1
  25. package/dist/src/capabilities/create/mount.js +4 -3
  26. package/dist/src/capabilities/create/mount.js.map +1 -1
  27. package/dist/src/capabilities/doctor/checks/checkNodeVersion.js +11 -6
  28. package/dist/src/capabilities/doctor/checks/checkNodeVersion.js.map +1 -1
  29. package/dist/src/capabilities/doctor/checks/checkPackageRefs.js +60 -4
  30. package/dist/src/capabilities/doctor/checks/checkPackageRefs.js.map +1 -1
  31. package/dist/src/capabilities/doctor/checks/checkShellCompletions.js +8 -8
  32. package/dist/src/capabilities/doctor/checks/checkShellCompletions.js.map +1 -1
  33. package/dist/src/capabilities/doctor/checks/harnessInventory.js +195 -0
  34. package/dist/src/capabilities/doctor/checks/harnessInventory.js.map +1 -0
  35. package/dist/src/capabilities/doctor/checks/index.js +6 -6
  36. package/dist/src/capabilities/doctor/checks/isSupportedNodeVersion.js +38 -0
  37. package/dist/src/capabilities/doctor/checks/isSupportedNodeVersion.js.map +1 -0
  38. package/dist/src/capabilities/doctor/checks/targetHealth.js +194 -50
  39. package/dist/src/capabilities/doctor/checks/targetHealth.js.map +1 -1
  40. package/dist/src/capabilities/doctor/doctor.render.js +43 -64
  41. package/dist/src/capabilities/doctor/doctor.render.js.map +1 -1
  42. package/dist/src/capabilities/doctor/doctor.verb.js +6 -2
  43. package/dist/src/capabilities/doctor/doctor.verb.js.map +1 -1
  44. package/dist/src/capabilities/doctor/runChecks.js +14 -11
  45. package/dist/src/capabilities/doctor/runChecks.js.map +1 -1
  46. package/dist/src/capabilities/graph/query.render.js +2 -2
  47. package/dist/src/capabilities/graph/query.render.js.map +1 -1
  48. package/dist/src/capabilities/info/collectInfo.js +8 -2
  49. package/dist/src/capabilities/info/collectInfo.js.map +1 -1
  50. package/dist/src/capabilities/info/info.render.js +6 -2
  51. package/dist/src/capabilities/info/info.render.js.map +1 -1
  52. package/dist/src/capabilities/prompt/prompt.render.js +2 -2
  53. package/dist/src/capabilities/prompt/prompt.render.js.map +1 -1
  54. package/dist/src/capabilities/setup/buildPlan.js +43 -39
  55. package/dist/src/capabilities/setup/buildPlan.js.map +1 -1
  56. package/dist/src/capabilities/setup/mode.js +2 -2
  57. package/dist/src/capabilities/setup/operations/index.js +4 -4
  58. package/dist/src/capabilities/setup/operations/index.js.map +1 -1
  59. package/dist/src/capabilities/setup/operations/setupCompletions.js +6 -2
  60. package/dist/src/capabilities/setup/operations/setupCompletions.js.map +1 -1
  61. package/dist/src/capabilities/setup/operations/setupConfig.js +4 -2
  62. package/dist/src/capabilities/setup/operations/setupConfig.js.map +1 -1
  63. package/dist/src/capabilities/setup/operations/setupGenerator.js +156 -58
  64. package/dist/src/capabilities/setup/operations/setupGenerator.js.map +1 -1
  65. package/dist/src/capabilities/setup/operations/setupLsp.js +136 -38
  66. package/dist/src/capabilities/setup/operations/setupLsp.js.map +1 -1
  67. package/dist/src/capabilities/setup/operations/setupMcp.js +101 -52
  68. package/dist/src/capabilities/setup/operations/setupMcp.js.map +1 -1
  69. package/dist/src/capabilities/setup/operations/setupSkills.js +329 -49
  70. package/dist/src/capabilities/setup/operations/setupSkills.js.map +1 -1
  71. package/dist/src/capabilities/setup/plan.js.map +1 -1
  72. package/dist/src/capabilities/setup/plan.render.js +144 -66
  73. package/dist/src/capabilities/setup/plan.render.js.map +1 -1
  74. package/dist/src/capabilities/setup/setup.render.js +1 -1
  75. package/dist/src/capabilities/setup/setup.verb.js +165 -63
  76. package/dist/src/capabilities/setup/setup.verb.js.map +1 -1
  77. package/dist/src/capabilities/setup/targets.js +122 -51
  78. package/dist/src/capabilities/setup/targets.js.map +1 -1
  79. package/dist/src/capabilities/shared/index.js +7 -6
  80. package/dist/src/capabilities/shared/index.js.map +1 -1
  81. package/dist/src/capabilities/shared/packageManager.js +300 -54
  82. package/dist/src/capabilities/shared/packageManager.js.map +1 -1
  83. package/dist/src/capabilities/skill/discover.js +97 -11
  84. package/dist/src/capabilities/skill/discover.js.map +1 -1
  85. package/dist/src/capabilities/skill/render.js +2 -2
  86. package/dist/src/capabilities/skill/render.js.map +1 -1
  87. package/dist/src/capabilities/sources/installSkills.js +145 -3
  88. package/dist/src/capabilities/sources/installSkills.js.map +1 -1
  89. package/dist/src/capabilities/sources/runUpdate.js +68 -2
  90. package/dist/src/capabilities/sources/runUpdate.js.map +1 -1
  91. package/dist/src/capabilities/sources/update.verb.js +3 -3
  92. package/dist/src/capabilities/sources/update.verb.js.map +1 -1
  93. package/dist/src/capabilities/upgrade/runUpgrade.js +43 -7
  94. package/dist/src/capabilities/upgrade/runUpgrade.js.map +1 -1
  95. package/dist/src/capabilities/upgrade/upgrade.render.js +10 -2
  96. package/dist/src/capabilities/upgrade/upgrade.render.js.map +1 -1
  97. package/dist/src/constants.js +13 -2
  98. package/dist/src/constants.js.map +1 -1
  99. package/dist/src/kernel/index.js +11 -2
  100. package/dist/src/kernel/index.js.map +1 -1
  101. package/dist/src/kernel/interactivity.js +68 -0
  102. package/dist/src/kernel/interactivity.js.map +1 -0
  103. package/dist/src/kernel/packs/renderPack.js +3 -3
  104. package/dist/src/kernel/packs/renderPack.js.map +1 -1
  105. package/dist/src/kernel/packs/resolveEntity.js +63 -12
  106. package/dist/src/kernel/packs/resolveEntity.js.map +1 -1
  107. package/dist/src/kernel/packs/runBodies.js +57 -4
  108. package/dist/src/kernel/packs/runBodies.js.map +1 -1
  109. package/dist/src/kernel/packs/schema.js +54 -2
  110. package/dist/src/kernel/packs/schema.js.map +1 -1
  111. package/dist/src/kernel/packs/sparql/applyFilters.js +104 -5
  112. package/dist/src/kernel/packs/sparql/applyFilters.js.map +1 -1
  113. package/dist/src/kernel/packs/sparql/buildLookupQuery.js +310 -43
  114. package/dist/src/kernel/packs/sparql/buildLookupQuery.js.map +1 -1
  115. package/dist/src/kernel/packs/types.js.map +1 -1
  116. package/dist/src/kernel/project/cli/buildProgram.js +1 -1
  117. package/dist/src/kernel/project/cli/buildProgram.js.map +1 -1
  118. package/dist/src/kernel/project/cli/dispatch.js +46 -24
  119. package/dist/src/kernel/project/cli/dispatch.js.map +1 -1
  120. package/dist/src/kernel/project/cli/globalFlags.js +4 -1
  121. package/dist/src/kernel/project/cli/globalFlags.js.map +1 -1
  122. package/dist/src/kernel/project/cli/helpFormat.js +1 -2
  123. package/dist/src/kernel/project/cli/helpFormat.js.map +1 -1
  124. package/dist/src/kernel/project/cli/rootHelp.js +42 -9
  125. package/dist/src/kernel/project/cli/rootHelp.js.map +1 -1
  126. package/dist/src/kernel/project/mcp/registerVerb.js +16 -1
  127. package/dist/src/kernel/project/mcp/registerVerb.js.map +1 -1
  128. package/dist/src/kernel/render/renderers.js +1 -1
  129. package/dist/src/kernel/render/style.js +29 -9
  130. package/dist/src/kernel/render/style.js.map +1 -1
  131. package/dist/src/kernel/render/vocabulary.js +174 -0
  132. package/dist/src/kernel/render/vocabulary.js.map +1 -0
  133. package/dist/src/kernel/runtime/graphpack/embedded/pack.generated.js +2 -3
  134. package/dist/src/kernel/runtime/graphpack/embedded/pack.generated.js.map +1 -1
  135. package/dist/src/kernel/runtime/graphpack/embedded/pack.index.generated.js +1 -1
  136. package/dist/src/kernel/runtime/graphpack/embedded/pack.index.generated.js.map +1 -1
  137. package/dist/src/kernel/runtime/graphpack/embedded/pack.manifest.generated.js +4 -0
  138. package/dist/src/kernel/runtime/graphpack/embedded/pack.manifest.generated.js.map +1 -0
  139. package/dist/src/kernel/runtime/graphpack/embedded.js +13 -6
  140. package/dist/src/kernel/runtime/graphpack/embedded.js.map +1 -1
  141. package/dist/src/kernel/runtime/graphpack/read.js +14 -9
  142. package/dist/src/kernel/runtime/graphpack/read.js.map +1 -1
  143. package/dist/src/kernel/runtime/loadSession.js +3 -1
  144. package/dist/src/kernel/runtime/loadSession.js.map +1 -1
  145. package/dist/src/kernel/runtime/probe.js +1 -1
  146. package/dist/src/kernel/runtime/probe.js.map +1 -1
  147. package/dist/src/kernel/runtime/refs/parseRef.js +17 -1
  148. package/dist/src/kernel/runtime/refs/parseRef.js.map +1 -1
  149. package/dist/src/kernel/runtime/refs/resolve.js +9 -3
  150. package/dist/src/kernel/runtime/refs/resolve.js.map +1 -1
  151. package/dist/src/kernel/spec/emitReference.js +1 -1
  152. package/dist/src/kernel/spec/emitReference.js.map +1 -1
  153. package/package.json +24 -20
  154. package/pragma.conf.ts +272 -34
  155. package/src/bin.ts +8 -1
  156. package/src/capabilities/block.parity.test.ts +25 -11
  157. package/src/capabilities/block.shipped.exec.test.ts +282 -0
  158. package/src/capabilities/block.tierRank.exec.test.ts +300 -0
  159. package/src/capabilities/capabilities/hints.ts +1 -1
  160. package/src/capabilities/colophon/colophon.render.ts +1 -2
  161. package/src/capabilities/colophon/colophon.test.ts +2 -2
  162. package/src/capabilities/config/collectConfigShow.test.ts +1 -0
  163. package/src/capabilities/create/byteEquality.test.ts +28 -1
  164. package/src/capabilities/create/create.test.ts +1 -0
  165. package/src/capabilities/create/create.verb.ts +2 -1
  166. package/src/capabilities/create/createGrammar.test.ts +5 -2
  167. package/src/capabilities/create/createSurface.generated.ts +23 -1
  168. package/src/capabilities/create/crossCli.subprocess.test.ts +10 -3
  169. package/src/capabilities/create/mount.ts +3 -3
  170. package/src/capabilities/distribution.parity.test.ts +4 -0
  171. package/src/capabilities/doctor/checks/checkNodeVersion.test.ts +49 -0
  172. package/src/capabilities/doctor/checks/checkNodeVersion.ts +11 -7
  173. package/src/capabilities/doctor/checks/checkPackageRefs.ts +69 -4
  174. package/src/capabilities/doctor/checks/checkShellCompletions.test.ts +6 -4
  175. package/src/capabilities/doctor/checks/checkShellCompletions.ts +8 -8
  176. package/src/capabilities/doctor/checks/harnessInventory.test.ts +219 -0
  177. package/src/capabilities/doctor/checks/harnessInventory.ts +298 -0
  178. package/src/capabilities/doctor/checks/index.ts +6 -6
  179. package/src/capabilities/doctor/checks/isSupportedNodeVersion.test.ts +111 -0
  180. package/src/capabilities/doctor/checks/isSupportedNodeVersion.ts +34 -0
  181. package/src/capabilities/doctor/checks/mcpCommand.test.ts +7 -1
  182. package/src/capabilities/doctor/checks/targetHealth.ts +229 -59
  183. package/src/capabilities/doctor/doctor.render.test.ts +108 -32
  184. package/src/capabilities/doctor/doctor.render.ts +56 -80
  185. package/src/capabilities/doctor/doctor.test.ts +247 -35
  186. package/src/capabilities/doctor/doctor.verb.ts +7 -2
  187. package/src/capabilities/doctor/runChecks.ts +14 -11
  188. package/src/capabilities/doctor/types.ts +15 -24
  189. package/src/capabilities/emptyRecovery.test.ts +10 -2
  190. package/src/capabilities/graph/query.render.ts +2 -2
  191. package/src/capabilities/info/collectInfo.ts +8 -1
  192. package/src/capabilities/info/info.render.ts +6 -2
  193. package/src/capabilities/info/info.test.ts +37 -1
  194. package/src/capabilities/info/types.ts +10 -2
  195. package/src/capabilities/lazy.test.ts +26 -2
  196. package/src/capabilities/listLookup.shipped.exec.test.ts +340 -0
  197. package/src/capabilities/prompt/prompt.render.ts +2 -2
  198. package/src/capabilities/renderVocabularySync.test.ts +82 -0
  199. package/src/capabilities/resources/resources.test.ts +38 -2
  200. package/src/capabilities/scopeTypeSync.test.ts +29 -15
  201. package/src/capabilities/setup/buildPlan.ts +45 -41
  202. package/src/capabilities/setup/index.ts +1 -1
  203. package/src/capabilities/setup/mode.ts +2 -2
  204. package/src/capabilities/setup/operations/index.ts +6 -1
  205. package/src/capabilities/setup/operations/setupCompletions.ts +9 -2
  206. package/src/capabilities/setup/operations/setupConfig.ts +7 -2
  207. package/src/capabilities/setup/operations/setupGenerator.ts +227 -84
  208. package/src/capabilities/setup/operations/setupLsp.ts +203 -38
  209. package/src/capabilities/setup/operations/setupMcp.ts +139 -62
  210. package/src/capabilities/setup/operations/setupSkills.ts +399 -60
  211. package/src/capabilities/setup/plan.render.ts +171 -76
  212. package/src/capabilities/setup/plan.ts +17 -12
  213. package/src/capabilities/setup/setup.mcp.correctness.test.ts +9 -9
  214. package/src/capabilities/setup/setup.render.test.ts +50 -36
  215. package/src/capabilities/setup/setup.render.ts +1 -1
  216. package/src/capabilities/setup/setup.test.ts +1126 -50
  217. package/src/capabilities/setup/setup.verb.ts +174 -68
  218. package/src/capabilities/setup/targets.ts +138 -60
  219. package/src/capabilities/setup/types.ts +10 -18
  220. package/src/capabilities/shared/index.ts +18 -6
  221. package/src/capabilities/shared/packageManager.test.ts +421 -6
  222. package/src/capabilities/shared/packageManager.ts +394 -59
  223. package/src/capabilities/skill/bundledSkills.test.ts +347 -0
  224. package/src/capabilities/skill/discover.ts +100 -11
  225. package/src/capabilities/skill/render.ts +2 -2
  226. package/src/capabilities/skill/skill.test.ts +17 -4
  227. package/src/capabilities/sources/installSkills.ts +181 -3
  228. package/src/capabilities/sources/runUpdate.ts +78 -2
  229. package/src/capabilities/sources/sources.test.ts +228 -0
  230. package/src/capabilities/sources/update.verb.ts +3 -3
  231. package/src/capabilities/surface.test.ts +14 -6
  232. package/src/capabilities/upgrade/runUpgrade.ts +45 -6
  233. package/src/capabilities/upgrade/types.ts +10 -2
  234. package/src/capabilities/upgrade/upgrade.render.ts +9 -2
  235. package/src/capabilities/upgrade/upgrade.test.ts +72 -0
  236. package/src/constants.ts +14 -1
  237. package/src/identity.test.ts +18 -5
  238. package/src/kernel/completion/entitySource.test.ts +15 -2
  239. package/src/kernel/completion/safety.test.ts +16 -9
  240. package/src/kernel/config/defaults.test.ts +19 -3
  241. package/src/kernel/config/readConfig.test.ts +1 -1
  242. package/src/kernel/index.ts +11 -2
  243. package/src/kernel/interactivity.test.ts +76 -0
  244. package/src/kernel/interactivity.ts +69 -0
  245. package/src/kernel/packs/compile.test.ts +92 -0
  246. package/src/kernel/packs/renderPack.ts +3 -3
  247. package/src/kernel/packs/resolveEntity.test.ts +75 -14
  248. package/src/kernel/packs/resolveEntity.ts +88 -24
  249. package/src/kernel/packs/runBodies.ts +84 -19
  250. package/src/kernel/packs/schema.ts +60 -4
  251. package/src/kernel/packs/sparql/applyFilters.ts +125 -4
  252. package/src/kernel/packs/sparql/buildLookupQuery.test.ts +290 -0
  253. package/src/kernel/packs/sparql/buildLookupQuery.ts +327 -45
  254. package/src/kernel/packs/toolDescription.test.ts +35 -1
  255. package/src/kernel/packs/types.ts +125 -2
  256. package/src/kernel/project/cli/buildProgram.test.ts +73 -2
  257. package/src/kernel/project/cli/buildProgram.ts +8 -1
  258. package/src/kernel/project/cli/dispatch.test.ts +5 -4
  259. package/src/kernel/project/cli/dispatch.ts +48 -25
  260. package/src/kernel/project/cli/globalFlags.ts +4 -1
  261. package/src/kernel/project/cli/helpFormat.ts +1 -2
  262. package/src/kernel/project/cli/rootHelp.ts +44 -8
  263. package/src/kernel/project/mcp/registerVerb.ts +20 -1
  264. package/src/kernel/render/prefixes.test.ts +2 -0
  265. package/src/kernel/render/renderers.ts +1 -1
  266. package/src/kernel/render/style.ts +32 -9
  267. package/src/kernel/render/vocabulary.ts +276 -0
  268. package/src/kernel/runtime/graphpack/embedded/pack.generated.ts +2 -3
  269. package/src/kernel/runtime/graphpack/embedded/pack.index.generated.ts +1 -1
  270. package/src/kernel/runtime/graphpack/embedded/pack.manifest.generated.ts +3 -0
  271. package/src/kernel/runtime/graphpack/embedded.ts +14 -6
  272. package/src/kernel/runtime/graphpack/graphpack.test.ts +31 -5
  273. package/src/kernel/runtime/graphpack/read.ts +14 -12
  274. package/src/kernel/runtime/loadSession.ts +3 -1
  275. package/src/kernel/runtime/probe.ts +1 -1
  276. package/src/kernel/runtime/refs/parseRef.test.ts +49 -1
  277. package/src/kernel/runtime/refs/parseRef.ts +25 -1
  278. package/src/kernel/runtime/refs/resolve.ts +9 -3
  279. package/src/kernel/runtime/types.ts +17 -0
  280. package/src/kernel/spec/emitReference.ts +1 -1
  281. package/src/kernel/spec/types.ts +35 -6
  282. package/src/testing/PARITY_GAPS.ts +9 -3
  283. package/src/testing/behavioral/docExamples.test.ts +4 -2
  284. package/src/testing/behavioral/errorMatrix.mcp.test.ts +110 -7
  285. package/src/testing/behavioral/journeys.firstInstall.test.ts +7 -2
  286. package/src/testing/behavioral/journeys.packageStories.test.ts +6 -3
  287. package/src/testing/eval/__snapshots__/eval.test.ts.snap +26 -1
  288. package/src/testing/eval/cases/readNouns.ts +245 -9
  289. package/src/testing/fixtures/blockGraph.ts +28 -13
  290. package/src/testing/fixtures/graph/canonical.ts +95 -12
  291. package/src/testing/helpers/runCli.test.ts +44 -0
  292. package/src/testing/helpers/runCli.ts +27 -3
  293. package/dist/src/capabilities/shared/bands.js +0 -16
  294. package/dist/src/capabilities/shared/bands.js.map +0 -1
  295. package/src/capabilities/shared/bands.ts +0 -16
@@ -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.
@@ -34,6 +34,7 @@ import {
34
34
  type TaskError,
35
35
  warn,
36
36
  } from "@canonical/task";
37
+ import type { UndoOutcome } from "@canonical/task/node";
37
38
  import { BIN_NAME } from "../../../constants.js";
38
39
  import { PragmaError } from "../../../kernel/error/index.js";
39
40
  import type { PragmaRuntime } from "../../../kernel/runtime/index.js";
@@ -55,7 +56,7 @@ import {
55
56
  type TargetId,
56
57
  withRows,
57
58
  } from "../plan.js";
58
- import type { ScopeBand, ScopeSelection, SetupMode } from "../types.js";
59
+ import type { Scope, ScopeSelection, SetupMode } from "../types.js";
59
60
 
60
61
  /** The answer key the run-all's row multiselect writes. */
61
62
  const ROWS_ANSWER = "targets";
@@ -72,9 +73,26 @@ const CHILD_ANSWER: Partial<Record<TargetId, string>> = {
72
73
  lsp: LSP_EDITORS_ANSWER,
73
74
  };
74
75
 
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}`;
76
+ /** A row's identity in an answer bag: scope-qualified, so `both` stays unambiguous. */
77
+ const rowKey = (scope: Scope, target: TargetId): string => `${scope}:${target}`;
78
+
79
+ /**
80
+ * One live row-progress event: a plan row starting, finishing, or failing,
81
+ * reported WHILE the composed task is being interpreted. `label` is the row's
82
+ * progress sentence — the same `target detail — note` shape
83
+ * `renderProgressLine` prints — so the listener (the Ink wizard's step view)
84
+ * shows the run in the plan's own vocabulary, not the effect transcript's.
85
+ *
86
+ * Structurally identical to summon-core's `StepReport` ON PURPOSE: the wizard
87
+ * branch forwards these events verbatim, and this module must not import
88
+ * summon-core values (it stays behind the verb's lazy import).
89
+ */
90
+ export interface RowEvent {
91
+ /** The row's stable identity — `scope:target`. */
92
+ readonly key: string;
93
+ readonly label: string;
94
+ readonly status: "start" | "done" | "failed";
95
+ }
78
96
 
79
97
  /**
80
98
  * A ready-to-run setup invocation: the plan as detected, the synthesized
@@ -86,6 +104,29 @@ export interface SetupRun {
86
104
  readonly generator: GeneratorDefinition;
87
105
  /** The plan with the wizard's selection and the run's outcomes applied. */
88
106
  applied(answers: Record<string, unknown>): SetupPlan;
107
+ /**
108
+ * The plan graded by the EXECUTED reversals — `--undo`'s own `applied`.
109
+ *
110
+ * A real `--undo` never interprets the composed task for real: the undo
111
+ * interpreter walks it with every effect mocked (to collect the
112
+ * reversals), so the {@link OutcomeSink} the forward run fills stays
113
+ * empty, and only the interpreter's per-undo outcomes — correlated by the
114
+ * `undoKey` each reversal was stamped with in `generate` — say what
115
+ * actually happened. This projection writes those outcomes onto the SAME
116
+ * sink and reads the plan back through the SAME `outcomeFor` the forward
117
+ * run uses: one outcome model, both directions. An actionable row none of
118
+ * whose reversals reported back is a failure, never a quiet `removed`.
119
+ */
120
+ appliedUndo(outcomes: readonly UndoOutcome[]): SetupPlan;
121
+ /**
122
+ * Register THE live row listener (the Ink wizard's step adapter). The
123
+ * composed rows emit through it while they are interpreted — which means
124
+ * EVERY interpretation fires it, the confirm gate's honest preview included;
125
+ * the receiving session drops reports outside its executing phase, exactly
126
+ * as the {@link OutcomeSink} relies on last-drive-wins for its records. No
127
+ * listener registered (a `--yes` run, `--dry-run`, MCP) costs nothing.
128
+ */
129
+ setRowListener(listener: (event: RowEvent) => void): void;
89
130
  }
90
131
 
91
132
  /**
@@ -144,25 +185,50 @@ const rowPragmaError = (error: TaskError): PragmaError =>
144
185
  * throw bypasses the trampoline's recovery frames — which is why the row bodies
145
186
  * raise via `checkExecOk`/`failPragma`, never a bare throw. Interruption bypasses
146
187
  * recovery by interpreter invariant, so Ctrl-C still stops the whole run.
188
+ *
189
+ * Each row is also BRACKETED with {@link RowEvent}s — `start` as its body is
190
+ * entered, `done`/`failed` as it settles — emitted from the continuations,
191
+ * which run at interpretation time. Like the sink writes beside them, the
192
+ * emits capture nothing IN the task: the composition stays pure and
193
+ * re-interpretable, and each drive simply narrates itself as it runs.
147
194
  */
148
195
  const runRowsIsolated = (
149
196
  sink: OutcomeSink,
150
- rows: readonly { key: string; task: Task<void> }[],
197
+ rows: readonly {
198
+ key: string;
199
+ label: string;
200
+ doneLabel: string;
201
+ task: Task<void>;
202
+ }[],
203
+ emit: (event: RowEvent) => void,
151
204
  ): Task<void> =>
152
205
  flatMap(pure(undefined), (): Task<void> => {
153
206
  sink.clear();
154
207
  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);
208
+ rows.map(({ key, label, doneLabel, task }) =>
209
+ recover(
210
+ flatMap(pure(undefined), () => {
211
+ emit({ key, label, status: "start" });
212
+ return flatMap(task, () => {
213
+ emit({ key, label: doneLabel, status: "done" });
163
214
  return pure(undefined);
164
- },
165
- ),
215
+ });
216
+ }),
217
+ (error) =>
218
+ flatMap(
219
+ warn(
220
+ `The ${key.split(":")[1]} step did not complete — continuing with the remaining targets.`,
221
+ ),
222
+ () => {
223
+ sink.record(key, error);
224
+ emit({
225
+ key,
226
+ label: `${label} — ${rowPragmaError(error).message}`,
227
+ status: "failed",
228
+ });
229
+ return pure(undefined);
230
+ },
231
+ ),
166
232
  ),
167
233
  ),
168
234
  );
@@ -198,7 +264,7 @@ const buildRowsPrompt = (plan: SetupPlan): PromptDefinition => {
198
264
  .filter((row) => row.action !== "skip")
199
265
  .map((row) => ({
200
266
  label: `${row.target} — ${row.detail}${row.action === "none" ? " (already configured)" : ""}`,
201
- value: rowKey(row.band, row.target),
267
+ value: rowKey(row.scope, row.target),
202
268
  }));
203
269
  return {
204
270
  name: ROWS_ANSWER,
@@ -207,27 +273,27 @@ const buildRowsPrompt = (plan: SetupPlan): PromptDefinition => {
207
273
  choices,
208
274
  default: plan.rows
209
275
  .filter((row) => row.selected)
210
- .map((row) => rowKey(row.band, row.target)),
276
+ .map((row) => rowKey(row.scope, row.target)),
211
277
  };
212
278
  };
213
279
 
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
280
  /**
226
281
  * 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.
282
+ * every scope the run covers. An already-current file is DEFAULT-DESELECTED; a
283
+ * file that is absent or drifted stays selected. It is row-level CHILD
284
+ * selection, which is exactly what the plan's child rows are.
285
+ *
286
+ * It used to sit behind a meta-question — "Customize which files pragma
287
+ * configures?", answered no by default. That gate was read in exactly ONE place
288
+ * in the repo (this prompt's `when`) and nowhere else: not by `generate`, not by
289
+ * `applied`, not by any plan, recap, renderer or JSON projection. Answering no
290
+ * meant the child prompt was never asked, its key was absent from the answer
291
+ * bag, `readList` returned `undefined`, and the compose bodies read that as
292
+ * "all" — so everything detected was configured anyway. A meta-question in front
293
+ * of a direct question is one more thing to answer and no more control; the
294
+ * choices below already carry the detected state per child (`— unchanged`,
295
+ * `— add`, `— update`), which is the answer the meta-question was standing in
296
+ * front of.
231
297
  */
232
298
  const buildChildPrompt = (
233
299
  plan: SetupPlan,
@@ -251,7 +317,7 @@ const buildChildPrompt = (
251
317
  };
252
318
  };
253
319
 
254
- /** Every child row a target contributes, across the bands in the plan. */
320
+ /** Every child row a target contributes, across the scopes in the plan. */
255
321
  const childrenOf = (
256
322
  plan: SetupPlan,
257
323
  target: TargetId,
@@ -268,7 +334,7 @@ const isChosen = (
268
334
  row.action !== "skip" &&
269
335
  (chosen === undefined
270
336
  ? row.selected
271
- : chosen.includes(rowKey(row.band, row.target)));
337
+ : chosen.includes(rowKey(row.scope, row.target)));
272
338
 
273
339
  /**
274
340
  * What a row DID, as a past-tense word. The row's detail says what and where;
@@ -320,7 +386,7 @@ function childNote(
320
386
  *
321
387
  * @param rt - The per-invocation runtime.
322
388
  * @param mode - The entry point: the run-all, or one target.
323
- * @param scope - The resolved band selection.
389
+ * @param scope - The resolved scope selection.
324
390
  * @param removal - Compose the removal (`--undo`) instead of the install.
325
391
  * @returns The plan, the generator, and the answers-to-plan projection.
326
392
  * @note Impure — runs every selected target's real detection.
@@ -350,35 +416,51 @@ export async function buildSetupRun(
350
416
  ] as const
351
417
  ).filter(([target]) => !removal && childrenOf(plan, target).length > 1);
352
418
 
353
- if (narrowable.length > 0) {
354
- const anyChosen = (answers: Record<string, unknown>): boolean => {
419
+ for (const [target, message] of narrowable) {
420
+ // Asked whenever the row it narrows is in the run — one direct question,
421
+ // no meta-question in front of it. Removing a prompt cannot break the
422
+ // non-interactive paths: `autoPrompt`/`mcpPrompt` return each question's
423
+ // default, so a smaller prompt list means a smaller answer bag, and the
424
+ // compose bodies already read an absent child answer as "all". (The
425
+ // asymmetry is the reason this direction is safe: ADDING a defaultless
426
+ // prompt would hard-fail with MISSING_REQUIRED_ANSWER.)
427
+ const rowChosen = (answers: Record<string, unknown>): boolean => {
355
428
  const chosen = readList(answers, ROWS_ANSWER);
356
- return narrowable.some(([target]) =>
357
- plan.rows.some((row) => row.target === target && isChosen(row, chosen)),
429
+ return plan.rows.some(
430
+ (row) => row.target === target && isChosen(row, chosen),
358
431
  );
359
432
  };
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
- }
433
+ prompts.push(buildChildPrompt(plan, target, message, rowChosen));
377
434
  }
378
435
 
379
436
  /** The detection behind a row, for composing and for re-reading its draft. */
380
437
  const detectionFor = (row: PlanRow): DetectedRow | undefined =>
381
- detected.find((d) => d.target.id === row.target && d.band === row.band);
438
+ detected.find((d) => d.target.id === row.target && d.scope === row.scope);
439
+
440
+ /**
441
+ * The success note a row's completion carries — ONE derivation for the
442
+ * recap's outcome and the live row event's `done` label, so the sentence a
443
+ * watcher saw land is the sentence the recap repeats. A removal never
444
+ * borrows the forward child summary: its children are the files the entry is
445
+ * being taken OUT of, and counting them as `1 updated` described the
446
+ * opposite of what the run had just done.
447
+ */
448
+ const noteFor = (
449
+ row: PlanRow,
450
+ answers: Record<string, unknown>,
451
+ ): string | undefined => {
452
+ if (removal) return ACTION_NOTES[row.action];
453
+ const childKey = CHILD_ANSWER[row.target];
454
+ const kept =
455
+ childKey === undefined ? undefined : readList(answers, childKey);
456
+ return childNote(row, kept) ?? ACTION_NOTES[row.action];
457
+ };
458
+
459
+ /** The live row listener, when a wizard is watching. See {@link SetupRun}. */
460
+ let rowListener: ((event: RowEvent) => void) | undefined;
461
+
462
+ /** The id column width — the recap's own rule, so the columns agree. */
463
+ const idWidth = Math.max(...plan.rows.map((row) => row.target.length));
382
464
 
383
465
  const generator: GeneratorDefinition = {
384
466
  meta: buildMeta(
@@ -396,12 +478,31 @@ export async function buildSetupRun(
396
478
  // MCP file paths to every other target's compose.
397
479
  const key = CHILD_ANSWER[row.target];
398
480
  const children = key === undefined ? undefined : readList(answers, key);
481
+ // A removal stamps the row's identity on every reversal it composes:
482
+ // `--undo` executes those reversals in a LATER phase (the undo
483
+ // interpreter's), whose outcomes come back correlated by this key —
484
+ // see `appliedUndo`.
399
485
  const task = removal
400
- ? hit.target.composeRemoval(hit.detection)
486
+ ? hit.target.composeRemoval(
487
+ hit.detection,
488
+ rowKey(row.scope, row.target),
489
+ )
401
490
  : hit.target.compose(hit.detection, children);
402
- return [{ key: rowKey(row.band, row.target), task }];
491
+ // The row's progress sentence — the `target detail — note` shape
492
+ // `renderProgressLine` prints — so the live view and the recap say the
493
+ // same thing about the same row.
494
+ const label = `${row.target.padEnd(idWidth)} ${row.detail}`;
495
+ const note = noteFor(row, answers);
496
+ return [
497
+ {
498
+ key: rowKey(row.scope, row.target),
499
+ label,
500
+ doneLabel: note === undefined ? label : `${label} — ${note}`,
501
+ task,
502
+ },
503
+ ];
403
504
  });
404
- return runRowsIsolated(sink, tasks);
505
+ return runRowsIsolated(sink, tasks, (event) => rowListener?.(event));
405
506
  },
406
507
  };
407
508
 
@@ -430,9 +531,19 @@ export async function buildSetupRun(
430
531
  // outcome at all left a converged re-run with a plan-shaped output that
431
532
  // claimed nothing had run — while the row still rendered a bare green ✓.
432
533
  // Convergence is a real result: one quiet line per row, zero writes.
433
- if (row.action === "none") return { status: "noop", note: "unchanged" };
534
+ //
535
+ // On a REMOVAL the same state is `kept`, not `noop`: the run owned
536
+ // nothing here and deliberately left the machine's state standing (the
537
+ // config the user edited, the editor with no extension copy). That is
538
+ // standing aside, which the vocabulary marks inert (○), where `noop`'s
539
+ // green ✓ would claim removal work that never existed.
540
+ if (row.action === "none") {
541
+ return removal
542
+ ? { status: "kept" }
543
+ : { status: "noop", note: "unchanged" };
544
+ }
434
545
  if (!isChosen(row, chosen)) return undefined;
435
- const error = sink.get(rowKey(row.band, row.target));
546
+ const error = sink.get(rowKey(row.scope, row.target));
436
547
  if (error !== undefined) {
437
548
  const pragma = rowPragmaError(error);
438
549
  return {
@@ -444,37 +555,69 @@ export async function buildSetupRun(
444
555
  };
445
556
  }
446
557
  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]);
558
+ const note = noteFor(row, answers);
456
559
  return {
457
560
  status: removal ? "removed" : "done",
458
561
  ...(note === undefined ? {} : { note }),
459
562
  };
460
563
  };
461
564
 
565
+ /** The one answers→plan projection both `applied` faces share. */
566
+ const project = (answers: Record<string, unknown>): SetupPlan => {
567
+ const chosen = readList(answers, ROWS_ANSWER);
568
+ return withRows(
569
+ plan,
570
+ plan.rows.map((row): PlanRow => {
571
+ const outcome = outcomeFor(row, chosen, answers);
572
+ return {
573
+ ...row,
574
+ selected: isChosen(row, chosen),
575
+ ...(outcome === undefined ? {} : { outcome }),
576
+ };
577
+ }),
578
+ );
579
+ };
580
+
462
581
  return {
463
582
  plan,
464
583
  generator,
465
- applied: (answers) => {
466
- const chosen = readList(answers, ROWS_ANSWER);
467
- return withRows(
468
- plan,
469
- plan.rows.map((row): PlanRow => {
470
- const outcome = outcomeFor(row, chosen, answers);
471
- return {
472
- ...row,
473
- selected: isChosen(row, chosen),
474
- ...(outcome === undefined ? {} : { outcome }),
475
- };
476
- }),
584
+ setRowListener: (listener) => {
585
+ rowListener = listener;
586
+ },
587
+ applied: project,
588
+ appliedUndo: (outcomes) => {
589
+ // The undo path's sink fill. The forward run's recover frames write the
590
+ // sink WHILE the task is interpreted; a real `--undo` interprets the
591
+ // task only as the mocked collection walk, so the executed reversals'
592
+ // outcomes land here instead — same sink, same reader (`outcomeFor`),
593
+ // other direction. The first failure per row wins: it is the earliest
594
+ // cause, and the row's note should name it, not the knock-on.
595
+ sink.clear();
596
+ for (const outcome of outcomes) {
597
+ if (outcome.status !== "failed" || outcome.key === undefined) continue;
598
+ if (sink.get(outcome.key) !== undefined) continue;
599
+ sink.record(outcome.key, outcome.error);
600
+ }
601
+ // A row that owed reversals and heard back from NONE of them cannot
602
+ // report `removed` — nothing proved anything happened. Structurally
603
+ // every actionable removal row composes at least one keyed reversal,
604
+ // so this is a guard against a future target breaking that invariant,
605
+ // not a path any current target takes.
606
+ const reported = new Set(
607
+ outcomes.flatMap((o) => (o.key === undefined ? [] : [o.key])),
477
608
  );
609
+ for (const row of plan.rows) {
610
+ const id = rowKey(row.scope, row.target);
611
+ if (!isActionable(row.action) || !row.selected) continue;
612
+ if (reported.has(id) || sink.get(id) !== undefined) continue;
613
+ sink.record(id, {
614
+ code: "UNSUPPORTED",
615
+ message: "no reversal was executed for this target",
616
+ });
617
+ }
618
+ // No answers: `--undo` asks nothing, so every row falls back to its
619
+ // own default selection — exactly the set `generate` composed from.
620
+ return project({});
478
621
  },
479
622
  };
480
623
  }