@canonical/pragma-cli 0.32.0 → 0.34.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (295) hide show
  1. package/README.md +72 -0
  2. package/dist/pragma +0 -0
  3. package/package.json +19 -15
  4. package/pragma.conf.ts +664 -0
  5. package/src/bin.ts +15 -6
  6. package/src/capabilities/block.parity.test.ts +250 -0
  7. package/src/capabilities/capabilities/capabilities.render.ts +3 -2
  8. package/src/capabilities/capabilities/capabilities.test.ts +10 -1
  9. package/src/capabilities/capabilities/capabilities.verb.ts +32 -14
  10. package/src/capabilities/capabilities/catalog.ts +20 -8
  11. package/src/capabilities/capabilities/hints.ts +21 -36
  12. package/src/capabilities/capabilities/types.ts +2 -0
  13. package/src/capabilities/colophon/collectColophon.ts +34 -19
  14. package/src/capabilities/colophon/colophon.render.test.ts +31 -4
  15. package/src/capabilities/colophon/colophon.render.ts +13 -4
  16. package/src/capabilities/colophon/colophon.test.ts +3 -3
  17. package/src/capabilities/colophon/colophon.verb.ts +11 -4
  18. package/src/capabilities/colophon/markdownTerminal.ts +33 -14
  19. package/src/capabilities/colophon/types.ts +16 -7
  20. package/src/capabilities/config/collectConfigShow.test.ts +57 -0
  21. package/src/capabilities/config/collectConfigShow.ts +41 -4
  22. package/src/capabilities/config/fields.ts +10 -108
  23. package/src/capabilities/config/set.test.ts +6 -6
  24. package/src/capabilities/config/set.verb.ts +12 -8
  25. package/src/capabilities/config/show.render.test.ts +137 -0
  26. package/src/capabilities/config/show.render.ts +57 -16
  27. package/src/capabilities/config/show.verb.ts +9 -8
  28. package/src/capabilities/config/types.ts +8 -0
  29. package/src/capabilities/create/byteEquality.subprocess.test.ts +1 -1
  30. package/src/capabilities/create/byteEquality.test.ts +82 -101
  31. package/src/capabilities/create/compiledCreate.subprocess.test.ts +53 -16
  32. package/src/capabilities/create/constants.ts +75 -0
  33. package/src/capabilities/create/create.mcp.test.ts +2 -2
  34. package/src/capabilities/create/create.test.ts +192 -16
  35. package/src/capabilities/create/create.verb.ts +208 -79
  36. package/src/capabilities/create/index.ts +2 -10
  37. package/src/capabilities/create/pickGenerator.ts +34 -28
  38. package/src/capabilities/create/templates.embedded.generated.ts +4 -29
  39. package/src/capabilities/create/types.ts +10 -6
  40. package/src/capabilities/{standard/parity.test.ts → distribution.parity.test.ts} +24 -14
  41. package/src/capabilities/distribution.test.ts +120 -0
  42. package/src/capabilities/distribution.ts +59 -0
  43. package/src/capabilities/doctor/checks/checkConfigFile.ts +6 -5
  44. package/src/capabilities/doctor/checks/checkKeStore.ts +2 -2
  45. package/src/capabilities/doctor/checks/checkMcpCommands.ts +6 -0
  46. package/src/capabilities/doctor/checks/checkMcpConfigured.ts +15 -8
  47. package/src/capabilities/doctor/checks/checkPackageRefs.ts +79 -45
  48. package/src/capabilities/doctor/checks/checkPragmaVersion.ts +3 -2
  49. package/src/capabilities/doctor/checks/checkShellCompletions.test.ts +61 -11
  50. package/src/capabilities/doctor/checks/checkShellCompletions.ts +63 -23
  51. package/src/capabilities/doctor/checks/checkSkillsSymlinked.ts +3 -2
  52. package/src/capabilities/doctor/checks/deriveBand.test.ts +42 -0
  53. package/src/capabilities/doctor/checks/deriveBand.ts +28 -0
  54. package/src/capabilities/doctor/doctor.render.test.ts +170 -0
  55. package/src/capabilities/doctor/doctor.render.ts +117 -34
  56. package/src/capabilities/doctor/doctor.test.ts +68 -9
  57. package/src/capabilities/doctor/doctor.verb.ts +6 -2
  58. package/src/capabilities/doctor/runChecks.ts +33 -9
  59. package/src/capabilities/doctor/types.ts +16 -1
  60. package/src/capabilities/emptyRecovery.test.ts +32 -15
  61. package/src/capabilities/graph/inspect.verb.ts +3 -2
  62. package/src/capabilities/graph/query.verb.ts +3 -2
  63. package/src/capabilities/graph/runQuery.ts +8 -5
  64. package/src/capabilities/index.ts +22 -18
  65. package/src/capabilities/info/collectInfo.ts +10 -6
  66. package/src/capabilities/info/info.render.ts +3 -2
  67. package/src/capabilities/info/info.verb.ts +3 -2
  68. package/src/capabilities/lazy.test.ts +93 -2
  69. package/src/capabilities/ontology/index.ts +10 -3
  70. package/src/capabilities/ontology/ontology.test.ts +157 -5
  71. package/src/capabilities/ontology/verbs.ts +203 -104
  72. package/src/capabilities/prompt/prompt.render.test.ts +94 -0
  73. package/src/capabilities/prompt/prompt.render.ts +23 -6
  74. package/src/capabilities/prompt/prompt.test.ts +68 -1
  75. package/src/capabilities/prompt/verbs.ts +24 -11
  76. package/src/capabilities/reference.test.ts +32 -0
  77. package/src/capabilities/resources/provider.ts +58 -25
  78. package/src/capabilities/resources/resources.test.ts +176 -21
  79. package/src/capabilities/scopeTypeSync.test.ts +37 -0
  80. package/src/capabilities/setup/index.ts +7 -1
  81. package/src/capabilities/setup/operations/setupCompletions.ts +49 -16
  82. package/src/capabilities/setup/operations/setupGenerator.ts +166 -63
  83. package/src/capabilities/setup/operations/setupLsp.ts +56 -12
  84. package/src/capabilities/setup/operations/setupMcp.ts +216 -39
  85. package/src/capabilities/setup/operations/setupSkills.ts +10 -4
  86. package/src/capabilities/setup/setup.render.test.ts +109 -0
  87. package/src/capabilities/setup/setup.render.ts +61 -14
  88. package/src/capabilities/setup/setup.test.ts +352 -1
  89. package/src/capabilities/setup/setup.verb.ts +68 -9
  90. package/src/capabilities/setup/shell.ts +29 -3
  91. package/src/capabilities/setup/types.ts +59 -2
  92. package/src/capabilities/shared/bands.ts +16 -0
  93. package/src/capabilities/skill/discover.ts +11 -5
  94. package/src/capabilities/skill/render.ts +4 -5
  95. package/src/capabilities/skill/verbs.ts +7 -7
  96. package/src/capabilities/sources/collectStatus.ts +69 -49
  97. package/src/capabilities/sources/index.ts +1 -1
  98. package/src/capabilities/sources/installSkills.ts +2 -2
  99. package/src/capabilities/sources/runUpdate.ts +146 -98
  100. package/src/capabilities/sources/sources.test.ts +264 -84
  101. package/src/capabilities/sources/status.render.test.ts +94 -0
  102. package/src/capabilities/sources/status.render.ts +55 -33
  103. package/src/capabilities/sources/status.verb.ts +15 -8
  104. package/src/capabilities/sources/types.ts +18 -25
  105. package/src/capabilities/sources/update.render.ts +9 -2
  106. package/src/capabilities/sources/update.verb.ts +12 -23
  107. package/src/capabilities/surface.test.ts +65 -17
  108. package/src/capabilities/tier.parity.test.ts +144 -0
  109. package/src/capabilities/upgrade/upgrade.verb.ts +7 -3
  110. package/src/constants.test.ts +9 -7
  111. package/src/constants.ts +49 -23
  112. package/src/identity.test.ts +333 -0
  113. package/src/index.ts +2 -2
  114. package/src/kernel/completion/__snapshots__/emitScripts.test.ts.snap +18 -16
  115. package/src/kernel/completion/complete.test.ts +3 -5
  116. package/src/kernel/completion/emitScripts.test.ts +1 -1
  117. package/src/kernel/completion/emitScripts.ts +4 -2
  118. package/src/kernel/completion/entitySource.test.ts +185 -41
  119. package/src/kernel/completion/entitySource.ts +131 -113
  120. package/src/kernel/completion/model.test.ts +3 -3
  121. package/src/kernel/completion/model.ts +11 -1
  122. package/src/kernel/completion/parse.test.ts +2 -4
  123. package/src/kernel/completion/safety.test.ts +82 -36
  124. package/src/kernel/completion/shellDrive.test.ts +592 -0
  125. package/src/kernel/completion/templates/fish.ts +28 -6
  126. package/src/kernel/completion/types.ts +3 -3
  127. package/src/kernel/config/__snapshots__/firstRun.test.ts.snap +1 -1
  128. package/src/kernel/config/defaults.test.ts +85 -0
  129. package/src/kernel/config/defaults.ts +12 -23
  130. package/src/kernel/config/evaluateProjectConfig.ts +2 -3
  131. package/src/kernel/config/findProjectConfig.ts +4 -3
  132. package/src/kernel/config/firstRun.ts +12 -5
  133. package/src/kernel/config/globalConfig.ts +2 -2
  134. package/src/kernel/config/index.ts +4 -5
  135. package/src/kernel/config/paths.ts +17 -17
  136. package/src/kernel/config/readConfig.test.ts +242 -6
  137. package/src/kernel/config/readConfig.ts +8 -8
  138. package/src/kernel/config/schema.test.ts +50 -0
  139. package/src/kernel/config/schema.ts +97 -10
  140. package/src/kernel/config/types.ts +73 -19
  141. package/src/kernel/copy.test.ts +453 -0
  142. package/src/kernel/error/PragmaError.test.ts +5 -5
  143. package/src/kernel/error/constants.ts +3 -2
  144. package/src/kernel/error/index.ts +1 -1
  145. package/src/kernel/error/recovery.test.ts +22 -45
  146. package/src/kernel/error/recovery.ts +22 -27
  147. package/src/kernel/error/renderError.ts +1 -1
  148. package/src/kernel/error/types.ts +5 -4
  149. package/src/kernel/packs/collect.test.ts +195 -11
  150. package/src/kernel/packs/collect.ts +213 -58
  151. package/src/kernel/packs/compile.test.ts +163 -7
  152. package/src/kernel/packs/compile.ts +15 -12
  153. package/src/kernel/packs/disclosure.test.ts +5 -2
  154. package/src/kernel/packs/graphql/fetchGraphqlLookup.ts +11 -10
  155. package/src/kernel/packs/graphqlSource.test.ts +6 -3
  156. package/src/kernel/packs/iri.ts +5 -7
  157. package/src/kernel/packs/renderPack.ts +5 -3
  158. package/src/kernel/packs/resolveEntity.ts +21 -17
  159. package/src/kernel/packs/runBodies.ts +15 -14
  160. package/src/kernel/packs/schema.ts +96 -10
  161. package/src/kernel/packs/sparql/runSelect.test.ts +104 -10
  162. package/src/kernel/packs/sparql/runSelect.ts +73 -34
  163. package/src/kernel/packs/toolDescription.test.ts +10 -2
  164. package/src/kernel/packs/types.ts +39 -4
  165. package/src/kernel/packs/uniqueness.ts +4 -4
  166. package/src/kernel/project/cli/buildProgram.test.ts +60 -11
  167. package/src/kernel/project/cli/buildProgram.ts +11 -0
  168. package/src/kernel/project/cli/dispatch.test.ts +67 -0
  169. package/src/kernel/project/cli/dispatch.ts +34 -12
  170. package/src/kernel/project/cli/globalFlags.test.ts +12 -4
  171. package/src/kernel/project/cli/globalFlags.ts +14 -14
  172. package/src/kernel/project/cli/helpFormat.ts +1 -1
  173. package/src/kernel/project/cli/rootHelp.ts +109 -107
  174. package/src/kernel/project/mcp/buildServer.ts +11 -6
  175. package/src/kernel/project/mcp/instructions.ts +12 -7
  176. package/src/kernel/project/mcp/mcpError.ts +52 -0
  177. package/src/kernel/project/mcp/parity.test.ts +39 -1
  178. package/src/kernel/project/mcp/prompts/prompts.test.ts +128 -5
  179. package/src/kernel/project/mcp/prompts/provider.ts +51 -7
  180. package/src/kernel/project/mcp/prompts/source.ts +68 -67
  181. package/src/kernel/project/mcp/registerVerb.ts +25 -6
  182. package/src/kernel/project/mcp/serve.ts +11 -3
  183. package/src/kernel/project/mcp/toolDescriptions.test.ts +6 -2
  184. package/src/kernel/render/disclosure.ts +4 -2
  185. package/src/kernel/render/formatters.ts +5 -4
  186. package/src/kernel/render/prefixes.test.ts +36 -0
  187. package/src/kernel/render/prefixes.ts +40 -13
  188. package/src/kernel/render/renderers.test.ts +35 -1
  189. package/src/kernel/render/renderers.ts +25 -7
  190. package/src/kernel/render/style.test.ts +31 -0
  191. package/src/kernel/render/style.ts +74 -0
  192. package/src/kernel/runtime/graphpack/build.ts +57 -3
  193. package/src/kernel/runtime/graphpack/buildIndex.test.ts +149 -0
  194. package/src/kernel/runtime/graphpack/buildIndex.ts +173 -50
  195. package/src/kernel/runtime/graphpack/embedded/pack.generated.ts +5 -8
  196. package/src/kernel/runtime/graphpack/embedded/pack.index.generated.ts +3 -4
  197. package/src/kernel/runtime/graphpack/embedded/pack.stories.generated.ts +3 -0
  198. package/src/kernel/runtime/graphpack/embedded.ts +37 -11
  199. package/src/kernel/runtime/graphpack/graphpack.test.ts +274 -6
  200. package/src/kernel/runtime/graphpack/hash.ts +3 -2
  201. package/src/kernel/runtime/graphpack/index.ts +1 -1
  202. package/src/kernel/runtime/graphpack/manifest.ts +114 -12
  203. package/src/kernel/runtime/graphpack/read.ts +32 -22
  204. package/src/kernel/runtime/graphpack/schemas.ts +74 -0
  205. package/src/kernel/runtime/graphpack/stories.ts +84 -0
  206. package/src/kernel/runtime/graphpack/types.ts +72 -45
  207. package/src/kernel/runtime/loadSession.ts +4 -12
  208. package/src/kernel/runtime/paths.ts +64 -15
  209. package/src/kernel/runtime/probe.test.ts +8 -4
  210. package/src/kernel/runtime/readEntity.ts +6 -5
  211. package/src/kernel/runtime/refs/gitOps.ts +1 -19
  212. package/src/kernel/runtime/refs/parseRef.ts +7 -7
  213. package/src/kernel/runtime/refs/resolve.ts +141 -65
  214. package/src/kernel/runtime/resolveSources.test.ts +63 -24
  215. package/src/kernel/runtime/resolveSources.ts +36 -25
  216. package/src/kernel/runtime/storeReadiness.ts +62 -0
  217. package/src/kernel/runtime/storeless.test.ts +1 -1
  218. package/src/kernel/runtime/types.ts +1 -1
  219. package/src/kernel/runtime/wasmEmbed.test.ts +25 -20
  220. package/src/kernel/spec/emitReference.test.ts +196 -0
  221. package/src/kernel/spec/emitReference.ts +624 -0
  222. package/src/kernel/spec/emitSurface.ts +13 -7
  223. package/src/kernel/spec/index.ts +2 -0
  224. package/src/kernel/spec/surfaceConformance.test.ts +2 -2
  225. package/src/kernel/spec/surfaceConformance.ts +8 -0
  226. package/src/kernel/spec/types.ts +29 -9
  227. package/src/kernel/spec/validate.ts +2 -1
  228. package/src/kernel/vocabulary.test.ts +164 -0
  229. package/src/kernel/vocabulary.ts +146 -0
  230. package/src/testing/PARITY_GAPS.ts +9 -3
  231. package/src/testing/behavioral/README.md +3 -3
  232. package/src/testing/behavioral/autoLlm.e2e.test.ts +12 -2
  233. package/src/testing/behavioral/completion.test.ts +21 -8
  234. package/src/testing/behavioral/disclosure.test.ts +12 -0
  235. package/src/testing/behavioral/docExamples.test.ts +283 -0
  236. package/src/testing/behavioral/errorMatrix.mcp.test.ts +4 -3
  237. package/src/testing/behavioral/errorMatrix.storeless.test.ts +3 -2
  238. package/src/testing/behavioral/exitCodes.e2e.test.ts +6 -5
  239. package/src/testing/behavioral/harness.foundation.test.ts +14 -5
  240. package/src/testing/behavioral/journeys.cli.test.ts +116 -73
  241. package/src/testing/behavioral/journeys.defaultPack.test.ts +477 -0
  242. package/src/testing/behavioral/journeys.firstInstall.test.ts +167 -0
  243. package/src/testing/behavioral/journeys.interactiveTty.e2e.test.ts +66 -0
  244. package/src/testing/behavioral/journeys.livePack.e2e.test.ts +129 -0
  245. package/src/testing/behavioral/journeys.mcpLifecycle.mcp.test.ts +179 -0
  246. package/src/testing/behavioral/journeys.packageStories.test.ts +188 -0
  247. package/src/testing/behavioral/liveReadSurface.ts +4 -3
  248. package/src/testing/behavioral/mcpSurface.test.ts +10 -5
  249. package/src/testing/behavioral/staleCommands.test.ts +86 -0
  250. package/src/testing/eval/__snapshots__/eval.test.ts.snap +9 -9
  251. package/src/testing/eval/cases/assembly.ts +16 -9
  252. package/src/testing/eval/cases/effectful.ts +31 -16
  253. package/src/testing/eval/cases/readNouns.ts +23 -18
  254. package/src/testing/eval/cases/stable.ts +5 -1
  255. package/src/testing/eval/eval.test.ts +7 -1
  256. package/src/testing/fixtures/fixtureCapability.ts +201 -1
  257. package/src/testing/fixtures/graph/canonical.ts +6 -3
  258. package/src/testing/fixtures/graph/defaultPack.ts +179 -0
  259. package/src/testing/fixtures/graph/owlExport.ts +97 -0
  260. package/src/testing/helpers/extractSnippets.ts +29 -0
  261. package/src/testing/helpers/fixtureGraph.ts +33 -16
  262. package/src/testing/helpers/golden.ts +1 -1
  263. package/src/testing/helpers/packRuntime.ts +2 -3
  264. package/src/testing/helpers/projectCli.ts +2 -1
  265. package/src/testing/helpers/projectMcp.ts +6 -0
  266. package/src/testing/perf/budgets.test.ts +4 -0
  267. package/src/testing/perf/budgets.ts +22 -7
  268. package/src/testing/perf/globalSetup.ts +121 -6
  269. package/src/types.ts +2 -2
  270. package/src/capabilities/block/blockList.verb.ts +0 -150
  271. package/src/capabilities/block/index.ts +0 -25
  272. package/src/capabilities/block/pack.ts +0 -155
  273. package/src/capabilities/block/parity.test.ts +0 -180
  274. package/src/capabilities/block/tierChain.ts +0 -78
  275. package/src/capabilities/colophon/pragmaColophon.ts +0 -66
  276. package/src/capabilities/config/field.test.ts +0 -188
  277. package/src/capabilities/modifier/index.ts +0 -15
  278. package/src/capabilities/modifier/pack.ts +0 -64
  279. package/src/capabilities/standard/index.ts +0 -18
  280. package/src/capabilities/standard/pack.ts +0 -128
  281. package/src/capabilities/tier/index.ts +0 -19
  282. package/src/capabilities/tier/lookup.render.ts +0 -34
  283. package/src/capabilities/tier/lookup.test.ts +0 -68
  284. package/src/capabilities/tier/lookup.verb.ts +0 -44
  285. package/src/capabilities/tier/pack.ts +0 -28
  286. package/src/capabilities/tier/runLookup.ts +0 -59
  287. package/src/capabilities/token/addConfig.render.ts +0 -27
  288. package/src/capabilities/token/addConfig.test.ts +0 -62
  289. package/src/capabilities/token/addConfig.verb.ts +0 -40
  290. package/src/capabilities/token/index.ts +0 -19
  291. package/src/capabilities/token/pack.ts +0 -58
  292. package/src/capabilities/token/runAddConfig.ts +0 -95
  293. package/src/kernel/completion/bashDrive.test.ts +0 -73
  294. package/src/kernel/runtime/graphpack/embedded/sample.ttl +0 -39
  295. package/src/kernel/runtime/lock.ts +0 -90
