@canonical/pragma-cli 0.30.0 → 0.31.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 (254) hide show
  1. package/README.md +64 -35
  2. package/dist/pragma +0 -0
  3. package/package.json +15 -13
  4. package/src/completions/constants.ts +7 -0
  5. package/src/completions/handleQuery.test.ts +32 -0
  6. package/src/completions/handleQuery.ts +20 -1
  7. package/src/completions/queryCompletions.test.ts +5 -0
  8. package/src/completions/queryCompletions.ts +17 -3
  9. package/src/completions/querySocket.test.ts +31 -0
  10. package/src/completions/querySocket.ts +36 -9
  11. package/src/config/parseConfigValues.ts +2 -0
  12. package/src/config/readConfigLayers.test.ts +2 -0
  13. package/src/config/readConfigLayers.ts +3 -0
  14. package/src/config/types.ts +7 -0
  15. package/src/constants.ts +3 -1
  16. package/src/domains/block/commands/sample.ts +2 -1
  17. package/src/domains/block/formatters/list.test.ts +3 -3
  18. package/src/domains/block/formatters/lookup.test.ts +5 -5
  19. package/src/domains/block/orchestration/enrichBlocks.ts +6 -3
  20. package/src/domains/block/stories.ts +9 -3
  21. package/src/domains/config/commands/channel.test.ts +16 -0
  22. package/src/domains/config/commands/channel.ts +14 -1
  23. package/src/domains/config/commands/framework.test.ts +14 -0
  24. package/src/domains/config/commands/framework.ts +13 -1
  25. package/src/domains/config/commands/tier.test.ts +7 -0
  26. package/src/domains/config/commands/tier.ts +14 -1
  27. package/src/domains/config/operations/validateTier.test.ts +9 -0
  28. package/src/domains/config/operations/validateTier.ts +10 -2
  29. package/src/domains/create/generatorPack/bundled.ts +49 -0
  30. package/src/domains/create/generatorPack/compileGeneratorPack.test.ts +159 -0
  31. package/src/domains/create/generatorPack/compileGeneratorPack.ts +312 -0
  32. package/src/domains/create/index.ts +13 -7
  33. package/src/domains/create/mcp/specs.ts +6 -161
  34. package/src/domains/create/renderGeneratorUi.test.ts +103 -0
  35. package/src/domains/create/renderGeneratorUi.ts +65 -0
  36. package/src/domains/doctor/formatters/doctor.test.ts +50 -2
  37. package/src/domains/doctor/formatters/doctor.ts +90 -38
  38. package/src/domains/doctor/operations/checks/checkMcpCommands.ts +9 -6
  39. package/src/domains/doctor/operations/checks/checkPackageRefs.ts +30 -12
  40. package/src/domains/doctor/operations/checks.test.ts +6 -4
  41. package/src/domains/doctor/operations/types.ts +23 -1
  42. package/src/domains/graph/commands/inspect.test.ts +2 -2
  43. package/src/domains/graph/commands/query.test.ts +5 -3
  44. package/src/domains/graph/formatters/inspect.ts +7 -7
  45. package/src/domains/graph/formatters/query.test.ts +87 -0
  46. package/src/domains/graph/formatters/query.ts +42 -5
  47. package/src/domains/graph/operations/inspectUri.test.ts +5 -5
  48. package/src/domains/graphql/commands/serve.test.ts +12 -6
  49. package/src/domains/graphql/commands/serve.ts +16 -6
  50. package/src/domains/graphql/helpers/parsePrefixes.test.ts +3 -3
  51. package/src/domains/graphql/helpers/parsePrefixes.ts +3 -3
  52. package/src/domains/graphql/helpers/resolveSourceFiles.test.ts +9 -0
  53. package/src/domains/graphql/helpers/resolveSourceFiles.ts +24 -3
  54. package/src/domains/graphql/operations/compileSchema.test.ts +2 -2
  55. package/src/domains/graphql/operations/createSchemaServer.test.ts +2 -2
  56. package/src/domains/graphql/operations/ensureSchemaArtifact.test.ts +167 -0
  57. package/src/domains/graphql/operations/ensureSchemaArtifact.ts +178 -0
  58. package/src/domains/graphql/operations/index.ts +5 -0
  59. package/src/domains/info/formatters/upgrade.test.ts +7 -4
  60. package/src/domains/info/formatters/upgrade.ts +3 -1
  61. package/src/domains/info/operations/collectEntityCounts.test.ts +156 -0
  62. package/src/domains/info/operations/collectEntityCounts.ts +123 -0
  63. package/src/domains/info/operations/index.ts +1 -0
  64. package/src/domains/info/types.ts +13 -0
  65. package/src/domains/llm/capabilities.test.ts +23 -3
  66. package/src/domains/llm/commands/capabilities.ts +12 -3
  67. package/src/domains/llm/commands/llm.ts +5 -1
  68. package/src/domains/llm/data/commandReference.ts +20 -11
  69. package/src/domains/llm/data/decisionTrees.ts +10 -5
  70. package/src/domains/llm/data/toolCatalog.ts +49 -18
  71. package/src/domains/llm/formatters/orientation.ts +5 -1
  72. package/src/domains/llm/llmCommand.test.ts +11 -3
  73. package/src/domains/llm/mcp/specs.ts +3 -4
  74. package/src/domains/llm/operations/collectContext.test.ts +64 -0
  75. package/src/domains/llm/operations/collectContext.ts +7 -17
  76. package/src/domains/modifier/commands/sample.ts +2 -1
  77. package/src/domains/modifier/operations/lookup.ts +5 -3
  78. package/src/domains/modifier/stories.ts +9 -0
  79. package/src/domains/ontology/commands/list.test.ts +5 -3
  80. package/src/domains/ontology/commands/show.test.ts +85 -9
  81. package/src/domains/ontology/formatters/index.ts +1 -0
  82. package/src/domains/ontology/formatters/list.ts +37 -12
  83. package/src/domains/ontology/formatters/show.ts +358 -67
  84. package/src/domains/ontology/helpers/buildClassTree.ts +89 -0
  85. package/src/domains/ontology/helpers/completeClassNames.ts +43 -0
  86. package/src/domains/ontology/helpers/expandOntologyIris.ts +79 -0
  87. package/src/domains/ontology/helpers/index.ts +14 -0
  88. package/src/domains/ontology/helpers/queryClasses.ts +32 -16
  89. package/src/domains/ontology/helpers/queryProperties.ts +39 -6
  90. package/src/domains/ontology/helpers/queryTboxFacts.ts +160 -0
  91. package/src/domains/ontology/operations/listOntologies.test.ts +80 -3
  92. package/src/domains/ontology/operations/listOntologies.ts +126 -30
  93. package/src/domains/ontology/operations/showOntology.test.ts +131 -12
  94. package/src/domains/ontology/operations/showOntology.ts +289 -7
  95. package/src/domains/ontology/operations/showOntologyRaw.test.ts +4 -2
  96. package/src/domains/ontology/stories.ts +53 -8
  97. package/src/domains/refs/operations/updateRefs.ts +0 -1
  98. package/src/domains/setup/commands/index.ts +1 -0
  99. package/src/domains/setup/commands/setup.ts +70 -0
  100. package/src/domains/setup/index.ts +7 -3
  101. package/src/domains/setup/operations/setupLsp.test.ts +3 -2
  102. package/src/domains/setup/operations/setupLsp.ts +7 -2
  103. package/src/domains/shared/bootStore.bareCore.test.ts +63 -0
  104. package/src/domains/shared/bootStore.test.ts +82 -0
  105. package/src/domains/shared/bootStore.ts +56 -20
  106. package/src/domains/shared/emptyErrorRecovery.test.ts +23 -26
  107. package/src/domains/shared/extractLocalName.test.ts +3 -3
  108. package/src/domains/shared/formatters.ts +23 -2
  109. package/src/domains/shared/listTiers.ts +40 -0
  110. package/src/domains/shared/loaders/bundledLoader.ts +48 -0
  111. package/src/domains/shared/loaders/gitLoader.ts +1 -2
  112. package/src/domains/shared/loaders/localLoader.ts +2 -18
  113. package/src/domains/shared/loaders/readPackageDir.test.ts +58 -0
  114. package/src/domains/shared/loaders/readPackageDir.ts +46 -8
  115. package/src/domains/shared/lookupMany.test.ts +15 -0
  116. package/src/domains/shared/lookupMany.ts +22 -6
  117. package/src/domains/shared/namespaces.ts +13 -5
  118. package/src/domains/shared/parseSampleCount.test.ts +47 -0
  119. package/src/domains/shared/parseSampleCount.ts +45 -0
  120. package/src/domains/shared/prefixes.test.ts +211 -0
  121. package/src/domains/shared/prefixes.ts +182 -15
  122. package/src/domains/shared/renderers.ts +5 -5
  123. package/src/domains/shared/resolveLookupExitCode.test.ts +48 -0
  124. package/src/domains/shared/resolveLookupExitCode.ts +42 -0
  125. package/src/domains/shared/semanticPackage.test.ts +89 -0
  126. package/src/domains/shared/semanticPackage.ts +17 -2
  127. package/src/domains/shared/stories/compileLookupCommand.ts +3 -1
  128. package/src/domains/shared/stories/compileLookupTool.ts +1 -1
  129. package/src/domains/shared/stories/pack/applyPackFilters.test.ts +49 -0
  130. package/src/domains/shared/stories/pack/applyPackFilters.ts +46 -11
  131. package/src/domains/shared/stories/pack/applyPackSearch.test.ts +57 -0
  132. package/src/domains/shared/stories/pack/applyPackSearch.ts +41 -0
  133. package/src/domains/shared/stories/pack/buildLookupQuery.test.ts +30 -0
  134. package/src/domains/shared/stories/pack/buildLookupQuery.ts +75 -1
  135. package/src/domains/shared/stories/pack/bundled/index.ts +23 -0
  136. package/src/domains/shared/stories/pack/bundled/standardPack.ts +164 -0
  137. package/src/domains/shared/stories/pack/bundled/tierPack.ts +50 -0
  138. package/src/domains/shared/stories/pack/collectPackStories.test.ts +305 -7
  139. package/src/domains/shared/stories/pack/collectPackStories.ts +90 -8
  140. package/src/domains/shared/stories/pack/compilePackCommands.ts +17 -7
  141. package/src/domains/shared/stories/pack/compilePackStories.test.ts +607 -2
  142. package/src/domains/shared/stories/pack/compilePackStories.ts +544 -73
  143. package/src/domains/shared/stories/pack/compilePackToolSpecs.ts +20 -8
  144. package/src/domains/shared/stories/pack/index.ts +14 -0
  145. package/src/domains/shared/stories/pack/reservedVerbs.test.ts +213 -0
  146. package/src/domains/shared/stories/pack/reservedVerbs.ts +170 -0
  147. package/src/domains/shared/stories/pack/standardParity.test.ts +293 -220
  148. package/src/domains/shared/stories/pack/types.ts +123 -3
  149. package/src/domains/shared/stories/pack/validateStoryPackDefinition.test.ts +502 -0
  150. package/src/domains/shared/stories/pack/validateStoryPackDefinition.ts +475 -24
  151. package/src/domains/shared/stories/types.ts +9 -1
  152. package/src/domains/shared/types/index.ts +5 -9
  153. package/src/domains/shared/types/ontology.ts +119 -24
  154. package/src/domains/shared/types/standard.ts +9 -52
  155. package/src/domains/token/commands/lookup.test.ts +8 -0
  156. package/src/domains/token/commands/lookup.ts +12 -0
  157. package/src/domains/token/commands/sample.ts +2 -1
  158. package/src/domains/token/featureFlag.ts +17 -0
  159. package/src/domains/token/index.ts +5 -0
  160. package/src/domains/token/mcp/specs.ts +13 -1
  161. package/src/domains/trace/commands/resolveTraceLimit.test.ts +31 -0
  162. package/src/domains/trace/commands/show.ts +30 -4
  163. package/src/index.ts +1 -13
  164. package/src/mcp/registerResources.test.ts +2 -2
  165. package/src/mcp/resources/pickPrimaryType.test.ts +5 -5
  166. package/src/mcp/tools/index.ts +18 -10
  167. package/src/mcp/tools/registerTools.test.ts +106 -30
  168. package/src/pipeline/collectCommands.test.ts +151 -1
  169. package/src/pipeline/collectCommands.ts +34 -19
  170. package/src/pipeline/createProgram.test.ts +5 -1
  171. package/src/pipeline/createProgram.ts +2 -2
  172. package/src/pipeline/firstRun.test.ts +80 -0
  173. package/src/pipeline/firstRun.ts +99 -0
  174. package/src/pipeline/parseGlobalFlags.test.ts +102 -1
  175. package/src/pipeline/parseGlobalFlags.ts +98 -9
  176. package/src/pipeline/resolveCommandKind.test.ts +12 -0
  177. package/src/pipeline/resolveCommandKind.ts +4 -0
  178. package/src/pipeline/rootHelp.test.ts +87 -0
  179. package/src/pipeline/rootHelp.ts +265 -0
  180. package/src/pipeline/runCli.orchestration.test.ts +28 -12
  181. package/src/pipeline/runCli.test.ts +17 -4
  182. package/src/pipeline/runCli.ts +162 -19
  183. package/src/testing/cli.ts +30 -2
  184. package/src/testing/dsFixtures.ts +21 -4
  185. package/src/testing/index.ts +1 -4
  186. package/src/testing/integration/__snapshots__/executor-golden.test.ts.snap +4 -4
  187. package/src/testing/integration/cli-user-stories.test.ts +63 -0
  188. package/src/testing/integration/cross-binary-byte-equality.test.ts +2 -2
  189. package/src/testing/integration/edge-cases.test.ts +62 -50
  190. package/src/testing/integration/executor-execution.test.ts +6 -4
  191. package/src/testing/integration/executor-golden.test.ts +2 -2
  192. package/src/testing/integration/mcp-surface.test.ts +40 -15
  193. package/src/testing/integration/parity.test.ts +119 -70
  194. package/src/testing/setupXdgIsolation.ts +16 -5
  195. package/src/testing/standardParityFixtures.ts +26 -83
  196. package/src/testing/store.ts +4 -4
  197. package/src/domains/create/commands/component.test.ts +0 -166
  198. package/src/domains/create/commands/component.ts +0 -123
  199. package/src/domains/create/commands/index.ts +0 -3
  200. package/src/domains/create/commands/package.test.ts +0 -35
  201. package/src/domains/create/commands/package.ts +0 -21
  202. package/src/domains/create/generators.ts +0 -8
  203. package/src/domains/standard/commands/categories.test.ts +0 -70
  204. package/src/domains/standard/commands/categories.ts +0 -48
  205. package/src/domains/standard/commands/index.ts +0 -6
  206. package/src/domains/standard/commands/list.test.ts +0 -105
  207. package/src/domains/standard/commands/list.ts +0 -11
  208. package/src/domains/standard/commands/lookup.test.ts +0 -135
  209. package/src/domains/standard/commands/lookup.ts +0 -14
  210. package/src/domains/standard/commands/sample.ts +0 -57
  211. package/src/domains/standard/formatters/categories.test.ts +0 -44
  212. package/src/domains/standard/formatters/categories.ts +0 -38
  213. package/src/domains/standard/formatters/index.ts +0 -10
  214. package/src/domains/standard/formatters/list.test.ts +0 -169
  215. package/src/domains/standard/formatters/list.ts +0 -184
  216. package/src/domains/standard/formatters/lookup.test.ts +0 -95
  217. package/src/domains/standard/formatters/lookup.ts +0 -101
  218. package/src/domains/standard/formatters/sample.ts +0 -15
  219. package/src/domains/standard/formatters/types.ts +0 -31
  220. package/src/domains/standard/index.ts +0 -40
  221. package/src/domains/standard/mcp/index.ts +0 -2
  222. package/src/domains/standard/mcp/specs.ts +0 -89
  223. package/src/domains/standard/operations/categories.test.ts +0 -61
  224. package/src/domains/standard/operations/categories.ts +0 -38
  225. package/src/domains/standard/operations/index.ts +0 -6
  226. package/src/domains/standard/operations/list.test.ts +0 -75
  227. package/src/domains/standard/operations/list.ts +0 -64
  228. package/src/domains/standard/operations/lookup.test.ts +0 -58
  229. package/src/domains/standard/operations/lookup.ts +0 -151
  230. package/src/domains/standard/operations/sample.ts +0 -35
  231. package/src/domains/standard/orchestration/buildStandardFilters.ts +0 -11
  232. package/src/domains/standard/orchestration/index.ts +0 -4
  233. package/src/domains/standard/orchestration/resolveStandardList.ts +0 -45
  234. package/src/domains/standard/orchestration/resolveStandardLookup.ts +0 -33
  235. package/src/domains/standard/orchestration/standardEmptyError.test.ts +0 -33
  236. package/src/domains/standard/orchestration/standardEmptyError.ts +0 -29
  237. package/src/domains/standard/standardConfig.ts +0 -42
  238. package/src/domains/standard/stories.test.ts +0 -24
  239. package/src/domains/standard/stories.ts +0 -151
  240. package/src/domains/standard/types.ts +0 -15
  241. package/src/domains/tier/commands/index.ts +0 -3
  242. package/src/domains/tier/commands/list.test.ts +0 -67
  243. package/src/domains/tier/commands/list.ts +0 -9
  244. package/src/domains/tier/formatters/index.ts +0 -3
  245. package/src/domains/tier/formatters/list.test.ts +0 -33
  246. package/src/domains/tier/formatters/list.ts +0 -45
  247. package/src/domains/tier/index.ts +0 -17
  248. package/src/domains/tier/mcp/index.ts +0 -2
  249. package/src/domains/tier/mcp/specs.ts +0 -12
  250. package/src/domains/tier/operations/index.ts +0 -3
  251. package/src/domains/tier/operations/list.test.ts +0 -32
  252. package/src/domains/tier/operations/list.ts +0 -33
  253. package/src/domains/tier/stories.test.ts +0 -22
  254. package/src/domains/tier/stories.ts +0 -52