@@ -1,11 +1,14 @@
1
1
  /**
2
2
  * Collect the `colophon` payload — the verb's run body (lazily imported).
3
3
  *
4
- * STORELESS: it reads the layered config (memoized) and resolves the EFFECTIVE
5
- * capability modules exactly as a real command would, so a config story that
6
- * overrides a bundled pack is reflected here — "active domain" is expressed the
7
- * way the rest of the CLI expresses it. It never boots the triple store, so the
8
- * storeless-guarantee spy still sees `store.booted === false`.
4
+ * STORELESS: it resolves the EFFECTIVE capability modules exactly as a real
5
+ * command would — through `loadEffectiveModules`, so a story declared by a
6
+ * PACKAGE the active pack carries surfaces its colophon here too, not only a
7
+ * config-declared one. It never boots the triple store, so the
8
+ * storeless-guarantee spy still sees `store.booted === false`. The static
9
+ * import of the validated distribution config (`config/defaults.js`) is safe
10
+ * here because this module sits behind the verb's dynamic `import()` — it
11
+ * never lands on the `--help`/`__complete` fast path.
9
12
  *
10
13
  * The registry is reached through a RUNTIME dynamic `import("../index.js")`
11
14
  * inside this async body — never a static import — so the static
@@ -13,44 +16,56 @@
13
16
  * into a static import.
14
17
  */
15
18
 
19
+ import { BIN_NAME } from "../../constants.js";
20
+ import distribution from "../../kernel/config/defaults.js";
16
21
  import type { PragmaRuntime } from "../../kernel/runtime/types.js";
17
22
  import type { CapabilityModule } from "../../kernel/spec/types.js";
18
- import { PRAGMA_COLOPHON, PRAGMA_COLOPHON_SUMMARY } from "./pragmaColophon.js";
19
23
  import type { ColophonData, ColophonSection } from "./types.js";
20
24
 
21
25
  /**
22
26
  * Assemble the colophon data for the current runtime.
23
27
  *
28
+ * The toolchain section is DECLARED, not authored here: the distribution
29
+ * config's `colophon` field carries the narrative (and its condensed summary),
30
+ * and this collector renders whatever it declares under the distribution's own
31
+ * name — a fork edits its config, never this module. A distribution declaring
32
+ * no `colophon` gets no toolchain section: the packs' domain colophons stand
33
+ * alone. The `kind: "pragma"` value is a frozen JSON discriminant (wire
34
+ * compatibility), not the distribution's name.
35
+ *
24
36
  * @param runtime - The per-invocation runtime.
25
- * @returns The storeless colophon payload: pragma's section, then each active
26
- * pack/domain that declares a `colophon`.
37
+ * @returns The storeless colophon payload: the distribution's declared section,
38
+ * then each active pack/domain that declares a `colophon`.
27
39
  * @note Impure — reads the config layers (never boots the store).
28
40
  */