@@ -15,8 +15,13 @@ import {
15
15
  RECIPE_STORY,
16
16
  RECIPE_TTL,
17
17
  } from "#testing";
18
+ import { TOKEN_READ_SURFACE_ENABLED } from "../../domains/token/featureFlag.js";
18
19
  import type { McpErrorPayload } from "../types.js";
19
20
 
21
+ // token_list, token_lookup, and token_sample are gated behind the token
22
+ // read-surface feature flag.
23
+ const EXPECTED_TOOL_COUNT = TOKEN_READ_SURFACE_ENABLED ? 34 : 31;
24
+
20
25
  let client: Client;
21
26
  let cleanup: () => Promise<void>;
22
27
 
@@ -57,9 +62,9 @@ function parseData(result: Record<string, unknown>): unknown {
57
62
  // =============================================================================
58
63
 
59
64
  describe("tool listing", () => {
60
- it("registers 30 tools", async () => {
65
+ it("registers the expected number of tools", async () => {
61
66
  const { tools } = await client.listTools();
62
- expect(tools).toHaveLength(30);
67
+ expect(tools).toHaveLength(EXPECTED_TOOL_COUNT);
63
68
  });
64
69
 
65
70
  it("all tools have descriptions", async () => {
@@ -80,8 +85,10 @@ describe("tool listing", () => {
80
85
  expect(names).toContain("standard_categories");
81
86
  expect(names).toContain("modifier_list");
82
87
  expect(names).toContain("modifier_lookup");
83
- expect(names).toContain("token_list");
84
- expect(names).toContain("token_lookup");
88
+ if (TOKEN_READ_SURFACE_ENABLED) {
89
+ expect(names).toContain("token_list");
90
+ expect(names).toContain("token_lookup");
91
+ }
85
92
  expect(names).toContain("tier_list");
86
93
  expect(names).toContain("config_show");
87
94
  expect(names).toContain("config_tier");
@@ -102,7 +109,9 @@ describe("tool listing", () => {
102
109
  // Sample tools
103
110
  expect(names).toContain("block_sample");
104
111
  expect(names).toContain("standard_sample");
105
- expect(names).toContain("token_sample");
112
+ if (TOKEN_READ_SURFACE_ENABLED) {
113
+ expect(names).toContain("token_sample");
114
+ }
106
115
  expect(names).toContain("modifier_sample");
107
116
  });
108
117
 
@@ -112,6 +121,67 @@ describe("tool listing", () => {
112
121
  expect(tool.name).not.toMatch(/^pragma_/);
113
122
  }
114
123
  });
124
+
125
+ // Golden surface: the full built-in tool set plus bundled packs, in
126
+ // registration order. The per-(noun, verb) reserved-guard flip must leave
127
+ // this byte-identical AND order-identical — registration order is the tool
128
+ // surface a client sees.
129
+ it("has a stable built-in tool surface", async () => {
130
+ const names = (await client.listTools()).tools.map((tool) => tool.name);
131
+
132
+ expect(names).toEqual([
133
+ "block_list",
134
+ "block_lookup",
135
+ "block_sample",
136
+ "modifier_list",
137
+ "modifier_lookup",
138
+ "modifier_sample",
139
+ ...(TOKEN_READ_SURFACE_ENABLED ? ["token_list", "token_lookup"] : []),
140
+ "tokens_add_config",
141
+ ...(TOKEN_READ_SURFACE_ENABLED ? ["token_sample"] : []),
142
+ "config_show",
143
+ "config_tier",
144
+ "config_channel",
145
+ "ontology_list",
146
+ "ontology_show",
147
+ "graph_query",
148
+ "graph_inspect",
149
+ "skill_list",
150
+ "skill_lookup",
151
+ "doctor",
152
+ "info",
153
+ "capabilities",
154
+ "llm",
155
+ "create_component",
156
+ "create_package",
157
+ "create_application",
158
+ "create_domain",
159
+ "create_route",
160
+ "create_wrapper",
161
+ // Bundled packs register after the built-in tools: `tier_list` and the
162
+ // whole `standard` noun are served by bundled story packs — the
163
+ // hand-written tier and standard domains were deleted.
164
+ "tier_list",
165
+ "standard_list",
166
+ "standard_lookup",
167
+ "standard_categories",
168
+ "standard_sample",
169
+ ]);
170
+ });
171
+
172
+ it("serves the standard tools from the bundled pack, not built-ins", async () => {
173
+ const { tools } = await client.listTools();
174
+ const standardList = tools.find((tool) => tool.name === "standard_list");
175
+ // Pack-compiled tool descriptions carry no story-pack suffix here only
176
+ // because the bundled pack curates them; the giveaway is the derived
177
+ // `search`/`category` params on the pack projection.
178
+ const params = Object.keys(
179
+ (standardList?.inputSchema as { properties?: object })?.properties ?? {},
180
+ );
181
+ expect(params).toContain("category");
182
+ expect(params).toContain("search");
183
+ expect(params).toContain("condensed");
184
+ });
115
185
  });
116
186
 
117
187
  // =============================================================================
@@ -419,7 +489,7 @@ describe("modifier_lookup", () => {
419
489
  // Token tools
420
490
  // =============================================================================
421
491
 
422
- describe("token_list", () => {
492
+ describe.skipIf(!TOKEN_READ_SURFACE_ENABLED)("token_list", () => {
423
493
  it("returns tokens", async () => {
424
494
  const result = await client.callTool({
425
495
  name: "token_list",
@@ -430,7 +500,7 @@ describe("token_list", () => {
430
500
  });
431
501
  });
432
502
 
433
- describe("token_lookup", () => {
503
+ describe.skipIf(!TOKEN_READ_SURFACE_ENABLED)("token_lookup", () => {
434
504
  it("returns token with theme values", async () => {
435
505
  const result = await client.callTool({
436
506
  name: "token_lookup",
@@ -468,10 +538,11 @@ describe("tier_list", () => {
468
538
  name: "tier_list",
469
539
  arguments: {},
470
540
  });
471
- const data = parseData(result) as { path: string }[];
541
+ // The bundled tier pack names the field `name` (its `ds:name` value).
542
+ const data = parseData(result) as { name: string }[];
472
543
  expect(data.length).toBeGreaterThan(0);
473
- const paths = data.map((t) => t.path);
474
- expect(paths).toContain("global");
544
+ const names = data.map((t) => t.name);
545
+ expect(names).toContain("global");
475
546
  });
476
547
  });
477
548
 
@@ -714,7 +785,7 @@ describe("create_component", () => {
714
785
  const result = await client.callTool({
715
786
  name: "create_component",
716
787
  arguments: {
717
- framework: "react",
788
+ variant: "react",
718
789
  componentPath: "src/components/Button",
719
790
  },
720
791
  });
@@ -729,7 +800,7 @@ describe("create_component", () => {
729
800
  const result = await client.callTool({
730
801
  name: "create_component",
731
802
  arguments: {
732
- framework: "svelte",
803
+ variant: "svelte",
733
804
  componentPath: "src/lib/components/Toggle",
734
805
  },
735
806
  });
@@ -738,11 +809,11 @@ describe("create_component", () => {
738
809
  expect((data.generator as { name: string }).name).toBe("component/svelte");
739
810
  });
740
811
 
741
- it("returns error for invalid framework", async () => {
812
+ it("returns error for invalid variant", async () => {
742
813
  const result = await client.callTool({
743
814
  name: "create_component",
744
815
  arguments: {
745
- framework: "angular",
816
+ variant: "angular",
746
817
  componentPath: "src/components/Button",
747
818
  },
748
819
  });
@@ -784,7 +855,7 @@ describe("llm", () => {
784
855
  expect(data.context.counts.blocks).toBeGreaterThan(0);
785
856
  expect(data.context.counts.standards).toBeGreaterThan(0);
786
857
  expect(data.context.namespaces.length).toBeGreaterThan(0);
787
- expect(data.decisionTrees).toHaveLength(5);
858
+ expect(data.decisionTrees).toHaveLength(TOKEN_READ_SURFACE_ENABLED ? 5 : 4);
788
859
  expect(data.commandReference.length).toBeGreaterThan(0);
789
860
  });
790
861
  });
@@ -798,7 +869,9 @@ describe("error handling", () => {
798
869
  const notFoundCalls = [
799
870
  { name: "block_lookup", arguments: { names: ["X"] } },
800
871
  { name: "modifier_lookup", arguments: { names: ["X"] } },
801
- { name: "token_lookup", arguments: { names: ["X"] } },
872
+ ...(TOKEN_READ_SURFACE_ENABLED
873
+ ? [{ name: "token_lookup", arguments: { names: ["X"] } }]
874
+ : []),
802
875
  { name: "standard_lookup", arguments: { names: ["X"] } },
803
876
  ] as const;
804
877
 
@@ -872,7 +945,7 @@ describe("ontology_show", () => {
872
945
  const envelope = parseEnvelope(result);
873
946
  expect(envelope.ok).toBe(true);
874
947
  expect(envelope.condensed).toBe(true);
875
- expect(envelope.text).toEqual(expect.stringContaining("## ds:"));
948
+ expect(envelope.text).toEqual(expect.stringContaining("## Ontology ds:"));
876
949
  });
877
950
  });
878
951
 
@@ -995,7 +1068,7 @@ describe("story-pack tools", () => {
995
1068
  const tools = (await scoped.client.listTools()).tools.map(
996
1069
  (tool) => tool.name,
997
1070
  );
998
- expect(tools).toHaveLength(32);
1071
+ expect(tools).toHaveLength(EXPECTED_TOOL_COUNT + 2);
999
1072
  expect(tools).toContain("recipe_list");
1000
1073
  expect(tools).toContain("recipe_lookup");
1001
1074
 
@@ -1105,9 +1178,9 @@ describe("capabilities", () => {
1105
1178
  expect(toolNames).toContain("block_list");
1106
1179
  expect(toolNames).toContain("capabilities");
1107
1180
  expect(data.tools.every((t) => t.use_when.length > 0)).toBe(true);
1108
- expect(data.counts.total).toBe(30);
1181
+ expect(data.counts.total).toBe(EXPECTED_TOOL_COUNT);
1109
1182
  expect(data.counts.read).toBeGreaterThan(0);
1110
- expect(data.counts.write).toBe(5);
1183
+ expect(data.counts.write).toBe(9);
1111
1184
  expect(data.counts.orientation).toBe(2);
1112
1185
  expect(data.counts.diagnostic).toBe(2);
1113
1186
  expect(data.version).toBeDefined();
@@ -1188,14 +1261,17 @@ describe("condensed parameter", () => {
1188
1261
  expect((envelope.text as string).length).toBeGreaterThan(0);
1189
1262
  });
1190
1263
 
1191
- it("token_lookup returns condensed text", async () => {
1192
- const result = await client.callTool({
1193
- name: "token_lookup",
1194
- arguments: { names: ["color.primary"], condensed: true },
1195
- });
1196
- const envelope = parseEnvelope(result);
1197
- expect(envelope.ok).toBe(true);
1198
- expect(envelope.condensed).toBe(true);
1199
- expect(typeof envelope.text).toBe("string");
1200
- });
1264
+ it.skipIf(!TOKEN_READ_SURFACE_ENABLED)(
1265
+ "token_lookup returns condensed text",
1266
+ async () => {
1267
+ const result = await client.callTool({
1268
+ name: "token_lookup",
1269
+ arguments: { names: ["color.primary"], condensed: true },
1270
+ });
1271
+ const envelope = parseEnvelope(result);
1272
+ expect(envelope.ok).toBe(true);
1273
+ expect(envelope.condensed).toBe(true);
1274
+ expect(typeof envelope.text).toBe("string");
1275
+ },
1276
+ );
1201
1277
  });
@@ -1,8 +1,16 @@
1
1
  import { afterAll, beforeAll, describe, expect, it } from "vitest";
2
2
  import type { PragmaContext } from "../domains/shared/context.js";
3
3
  import type { PragmaRuntime } from "../domains/shared/runtime.js";
4
+ import {
5
+ deriveReservedVerbs,
6
+ isReserved,
7
+ nounVerbFromPath,
8
+ nounVerbFromToolName,
9
+ } from "../domains/shared/stories/pack/index.js";
10
+ import { TOKEN_READ_SURFACE_ENABLED } from "../domains/token/featureFlag.js";
11
+ import { allSpecs } from "../mcp/tools/index.js";
4
12
  import createTestRuntime from "../testing/helpers/createTestRuntime.js";
5
- import collectCommands from "./collectCommands.js";
13
+ import collectCommands, { builtInCommands } from "./collectCommands.js";
6
14
 
7
15
  let runtime: PragmaRuntime;
8
16
 
@@ -34,4 +42,146 @@ describe("collectCommands", () => {
34
42
  expect(paths).toContain("graph query");
35
43
  expect(paths).toContain("graph inspect");
36
44
  });
45
+
46
+ // Golden surface: the full built-in command set, in emission order. The
47
+ // per-(noun, verb) reserved-guard flip must not add, drop, rename, or REORDER
48
+ // a single built-in command — order drives help output and registration.
49
+ // `tier list` and the whole `standard` noun are no longer here: the
50
+ // hand-written domains were deleted and are served by bundled story packs
51
+ // (asserted below).
52
+ it("has a stable built-in command surface", () => {
53
+ const paths = builtInCommands(makeCtx()).map((command) =>
54
+ command.path.join(" "),
55
+ );
56
+
57
+ expect(paths).toEqual([
58
+ "config tier",
59
+ "config channel",
60
+ "config trace",
61
+ "config framework",
62
+ "config show",
63
+ "create component",
64
+ "create package",
65
+ "create application",
66
+ "create domain",
67
+ "create route",
68
+ "create wrapper",
69
+ "setup",
70
+ "setup lsp",
71
+ "setup mcp",
72
+ "setup completions",
73
+ "setup skills",
74
+ "modifier list",
75
+ "modifier lookup",
76
+ "modifier sample",
77
+ ...(TOKEN_READ_SURFACE_ENABLED ? ["token list", "token lookup"] : []),
78
+ "tokens add-config",
79
+ ...(TOKEN_READ_SURFACE_ENABLED ? ["token sample"] : []),
80
+ "block list",
81
+ "block lookup",
82
+ "block sample",
83
+ "ontology list",
84
+ "ontology show",
85
+ "graph query",
86
+ "graph inspect",
87
+ "graphql build",
88
+ "graphql check",
89
+ "graphql serve",
90
+ "skill list",
91
+ "skill lookup",
92
+ "doctor",
93
+ "info",
94
+ "upgrade",
95
+ "llm",
96
+ "capabilities",
97
+ ]);
98
+ });
99
+
100
+ it("serves `tier list` from the bundled pack, not a built-in", () => {
101
+ const builtInPaths = builtInCommands(makeCtx()).map((c) =>
102
+ c.path.join(" "),
103
+ );
104
+ const allPaths = collectCommands(makeCtx()).map((c) => c.path.join(" "));
105
+
106
+ expect(builtInPaths).not.toContain("tier list");
107
+ expect(allPaths).toContain("tier list");
108
+ });
109
+
110
+ it("serves the whole `standard` noun from the bundled pack", () => {
111
+ const builtInPaths = builtInCommands(makeCtx()).map((c) =>
112
+ c.path.join(" "),
113
+ );
114
+ const allPaths = collectCommands(makeCtx()).map((c) => c.path.join(" "));
115
+
116
+ const standardVerbs = [
117
+ "standard list",
118
+ "standard lookup",
119
+ "standard categories",
120
+ "standard sample",
121
+ ];
122
+ for (const path of standardVerbs) {
123
+ expect(builtInPaths).not.toContain(path);
124
+ expect(allPaths).toContain(path);
125
+ }
126
+ });
127
+ });
128
+
129
+ // Cross-surface skew invariant (FIX 2 / R4-F1): the CLI and MCP surfaces both
130
+ // derive their reserved map through the single shared deriveReservedVerbs, so
131
+ // they MUST agree on every leaf read (noun, verb) reservation. If they drifted,
132
+ // a pack migrating a `list`/`lookup` verb could be admitted on one surface and
133
+ // blocked on the other. Operational nouns legitimately differ (graphql/setup/
134
+ // upgrade are CLI-only), so the invariant is scoped to the read verbs that
135
+ // packs actually emit.
136
+ describe("cross-surface reserved-verb parity", () => {
137
+ const READ_VERBS = ["list", "lookup"] as const;
138
+
139
+ it("CLI and MCP reserve the same list/lookup verbs for every leaf read noun", () => {
140
+ // Both surfaces derive reservations from BUILT-INS only (production reserves
141
+ // from built-ins, before appending packs), so a bundled/config/package pack
142
+ // never inflates the reserved map. Using collectCommands here would fold the
143
+ // bundled tier pack into the CLI side and break the symmetry.
144
+ const cliPairs = builtInCommands(makeCtx()).map((command) =>
145
+ nounVerbFromPath(command.path),
146
+ );
147
+ const mcpPairs = allSpecs.map((spec) => nounVerbFromToolName(spec.name));
148
+
149
+ const cliReserved = deriveReservedVerbs(cliPairs);
150
+ const mcpReserved = deriveReservedVerbs(mcpPairs);
151
+
152
+ // FIX 1 on the real CLI surface: operational nouns own no list/lookup, so
153
+ // they are reserved wholesale — a pack emitting `list` stays blocked.
154
+ for (const noun of ["config", "graph", "graphql", "setup", "create"]) {
155
+ expect(isReserved(cliReserved, noun, "list")).toBe(true);
156
+ }
157
+
158
+ // Leaf read nouns: those that own a `list`/`lookup` verb on either surface.
159
+ const readNouns = new Set(
160
+ [...cliPairs, ...mcpPairs]
161
+ .filter(([, verb]) => verb === "list" || verb === "lookup")
162
+ .map(([noun]) => noun),
163
+ );
164
+
165
+ // Sanity: the real leaf-migration targets are present, so this asserts
166
+ // something (a silently empty set would make the test vacuous). `tier`
167
+ // and `standard` are no longer here — both were cut over to bundled
168
+ // packs, so they are correctly absent from the built-in reserved
169
+ // surface on both sides. The token noun only owns read verbs while its
170
+ // feature flag is on.
171
+ const expectedReadNouns = ["block", "modifier"];
172
+ if (TOKEN_READ_SURFACE_ENABLED) expectedReadNouns.push("token");
173
+ for (const noun of expectedReadNouns) {
174
+ expect(readNouns.has(noun)).toBe(true);
175
+ }
176
+ expect(readNouns.has("standard")).toBe(false);
177
+
178
+ for (const noun of readNouns) {
179
+ for (const verb of READ_VERBS) {
180
+ expect(
181
+ isReserved(cliReserved, noun, verb),
182
+ `CLI/MCP disagree on ${noun} ${verb}`,
183
+ ).toBe(isReserved(mcpReserved, noun, verb));
184
+ }
185
+ }
186
+ });
37
187
  });
@@ -14,31 +14,27 @@ import { commands as modifierCommands } from "../domains/modifier/index.js";
14
14
  import { commands as ontologyCommands } from "../domains/ontology/index.js";
15
15
  import { commands as setupCommands } from "../domains/setup/index.js";
16
16
  import type { PragmaContext } from "../domains/shared/context.js";
17
- import { compilePackCommands } from "../domains/shared/stories/pack/index.js";
17
+ import {
18
+ compilePackCommands,
19
+ deriveReservedVerbs,
20
+ nounVerbFromPath,
21
+ } from "../domains/shared/stories/pack/index.js";
18
22
  import { commands as skillCommands } from "../domains/skill/index.js";
19
- import { commands as standardCommands } from "../domains/standard/index.js";
20
- import { commands as tierCommands } from "../domains/tier/index.js";
21
23
  import { commands as tokenCommands } from "../domains/token/index.js";
22
24
 
23
25
  /**
24
- * Collect all CLI command definitions from domain modules.
25
- *
26
- * Extracted from runCli so the completions server can import
27
- * the command list independently without running the full CLI pipeline.
26
+ * The built-in (hand-written domain) command surface, in emission order.
28
27
  *
29
- * @param ctx - The pragma context providing store, config, and global flags.
30
- * @returns The full array of command definitions for all domains.
28
+ * Excludes story-pack commands (bundled, config, or package). Exported so the
29
+ * reserved-verb derivation and its tests share the exact input production uses
30
+ * — the reserved map is built from built-ins only, never from packs.
31
31
  */
32
- export default function collectCommands(
33
- ctx: PragmaContext,
34
- ): CommandDefinition[] {
35
- const builtIn: CommandDefinition[] = [
32
+ export function builtInCommands(ctx: PragmaContext): CommandDefinition[] {
33
+ return [
36
34
  ...configCommands(ctx),
37
35
  ...createCommands(),
38
36
  ...setupCommands(),
39
- ...standardCommands(ctx),
40
37
  ...modifierCommands(ctx),
41
- ...tierCommands(ctx),
42
38
  ...tokenCommands(ctx),
43
39
  ...blockCommands(ctx),
44
40
  ...ontologyCommands(ctx),
@@ -51,10 +47,29 @@ export default function collectCommands(
51
47
  buildLlmCommand(ctx),
52
48
  buildCapabilitiesCommand(),
53
49
  ];
50
+ }
54
51
 
55
- // Story packs project onto the same surface; built-in nouns are reserved.
56
- const reservedNouns = new Set(
57
- builtIn.map((command) => command.path.at(0) ?? ""),
52
+ /**
53
+ * Collect all CLI command definitions: built-in domains plus story packs.
54
+ *
55
+ * Extracted from runCli so the completions server can import the command list
56
+ * independently without running the full CLI pipeline.
57
+ *
58
+ * Story packs (bundled transitional packs, plus config- and package-declared
59
+ * stories) project onto the same surface. Leaf read nouns reserve only the
60
+ * (noun, verb) pairs they own, so a pack can add a verb no built-in owns and,
61
+ * once a built-in leaf is deleted, its bundled pack takes over the freed noun;
62
+ * operational nouns (config, graph, setup, …) stay reserved wholesale.
63
+ *
64
+ * @param ctx - The pragma context providing store, config, and global flags.
65
+ * @returns The full array of command definitions for all domains.
66
+ */
67
+ export default function collectCommands(
68
+ ctx: PragmaContext,
69
+ ): CommandDefinition[] {
70
+ const builtIn = builtInCommands(ctx);
71
+ const reserved = deriveReservedVerbs(
72
+ builtIn.map((command) => nounVerbFromPath(command.path)),
58
73
  );
59
- return [...builtIn, ...compilePackCommands(ctx, reservedNouns)];
74
+ return [...builtIn, ...compilePackCommands(ctx, reserved)];
60
75
  }
@@ -36,9 +36,13 @@ describe("createProgram", () => {
36
36
  }
37
37
  )._events.beforeAllHelp({ command: program, write });
38
38
 
39
+ // Assert on single-chalk-span strings so the check is robust whether or
40
+ // not color is enabled (CI renders with color; ANSI resets would otherwise
41
+ // split a multi-word phrase like "Usage: pragma").
39
42
  expect(write).toHaveBeenCalledWith(
40
- expect.stringContaining("Usage: pragma <command> [options]"),
43
+ expect.stringContaining("For AI agents"),
41
44
  );
45
+ expect(write).toHaveBeenCalledWith(expect.stringContaining("Global flags"));
42
46
  });
43
47
 
44
48
  it("uses llm root help when llm is enabled", async () => {
@@ -1,7 +1,6 @@
1
1
  import {
2
2
  type CommandDefinition,
3
3
  detectRenderMode,
4
- formatHelp,
5
4
  formatLlmHelp,
6
5
  type HandleResultOptions,
7
6
  registerAll,
@@ -9,6 +8,7 @@ import {
9
8
  import { Command } from "commander";
10
9
  import { PROGRAM_DESCRIPTION, PROGRAM_NAME, VERSION } from "../constants.js";
11
10
  import type { PragmaContext } from "../domains/shared/context.js";
11
+ import formatRootHelp from "./rootHelp.js";
12
12
 
13
13
  /**
14
14
  * Build and configure the top-level Commander program.
@@ -57,7 +57,7 @@ export default function createProgram(
57
57
  if (ctx.globalFlags.llm) {
58
58
  return formatLlmHelp(PROGRAM_NAME, commands);
59
59
  }
60
- return formatHelp(PROGRAM_NAME, PROGRAM_DESCRIPTION, commands);
60
+ return formatRootHelp(PROGRAM_NAME, PROGRAM_DESCRIPTION, commands);
61
61
  });
62
62
 
63
63
  program.helpOption("-h, --help", "Show help");
@@ -0,0 +1,80 @@
1
+ import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
5
+ import ensureFirstRun from "./firstRun.js";
6
+
7
+ let xdg: string;
8
+ let savedXdg: string | undefined;
9
+ let lines: string[];
10
+
11
+ const write = (line: string) => {
12
+ lines.push(line);
13
+ };
14
+
15
+ beforeEach(() => {
16
+ savedXdg = process.env.XDG_CONFIG_HOME;
17
+ xdg = mkdtempSync(join(tmpdir(), "pragma-firstrun-"));
18
+ process.env.XDG_CONFIG_HOME = xdg;
19
+ lines = [];
20
+ });
21
+
22
+ afterEach(() => {
23
+ process.env.XDG_CONFIG_HOME = savedXdg;
24
+ rmSync(xdg, { recursive: true, force: true });
25
+ });
26
+
27
+ const configPath = () => join(xdg, "pragma", "config.json");
28
+
29
+ describe("ensureFirstRun", () => {
30
+ it("creates the global config with defaults and greets on first run", async () => {
31
+ await ensureFirstRun(write);
32
+
33
+ expect(existsSync(configPath())).toBe(true);
34
+ expect(JSON.parse(readFileSync(configPath(), "utf-8"))).toEqual({});
35
+
36
+ const text = lines.join("\n");
37
+ expect(text).toContain("pre-release pragma CLI");
38
+ expect(text).toContain("github.com/canonical/pragma");
39
+ expect(text).toContain(configPath());
40
+ expect(text).toContain("pragma.config.json");
41
+ });
42
+
43
+ it("is silent and leaves the config untouched on subsequent runs", async () => {
44
+ await ensureFirstRun(write);
45
+ lines = [];
46
+
47
+ await ensureFirstRun(write);
48
+
49
+ expect(lines).toEqual([]);
50
+ expect(readFileSync(configPath(), "utf-8")).toBe("{}\n");
51
+ });
52
+
53
+ it("never overwrites an existing config", async () => {
54
+ await ensureFirstRun(write);
55
+ const custom = '{ "channel": "experimental" }\n';
56
+ const { writeFileSync } = await import("node:fs");
57
+ writeFileSync(configPath(), custom);
58
+ lines = [];
59
+
60
+ await ensureFirstRun(write);
61
+
62
+ expect(readFileSync(configPath(), "utf-8")).toBe(custom);
63
+ expect(lines).toEqual([]);
64
+ });
65
+
66
+ it("degrades to a warning instead of throwing when the config cannot be created", async () => {
67
+ // Point XDG at a path whose parent is a FILE, so mkdir must fail.
68
+ const blocker = join(xdg, "blocker");
69
+ const { writeFileSync } = await import("node:fs");
70
+ writeFileSync(blocker, "not a directory");
71
+ process.env.XDG_CONFIG_HOME = join(blocker, "nested");
72
+
73
+ await expect(ensureFirstRun(write)).resolves.toBeUndefined();
74
+ // Exactly one warning line and no welcome banner: creation is attempted
75
+ // before the greeting, so a failure degrades to a single stderr warning.
76
+ expect(lines).toHaveLength(1);
77
+ expect(lines[0]).toContain("could not create the global pragma config");
78
+ expect(lines.join("\n")).not.toContain("pre-release pragma CLI");
79
+ });
80
+ });