29
41
  export async function collectColophon(
30
42
  runtime: PragmaRuntime,
31
43
  ): Promise<ColophonData> {
32
- const sections: ColophonSection[] = [
33
- {
44
+ const sections: ColophonSection[] = [];
45
+ if (distribution.colophon) {
46
+ sections.push({
34
47
  kind: "pragma",
35
- title: "pragma",
36
- markdown: PRAGMA_COLOPHON,
37
- summary: PRAGMA_COLOPHON_SUMMARY,
48
+ title: BIN_NAME,
49
+ markdown: distribution.colophon.markdown,
50
+ ...(distribution.colophon.summary === undefined
51
+ ? {}
52
+ : { summary: distribution.colophon.summary }),
38
53
  source: "built-in",
39
- },
40
- ];
54
+ });
55
+ }
41
56
 
42
- const { assembleEffectiveModules } = await import(
57
+ const { loadEffectiveModules } = await import(
43
58
  "../../kernel/packs/collect.js"
44
59
  );
45
60
  const { capabilities } = await import("../index.js");
46
61
 
47
62
  let modules: readonly CapabilityModule[] = capabilities;
48
63
  try {
49
- const layers = await runtime.loadConfig();
50
- modules = assembleEffectiveModules(capabilities, layers);
64
+ modules = (await loadEffectiveModules(capabilities, runtime.cwd)).modules;
51
65
  } catch {
52
66
  // A bad config story is surfaced by the real commands; the colophon degrades
53
- // to the static (bundled) modules rather than failing the narrative.
67
+ // to the static modules rather than failing the narrative. (A bad PACKAGE
68
+ // story never throws — it is dropped and reported by doctor.)
54
69
  }
55
70
 
56
71
  for (const module of modules) {
@@ -78,10 +78,37 @@ describe("colophon render golden — color-free (chalk.level = 0)", () => {
78
78
  });
79
79
  });
80
80
 
81
- describe("colophon render — color ON", () => {
82
- it("plain emits ANSI escapes when chalk color is enabled", () => {
81
+ /** Run `body` with stdout's `isTTY` forced to `value`, then restore it. */
82
+ function withStdoutTty(value: boolean | undefined, body: () => void): void {
83
+ const stream = process.stdout as { isTTY?: boolean };
84
+ const saved = stream.isTTY;
85
+ stream.isTTY = value;
86
+ try {
87
+ body();
88
+ } finally {
89
+ stream.isTTY = saved;
90
+ }
91
+ }
92
+
93
+ describe("colophon render — color ON (attended TTY)", () => {
94
+ it("plain emits ANSI escapes on a color-capable TTY", () => {
83
95
  chalk.level = 1;
84
- // biome-ignore lint/suspicious/noControlCharactersInRegex: asserting the literal ESC byte is the point
85
- expect(colophonFormatters.plain(FIXTURE)).toMatch(/\x1b\[/);
96
+ withStdoutTty(true, () => {
97
+ // biome-ignore lint/suspicious/noControlCharactersInRegex: asserting the literal ESC byte is the point
98
+ expect(colophonFormatters.plain(FIXTURE)).toMatch(/\x1b\[/);
99
+ });
100
+ });
101
+ });
102
+
103
+ describe("colophon render — piped output is ANSI-free (F1)", () => {
104
+ it("plain emits ZERO ANSI off a TTY even when chalk reports color (CI/FORCE_COLOR)", () => {
105
+ // The CI leak: `supports-color` sets a non-zero level off a TTY under
106
+ // GITHUB_ACTIONS / FORCE_COLOR, so gating on `chalk.level` alone bled ANSI
107
+ // into `colophon --format plain | tee`. The isTTY gate closes it.
108
+ chalk.level = 3;
109
+ withStdoutTty(undefined, () => {
110
+ // biome-ignore lint/suspicious/noControlCharactersInRegex: asserting NO ESC byte survives
111
+ expect(colophonFormatters.plain(FIXTURE)).not.toMatch(/\x1b\[/);
112
+ });
86
113
  });
87
114
  });
@@ -9,23 +9,32 @@
9
9
  * (which carries no leading H1) is never double-titled.
10
10
  */
11
11
 
12
+ import { defaultStyle, type RenderStyle } from "../../kernel/render/style.js";
12
13
  import type { Formatters } from "../../kernel/spec/types.js";
13
14
  import { renderMarkdownToTerminal } from "./markdownTerminal.js";
14
15
  import type { ColophonData, ColophonSection } from "./types.js";
15
16
 
16
17
  /** Render one section (title as H1 + body) as styled terminal Markdown. */
17
- function plainSection(section: ColophonSection): string {
18
- return renderMarkdownToTerminal(`# ${section.title}\n\n${section.markdown}`);
18
+ function plainSection(section: ColophonSection, style: RenderStyle): string {
19
+ return renderMarkdownToTerminal(
20
+ `# ${section.title}\n\n${section.markdown}`,
21
+ style,
22
+ );
19
23
  }
20
24
 
21
- /** Render one section as condensed Markdown for `--llm` (summary preferred). */
25
+ /** Render one section as condensed Markdown for `--format llm` (summary preferred). */
22
26
  function llmSection(section: ColophonSection): string {
23
27
  return `## ${section.title}\n\n${section.summary ?? section.markdown}`;
24
28
  }
25
29
 
26
30
  export const colophonFormatters: Formatters<ColophonData> = {
27
31
  plain(data) {
28
- return data.sections.map(plainSection).join("\n\n");
32
+ // Resolve the TTY decision ONCE (impure: reads stdout.isTTY + chalk level),
33
+ // then render every section through the same inert-off-a-TTY styler.
34
+ const style = defaultStyle();
35
+ return data.sections
36
+ .map((section) => plainSection(section, style))
37
+ .join("\n\n");
29
38
  },
30
39
 
31
40
  llm(data) {
@@ -4,7 +4,7 @@
4
4
  * Pins the covenant-exact emitted slice (`{ v:"colophon", mcp:"colophon" }`),
5
5
  * proves the collector is storeless and combines pragma's built-in section with
6
6
  * the active pack's (the bundled `block` design-system colophon), exercises the
7
- * three formatter modes + `--format`/`--llm` precedence, holds CLI-json ≡ MCP
7
+ * three formatter modes + `--format` precedence, holds CLI-json ≡ MCP
8
8
  * parity, and checks the pack-grammar accepts a `colophon` field (rejecting a
9
9
  * non-string).
10
10
  */
@@ -47,7 +47,7 @@ const FLAGS: GlobalFlags = {
47
47
  verbose: false,
48
48
  };
49
49
  const FLAGS_JSON: GlobalFlags = { ...FLAGS, format: "json" };
50
- const FLAGS_LLM: GlobalFlags = { ...FLAGS, llm: true };
50
+ const FLAGS_LLM: GlobalFlags = { ...FLAGS, format: "llm", llm: true };
51
51
  const NO_MUT = { dryRun: false, undo: false, yes: false };
52
52
 
53
53
  /** A deterministic two-section fixture for pure formatter assertions. */
@@ -170,7 +170,7 @@ describe("colophon — formatter modes", () => {
170
170
  expect((envelope.data as ColophonData).sections[0]?.kind).toBe("pragma");
171
171
  });
172
172
 
173
- it("--llm selects the condensed Markdown form", async () => {
173
+ it("--format llm selects the condensed Markdown form", async () => {
174
174
  const outcome = await executeVerb(
175
175
  colophonVerb,
176
176
  {},
@@ -7,6 +7,7 @@
7
7
  * `infoVerb` / `doctorVerb` / `capabilitiesVerb`.
8
8
  */
9
9
 
10
+ import { BIN_NAME } from "../../constants.js";
10
11
  import { asVerb } from "../../kernel/spec/asVerb.js";
11
12
  import type { CapabilityModule, VerbSpec } from "../../kernel/spec/types.js";
12
13
  import { colophonFormatters } from "./colophon.render.js";
@@ -14,13 +15,19 @@ import type { ColophonData } from "./types.js";
14
15
 
15
16
  const colophonVerb: VerbSpec<Record<string, unknown>, ColophonData> = {
16
17
  path: ["colophon"],
17
- summary: "Narrate how pragma and the active domain are made.",
18
- doc: "Storeless — a colophon for the toolchain. Prints pragma's own story (the effect monad, one-grammar-many-projections, the render/LLM-output model, storeless modularity, and the domain-as-data pack model) followed by the active pack's domain colophon. Also available as a condensed Markdown narration for agents, or as a structured JSON projection of the sections.",
18
+ summary: `Narrate how ${BIN_NAME} and the active domain are made.`,
19
+ doc: `Storeless — a colophon for the toolchain. Prints the story the distribution declares for itself (its config's \`colophon\`) followed by the active pack's domain colophon. Also available as a condensed Markdown narration for agents, or as a structured JSON projection of the sections.`,
19
20
  params: [],
20
21
  output: { formatters: colophonFormatters },
21
22
  examples: [
22
- { cmd: "pragma colophon", note: "the toolchain + active domain story" },
23
- { cmd: "pragma colophon --llm", note: "condensed Markdown for agents" },
23
+ {
24
+ cmd: `${BIN_NAME} colophon`,
25
+ note: "the toolchain + active domain story",
26
+ },
27
+ {
28
+ cmd: `${BIN_NAME} colophon --format llm`,
29
+ note: "condensed Markdown for agents",
30
+ },
24
31
  ],
25
32
  capability: {
26
33
  needsStore: false,
@@ -7,11 +7,14 @@
7
7
  *
8
8
  * Single consumer today (`pragma colophon`); a promotion candidate to
9
9
  * `kernel/render/` if another verb ever wants the same helper. Determinism note:
10
- * styling routes entirely through `chalk`, so a test forcing `chalk.level = 0`
11
- * gets a stable, color-free snapshot of the structural transforms.
10
+ * styling routes through the shared {@link RenderStyle} seam (color only on a
11
+ * color-capable TTY), so piped / redirected / CI output — where `supports-color`
12
+ * can report a level with no TTY — stays byte-for-byte the color-free structural
13
+ * transform, and a test can force either mode by passing an explicit style.
12
14
  */
13
15
 
14
16
  import chalk from "chalk";
17
+ import { defaultStyle, type RenderStyle } from "../../kernel/render/style.js";
15
18
 
16
19
  /** Full ANSI escape sequences (`ESC [ … final-letter`) — colour/cursor codes. */
17
20
  // biome-ignore lint/suspicious/noControlCharactersInRegex: deliberate control-char scrub
@@ -33,25 +36,35 @@ function stripControl(line: string): string {
33
36
  return line.replace(ANSI_ESCAPE, "").replace(C0_CONTROLS, "");
34
37
  }
35
38
 
36
- /** Style inline `**bold**` and `` `code` `` spans within one line. */
37
- function styleInline(text: string): string {
39
+ /**
40
+ * Style inline `**bold**` and `` `code` `` spans within one line. The `**` / `` ` ``
41
+ * markers are ALWAYS consumed; off a TTY the styler is inert, so the markers drop
42
+ * with no color (byte-identical to the pre-seam `chalk.level = 0` behavior).
43
+ */
44
+ function styleInline(text: string, style: RenderStyle): string {
38
45
  return text
39
- .replace(/\*\*([^*]+)\*\*/g, (_match, inner: string) => chalk.bold(inner))
40
- .replace(/`([^`]+)`/g, (_match, inner: string) => chalk.cyan(inner));
46
+ .replace(/\*\*([^*]+)\*\*/g, (_match, inner: string) => style.bold(inner))
47
+ .replace(/`([^`]+)`/g, (_match, inner: string) => style.cyan(inner));
41
48
  }
42
49
 
43
50
  /**
44
51
  * Render a Markdown string to styled terminal text.
45
52
  *
46
53
  * @param markdown - The authored Markdown body.
47
- * @returns The chalk-styled terminal string (no trailing newline).
54
+ * @param style - The TTY styler; defaults to the process style. On a color-capable
55
+ * terminal headings/bullets/inline spans are tinted; off a TTY the styler is
56
+ * inert, so the output is byte-identical to the color-free structural form.
57
+ * @returns The styled terminal string (no trailing newline).
48
58
  */
49
- export function renderMarkdownToTerminal(markdown: string): string {
59
+ export function renderMarkdownToTerminal(
60
+ markdown: string,
61
+ style: RenderStyle = defaultStyle(),
62
+ ): string {
50
63
  const lines: string[] = [];
51
64
  let inFence = false;
52
65
 
53
66
  for (const rawLine of markdown.split("\n")) {
54
- // Scrub pack-authored terminal control codes BEFORE our own chalk styling
67
+ // Scrub pack-authored terminal control codes BEFORE our own styling
55
68
  // (colophon bodies are domain-as-data — never a channel for raw ANSI).
56
69
  const raw = stripControl(rawLine);
57
70
  // A fence toggles code mode; the ``` markers themselves are dropped.
@@ -60,25 +73,31 @@ export function renderMarkdownToTerminal(markdown: string): string {
60
73
  continue;
61
74
  }
62
75
  if (inFence) {
63
- lines.push(chalk.dim(` ${raw}`));
76
+ lines.push(style.dim(` ${raw}`));
64
77
  continue;
65
78
  }
66
79
 
67
80
  const heading = /^(#{1,6})\s+(.+)$/.exec(raw);
68
81
  if (heading) {
69
82
  const level = heading[1]?.length ?? 1;
70
- const text = styleInline(heading[2] ?? "");
71
- lines.push(level === 1 ? chalk.bold.underline(text) : chalk.bold(text));
83
+ const text = styleInline(heading[2] ?? "", style);
84
+ // H1 earns bold+underline on a TTY; underline is outside the RenderStyle
85
+ // seam, so reach for chalk only when the styler is enabled.
86
+ lines.push(
87
+ level === 1 && style.enabled
88
+ ? chalk.bold.underline(text)
89
+ : style.bold(text),
90
+ );
72
91
  continue;
73
92
  }
74
93
 
75
94
  const bullet = /^\s*[-*]\s+(.+)$/.exec(raw);
76
95
  if (bullet) {
77
- lines.push(` ${chalk.cyan("•")} ${styleInline(bullet[1] ?? "")}`);
96
+ lines.push(` ${style.cyan("•")} ${styleInline(bullet[1] ?? "", style)}`);
78
97
  continue;
79
98
  }
80
99
 
81
- lines.push(styleInline(raw));
100
+ lines.push(styleInline(raw, style));
82
101
  }
83
102
 
84
103
  return lines.join("\n");
@@ -2,28 +2,37 @@
2
2
  * Data shapes for `pragma colophon`.
3
3
  *
4
4
  * A colophon (the printer's end-note on how a book is made) for the toolchain:
5
- * one section narrating how pragma itself is built, followed by one section per
6
- * active pack/domain that declares a `colophon`. Storeless and read-only.
5
+ * one section narrating how the distribution itself is built — declared in its
6
+ * config, not authored in code — followed by one section per active pack/domain
7
+ * that declares a `colophon`. Storeless and read-only.
7
8
  */
8
9
 
9
- /** One colophon section — pragma's own, or an active pack's domain story. */
10
+ /** One colophon section — the distribution's own, or an active pack's story. */
10
11
  export interface ColophonSection {
11
- /** Whether this is pragma's built-in section or an active pack's. */
12
+ /**
13
+ * Whether this is the distribution's declared section or an active pack's.
14
+ * `"pragma"` is a FROZEN JSON-visible discriminant (wire compatibility, like
15
+ * the `pragma:` resource scheme) — it does not track the distribution's
16
+ * name; a fork's own section still carries `kind: "pragma"`.
17
+ */
12
18
  readonly kind: "pragma" | "pack";
13
- /** Section heading — `"pragma"`, or the pack's noun/name. */
19
+ /** Section heading — the distribution's name, or the pack's noun/name. */
14
20
  readonly title: string;
15
21
  /**
16
22
  * The authored Markdown BODY (no leading H1 — the renderer supplies the
17
23
  * heading from {@link title}, so a section is never double-titled).
18
24
  */
19
25
  readonly markdown: string;
20
- /** Optional condensed body for `--llm` (pragma supplies one). */
26
+ /** Optional condensed body for `--format llm` (the declaration's `summary`). */
21
27
  readonly summary?: string;
22
28
  /** Provenance: `"built-in"`, or `"pack:<name>"`. */
23
29
  readonly source?: string;
24
30
  }
25
31
 
26
- /** The full `colophon` payload: pragma's section first, then active packs'. */
32
+ /**
33
+ * The full `colophon` payload: the distribution's declared section first (when
34
+ * its config declares one), then active packs'.
35
+ */
27
36
  export interface ColophonData {
28
37
  readonly sections: readonly ColophonSection[];
29
38
  }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * `config show` payload projection — declared story BODIES never ship.
3
+ *
4
+ * MCP returns the JSON formatter's output verbatim, so anything in this payload
5
+ * is tokens in every `config_show` tool call. The distribution declares five
6
+ * read stories on its packs, whose bodies are SPARQL; carried through, they take
7
+ * the payload from ~1.3 KB to ~11 KB — larger than `capabilities`, the discovery
8
+ * tool, for the least useful content in the surface.
9
+ */
10
+
11
+ import { mkdtempSync } from "node:fs";
12
+ import { tmpdir } from "node:os";
13
+ import { join } from "node:path";
14
+ import { describe, expect, it } from "vitest";
15
+ import { bootRuntime } from "../../kernel/runtime/boot.js";
16
+ import type { GlobalFlags } from "../../kernel/runtime/types.js";
17
+ import { collectConfigShow } from "./collectConfigShow.js";
18
+
19
+ const FLAGS: GlobalFlags = {
20
+ llm: false,
21
+ autoLlm: false,
22
+ format: "json",
23
+ verbose: false,
24
+ };
25
+
26
+ describe("config show payload", () => {
27
+ it("names the declared packs but carries no story bodies", async () => {
28
+ const cwd = mkdtempSync(join(tmpdir(), "pragma-config-show-"));
29
+ const data = await collectConfigShow(bootRuntime(FLAGS, cwd));
30
+
31
+ // The pack declarations themselves are still fully reported…
32
+ expect(
33
+ data.config.packs?.map((pack) =>
34
+ typeof pack === "string" ? pack : pack.name,
35
+ ),
36
+ ).toEqual([
37
+ "@canonical/design-system",
38
+ "@canonical/anatomy-dsl",
39
+ "@canonical/code-standards",
40
+ ]);
41
+ for (const pack of data.config.packs ?? []) {
42
+ expect(
43
+ typeof pack === "string" ? undefined : pack.stories,
44
+ ).toBeUndefined();
45
+ }
46
+ expect(data.config.stories).toBeUndefined();
47
+
48
+ // …and provenance stays honest: the payload still says where they came from.
49
+ expect(data.origins.packs).toBe("default");
50
+ expect(data.origins.stories).toBe("default");
51
+
52
+ // The sharp end: no query text reaches the agent surface.
53
+ const payload = JSON.stringify(data);
54
+ expect(payload).not.toContain("SELECT ?");
55
+ expect(payload.length).toBeLessThan(3000);
56
+ });
57
+ });
@@ -1,15 +1,52 @@
1
1
  /**
2
2
  * Collect the `config show` payload — the verb's run body (lazily imported).
3
3
  *
4
- * Storeless: read the layered config and pass it through with its provenance.
5
- * Dynamic-imported by the spec so building the tree never pulls the config
6
- * reader (or zod) onto the fast path.
4
+ * Storeless: read the layered config and pass it through with its provenance,
5
+ * minus the declared story BODIES (see {@link withoutStoryBodies}). Dynamic-
6
+ * imported by the spec so building the tree never pulls the config reader (or
7
+ * zod) onto the fast path.
7
8
  */
8
9
 
9
10
  import { readConfig } from "../../kernel/config/readConfig.js";
11
+ import type {
12
+ PackDeclaration,
13
+ PragmaConfig,
14
+ } from "../../kernel/config/types.js";
10
15
  import type { PragmaRuntime } from "../../kernel/runtime/types.js";
11
16
  import type { ConfigShowData } from "./types.js";
12
17
 
18
+ /** A pack declaration with any declared story bodies dropped. */
19
+ function nameAndSourceOnly(pack: PackDeclaration): PackDeclaration {
20
+ if (typeof pack === "string") return pack;
21
+ return {
22
+ name: pack.name,
23
+ ...(pack.source === undefined ? {} : { source: pack.source }),
24
+ };
25
+ }
26
+
27
+ /**
28
+ * The effective config with declared story BODIES dropped — `packs` entries keep
29
+ * `{ name, source }`, and the top-level `stories` array is omitted.
30
+ *
31
+ * A PROJECTION of the payload, not a hiding of config: `origins` still reports
32
+ * where `packs` and `stories` came from, the plain and llm renders already print
33
+ * pack NAMES only, and `pragma capabilities` lists the verbs the stories
34
+ * produce. It exists because the bodies are SPARQL — the distribution's own five
35
+ * stories take `config show --format json` from 1.3 KB to 11 KB, and MCP returns
36
+ * the JSON formatter's output, so every `config_show` tool call would carry
37
+ * ~2.7k tokens of query text: the largest payload in the surface, for its least
38
+ * useful content.
39
+ */
40
+ function withoutStoryBodies(config: PragmaConfig): PragmaConfig {
41
+ const { stories, ...rest } = config;
42
+ return {
43
+ ...rest,
44
+ ...(config.packs === undefined
45
+ ? {}
46
+ : { packs: config.packs.map(nameAndSourceOnly) }),
47
+ };
48
+ }
49
+
13
50
  /**
14
51
  * Assemble the resolved config with provenance for the current runtime.
15
52
  *
@@ -22,7 +59,7 @@ export async function collectConfigShow(
22
59
  ): Promise<ConfigShowData> {
23
60
  const layers = await readConfig(runtime.cwd);
24
61
  return {
25
- config: layers.config,
62
+ config: withoutStoryBodies(layers.config),
26
63
  origins: layers.origins,
27
64
  ...(layers.project.path ? { projectConfigPath: layers.project.path } : {}),
28
65
  globalConfigPath: layers.global.path,
@@ -1,28 +1,19 @@
1
1
  /**
2
- * The config field table — one declarative row per writable field drives all
3
- * three setters (`tier` / `channel` / `detail`), the way the pack grammar
4
- * compiles read verbs from a table.
5
- *
6
- * Covenant-exact emission is the hard gate (surfaceConformance): each verb must
7
- * `emitVerb` to `{ v:<field>, args:["<positional>"], mutates:true,
8
- * mcp:"config_<field>" }` — one REQUIRED positional, NO flags, NO needsStore
9
- * (config setters are storeless; the old store-backed `validateTier` is dropped
10
- * — PARITY_GAP `config-tier-no-ontology-validation`). The factory produces
11
- * exactly that shape; the `field.test.ts` deep-equals the golden slice.
2
+ * The config field table — one declarative row per writable field. Since AV-228
3
+ * B3 retired the per-field `config tier`/`channel`/`detail` verbs, this table no
4
+ * longer emits verbs of its own: it is the shared source of truth that drives
5
+ * `config set <key> <value>` (via `runSet` → `runField`), giving `set` its
6
+ * `<key>` enum, per-field reset sentinels, enum validation, and positional
7
+ * shaping for free.
12
8
  */
13
9
 
14
- import type { Task } from "@canonical/task";
15
10
  import { DETAIL_LEVELS } from "../../constants.js";
16
11
  import type { RawConfig } from "../../kernel/config/types.js";
17
12
  import { CHANNELS } from "../../kernel/config/types.js";
18
- import { asVerb } from "../../kernel/spec/asVerb.js";
19
- import type { ParamSpec, VerbSpec } from "../../kernel/spec/types.js";
20
- import { configFieldFormatters } from "./field.render.js";
21
- import type { ConfigFieldResult } from "./types.js";
22
13
 
23
14
  /** One writable config field and how its single positional is shaped/validated. */
24
15
  export interface ConfigFieldSpec {
25
- /** The `RawConfig` key written (also the verb label + `config_<field>` tool). */
16
+ /** The `RawConfig` key written (also the `config set` `<key>` enum member). */
26
17
  readonly field: keyof RawConfig & ("tier" | "channel" | "detail");
27
18
  /** The positional param NAME — its usage token is `<positional>` (covenant). */
28
19
  readonly positional: string;
@@ -30,24 +21,16 @@ export interface ConfigFieldSpec {
30
21
  readonly kind: "string" | "enum";
31
22
  /** The allowed values for an enum field. */
32
23
  readonly values?: readonly string[];
33
- /** One-line verb summary. */
34
- readonly summary: string;
35
- /** Extended help doc. */
36
- readonly doc: string;
37
24
  /** Reserved values that REMOVE the field instead of setting it. */
38
25
  readonly resetSentinel?: readonly string[];
39
- /** Verb help examples. */
40
- readonly examples: readonly { cmd: string; note?: string }[];
41
26
  }
42
27
 
43
28
  /**
44
29
  * The three writable fields. Only `tier` (a free string with a meaningful
45
30
  * "no value") carries reset sentinels; `channel`/`detail` reset by setting
46
- * their default (`normal`/`standard`) — no `--reset` flag exists, because a
47
- * flag would emit `flags:[...]` and break the covenant (PARITY_GAP
48
- * `config-field-reset-sentinel`). Query is `config show` — the covenant gives
49
- * each setter exactly ONE required positional, so a no-arg "print current" mode
50
- * is impossible here (PARITY_GAP `config-field-query-via-show`).
31
+ * their default (`normal`/`standard`). The migration path for the retired
32
+ * field-verbs is `config set <field> <value>` (e.g. `config set tier apps/lxd`,
33
+ * `config set tier none` to clear it).
51
34
  */
52
35
  export const CONFIG_FIELDS: readonly ConfigFieldSpec[] = [
53
36
  {
@@ -55,98 +38,17 @@ export const CONFIG_FIELDS: readonly ConfigFieldSpec[] = [
55
38
  positional: "path",
56
39
  kind: "string",
57
40
  resetSentinel: ["none", "default", "-"],
58
- summary: "Set the active design-system tier.",
59
- doc: "Writes the `tier` field to the global config. Pass `none`, `default`, or `-` to clear it. Written to the global layer only — project configs are authored by hand.",
60
- examples: [
61
- { cmd: "pragma config tier apps/lxd", note: "scope reads to a tier" },
62
- { cmd: "pragma config tier none", note: "clear the tier" },
63
- ],
64
41
  },
65
42
  {
66
43
  field: "channel",
67
44
  positional: "name",
68
45
  kind: "enum",
69
46
  values: CHANNELS,
70
- summary: "Set the release channel controlling component visibility.",
71
- doc: "Writes the `channel` field to the global config. Reset by setting the default, `normal`.",
72
- examples: [
73
- { cmd: "pragma config channel experimental" },
74
- { cmd: "pragma config channel normal", note: "back to the default" },
75
- ],
76
47
  },
77
48
  {
78
49
  field: "detail",
79
50
  positional: "level",
80
51
  kind: "enum",
81
52
  values: DETAIL_LEVELS,
82
- summary: "Set the default progressive-disclosure level.",
83
- doc: "Writes the `detail` field to the global config. Reset by setting the default, `standard`.",
84
- examples: [
85
- { cmd: "pragma config detail detailed" },
86
- { cmd: "pragma config detail standard", note: "back to the default" },
87
- ],
88
53
  },
89
54
  ];
90
-
91
- /** The single required positional for a field, shaped per its `kind`. */
92
- function fieldPositional(spec: ConfigFieldSpec): ParamSpec {
93
- // Name the positional by its usage token (`<path>`/`<name>`/`<level>`) so the
94
- // doc and the token agree (was "The tier value to write." for a `<path>`).
95
- const doc = `The ${spec.field} ${spec.positional} to write.`;
96
- if (spec.kind === "enum") {
97
- return {
98
- kind: "enum",
99
- name: spec.positional,
100
- doc,
101
- values: spec.values ?? [],
102
- required: true,
103
- positional: true,
104
- };
105
- }
106
- return {
107
- kind: "string",
108
- name: spec.positional,
109
- doc,
110
- required: true,
111
- positional: true,
112
- };
113
- }
114
-
115
- /**
116
- * Build one storeless, mutating config setter from a field spec.
117
- *
118
- * `run` lazily imports the write body and presents `Promise<Task<R>>` through
119
- * the `Task<R>` arm by the honest cast (the same seam `sources update` uses) —
120
- * a literal `Promise<Task>` union arm would poison async read-verb inference.
121
- *
122
- * @param spec - The field-table row.
123
- * @returns The verb spec for `config <field>`.
124
- */
125
- export function fieldVerb(
126
- spec: ConfigFieldSpec,
127
- ): VerbSpec<Record<string, unknown>, ConfigFieldResult> {
128
- return {
129
- path: ["config", spec.field],
130
- summary: spec.summary,
131
- doc: spec.doc,
132
- params: [fieldPositional(spec)],
133
- output: { formatters: configFieldFormatters },
134
- examples: [...spec.examples],
135
- capability: {
136
- needsStore: false,
137
- mutates: true,
138
- mcp: {
139
- expose: true,
140
- annotations: { readOnlyHint: false, openWorldHint: false },
141
- },
142
- },
143
- run: (params) =>
144
- import("./runField.js").then((m) =>
145
- m.runField(spec, params),
146
- ) as unknown as Task<ConfigFieldResult>,
147
- };
148
- }
149
-
150
- /** The three config setters, in covenant order (tier, channel, detail). */
151
- export const configFieldVerbs: VerbSpec[] =
152
- CONFIG_FIELDS.map(fieldVerb).map(asVerb);