@canonical/pragma-cli 0.33.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 (250) hide show
  1. package/README.md +7 -7
  2. package/dist/pragma +0 -0
  3. package/package.json +18 -15
  4. package/pragma.conf.ts +664 -0
  5. package/src/bin.ts +13 -4
  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.verb.ts +31 -13
  9. package/src/capabilities/capabilities/catalog.ts +18 -10
  10. package/src/capabilities/capabilities/hints.ts +13 -19
  11. package/src/capabilities/colophon/collectColophon.ts +34 -19
  12. package/src/capabilities/colophon/colophon.verb.ts +8 -4
  13. package/src/capabilities/colophon/types.ts +16 -7
  14. package/src/capabilities/config/collectConfigShow.test.ts +57 -0
  15. package/src/capabilities/config/collectConfigShow.ts +41 -4
  16. package/src/capabilities/config/fields.ts +0 -27
  17. package/src/capabilities/config/set.verb.ts +7 -3
  18. package/src/capabilities/config/show.render.test.ts +77 -5
  19. package/src/capabilities/config/show.render.ts +7 -12
  20. package/src/capabilities/config/show.verb.ts +4 -3
  21. package/src/capabilities/config/types.ts +8 -0
  22. package/src/capabilities/create/byteEquality.test.ts +78 -100
  23. package/src/capabilities/create/compiledCreate.subprocess.test.ts +52 -15
  24. package/src/capabilities/create/constants.ts +75 -0
  25. package/src/capabilities/create/create.mcp.test.ts +2 -2
  26. package/src/capabilities/create/create.test.ts +162 -15
  27. package/src/capabilities/create/create.verb.ts +140 -69
  28. package/src/capabilities/create/index.ts +2 -10
  29. package/src/capabilities/create/pickGenerator.ts +34 -28
  30. package/src/capabilities/create/templates.embedded.generated.ts +4 -29
  31. package/src/capabilities/create/types.ts +10 -6
  32. package/src/capabilities/{standard/parity.test.ts → distribution.parity.test.ts} +24 -14
  33. package/src/capabilities/distribution.test.ts +120 -0
  34. package/src/capabilities/distribution.ts +59 -0
  35. package/src/capabilities/doctor/checks/checkConfigFile.ts +6 -5
  36. package/src/capabilities/doctor/checks/checkKeStore.ts +2 -2
  37. package/src/capabilities/doctor/checks/checkMcpConfigured.ts +7 -6
  38. package/src/capabilities/doctor/checks/checkPackageRefs.ts +79 -45
  39. package/src/capabilities/doctor/checks/checkPragmaVersion.ts +3 -2
  40. package/src/capabilities/doctor/checks/checkShellCompletions.test.ts +61 -11
  41. package/src/capabilities/doctor/checks/checkShellCompletions.ts +63 -23
  42. package/src/capabilities/doctor/checks/checkSkillsSymlinked.ts +3 -2
  43. package/src/capabilities/doctor/doctor.render.test.ts +6 -6
  44. package/src/capabilities/doctor/doctor.render.ts +2 -1
  45. package/src/capabilities/doctor/doctor.test.ts +37 -8
  46. package/src/capabilities/doctor/doctor.verb.ts +6 -2
  47. package/src/capabilities/doctor/runChecks.ts +5 -5
  48. package/src/capabilities/doctor/types.ts +1 -1
  49. package/src/capabilities/emptyRecovery.test.ts +32 -15
  50. package/src/capabilities/graph/inspect.verb.ts +3 -2
  51. package/src/capabilities/graph/query.verb.ts +3 -2
  52. package/src/capabilities/graph/runQuery.ts +8 -5
  53. package/src/capabilities/index.ts +22 -18
  54. package/src/capabilities/info/collectInfo.ts +10 -6
  55. package/src/capabilities/info/info.render.ts +3 -2
  56. package/src/capabilities/info/info.verb.ts +3 -2
  57. package/src/capabilities/lazy.test.ts +93 -2
  58. package/src/capabilities/ontology/verbs.ts +21 -21
  59. package/src/capabilities/prompt/prompt.test.ts +12 -6
  60. package/src/capabilities/prompt/verbs.ts +24 -11
  61. package/src/capabilities/resources/provider.ts +30 -11
  62. package/src/capabilities/resources/resources.test.ts +85 -29
  63. package/src/capabilities/setup/operations/setupGenerator.ts +16 -9
  64. package/src/capabilities/setup/operations/setupMcp.ts +2 -2
  65. package/src/capabilities/setup/operations/setupSkills.ts +3 -3
  66. package/src/capabilities/setup/setup.verb.ts +12 -5
  67. package/src/capabilities/setup/shell.ts +29 -3
  68. package/src/capabilities/skill/discover.ts +11 -5
  69. package/src/capabilities/skill/render.ts +4 -5
  70. package/src/capabilities/skill/verbs.ts +7 -7
  71. package/src/capabilities/sources/collectStatus.ts +55 -50
  72. package/src/capabilities/sources/index.ts +1 -1
  73. package/src/capabilities/sources/installSkills.ts +2 -2
  74. package/src/capabilities/sources/runUpdate.ts +113 -122
  75. package/src/capabilities/sources/sources.test.ts +111 -117
  76. package/src/capabilities/sources/status.render.test.ts +53 -24
  77. package/src/capabilities/sources/status.render.ts +24 -29
  78. package/src/capabilities/sources/status.verb.ts +15 -8
  79. package/src/capabilities/sources/types.ts +18 -25
  80. package/src/capabilities/sources/update.render.ts +9 -2
  81. package/src/capabilities/sources/update.verb.ts +12 -23
  82. package/src/capabilities/surface.test.ts +33 -10
  83. package/src/capabilities/tier.parity.test.ts +144 -0
  84. package/src/capabilities/upgrade/upgrade.verb.ts +7 -3
  85. package/src/constants.test.ts +8 -6
  86. package/src/constants.ts +47 -21
  87. package/src/identity.test.ts +333 -0
  88. package/src/index.ts +2 -2
  89. package/src/kernel/completion/__snapshots__/emitScripts.test.ts.snap +5 -2
  90. package/src/kernel/completion/emitScripts.test.ts +1 -1
  91. package/src/kernel/completion/emitScripts.ts +4 -2
  92. package/src/kernel/completion/entitySource.test.ts +185 -41
  93. package/src/kernel/completion/entitySource.ts +117 -107
  94. package/src/kernel/completion/model.test.ts +1 -1
  95. package/src/kernel/completion/model.ts +11 -0
  96. package/src/kernel/completion/safety.test.ts +82 -36
  97. package/src/kernel/completion/shellDrive.test.ts +592 -0
  98. package/src/kernel/completion/templates/fish.ts +28 -6
  99. package/src/kernel/completion/types.ts +3 -3
  100. package/src/kernel/config/__snapshots__/firstRun.test.ts.snap +1 -1
  101. package/src/kernel/config/defaults.test.ts +85 -0
  102. package/src/kernel/config/defaults.ts +12 -23
  103. package/src/kernel/config/evaluateProjectConfig.ts +2 -3
  104. package/src/kernel/config/findProjectConfig.ts +4 -3
  105. package/src/kernel/config/firstRun.ts +12 -5
  106. package/src/kernel/config/globalConfig.ts +2 -2
  107. package/src/kernel/config/index.ts +4 -5
  108. package/src/kernel/config/paths.ts +17 -17
  109. package/src/kernel/config/readConfig.test.ts +242 -6
  110. package/src/kernel/config/readConfig.ts +8 -8
  111. package/src/kernel/config/schema.test.ts +50 -0
  112. package/src/kernel/config/schema.ts +97 -10
  113. package/src/kernel/config/types.ts +73 -19
  114. package/src/kernel/copy.test.ts +453 -0
  115. package/src/kernel/error/PragmaError.test.ts +1 -1
  116. package/src/kernel/error/constants.ts +3 -2
  117. package/src/kernel/error/index.ts +1 -1
  118. package/src/kernel/error/recovery.test.ts +22 -45
  119. package/src/kernel/error/recovery.ts +22 -27
  120. package/src/kernel/error/types.ts +5 -4
  121. package/src/kernel/packs/collect.test.ts +195 -11
  122. package/src/kernel/packs/collect.ts +213 -58
  123. package/src/kernel/packs/compile.test.ts +163 -7
  124. package/src/kernel/packs/compile.ts +15 -12
  125. package/src/kernel/packs/disclosure.test.ts +5 -2
  126. package/src/kernel/packs/graphql/fetchGraphqlLookup.ts +11 -10
  127. package/src/kernel/packs/graphqlSource.test.ts +6 -3
  128. package/src/kernel/packs/iri.ts +5 -7
  129. package/src/kernel/packs/renderPack.ts +5 -3
  130. package/src/kernel/packs/resolveEntity.ts +21 -17
  131. package/src/kernel/packs/runBodies.ts +15 -14
  132. package/src/kernel/packs/schema.ts +96 -10
  133. package/src/kernel/packs/sparql/runSelect.test.ts +104 -10
  134. package/src/kernel/packs/sparql/runSelect.ts +73 -34
  135. package/src/kernel/packs/toolDescription.test.ts +10 -2
  136. package/src/kernel/packs/types.ts +39 -4
  137. package/src/kernel/packs/uniqueness.ts +4 -4
  138. package/src/kernel/project/cli/buildProgram.test.ts +9 -4
  139. package/src/kernel/project/cli/dispatch.test.ts +67 -0
  140. package/src/kernel/project/cli/dispatch.ts +34 -12
  141. package/src/kernel/project/cli/helpFormat.ts +1 -1
  142. package/src/kernel/project/cli/rootHelp.ts +105 -105
  143. package/src/kernel/project/mcp/buildServer.ts +11 -6
  144. package/src/kernel/project/mcp/instructions.ts +11 -6
  145. package/src/kernel/project/mcp/parity.test.ts +39 -1
  146. package/src/kernel/project/mcp/prompts/prompts.test.ts +120 -5
  147. package/src/kernel/project/mcp/prompts/provider.ts +22 -8
  148. package/src/kernel/project/mcp/prompts/source.ts +68 -67
  149. package/src/kernel/project/mcp/registerVerb.ts +25 -6
  150. package/src/kernel/project/mcp/serve.ts +11 -3
  151. package/src/kernel/project/mcp/toolDescriptions.test.ts +6 -2
  152. package/src/kernel/render/disclosure.ts +4 -2
  153. package/src/kernel/render/prefixes.test.ts +36 -0
  154. package/src/kernel/render/prefixes.ts +40 -13
  155. package/src/kernel/render/renderers.test.ts +1 -1
  156. package/src/kernel/runtime/graphpack/build.ts +29 -3
  157. package/src/kernel/runtime/graphpack/buildIndex.test.ts +46 -0
  158. package/src/kernel/runtime/graphpack/buildIndex.ts +52 -17
  159. package/src/kernel/runtime/graphpack/embedded/pack.generated.ts +5 -5
  160. package/src/kernel/runtime/graphpack/embedded/pack.index.generated.ts +3 -3
  161. package/src/kernel/runtime/graphpack/embedded/pack.stories.generated.ts +3 -0
  162. package/src/kernel/runtime/graphpack/embedded.ts +37 -11
  163. package/src/kernel/runtime/graphpack/graphpack.test.ts +205 -5
  164. package/src/kernel/runtime/graphpack/hash.ts +3 -2
  165. package/src/kernel/runtime/graphpack/index.ts +1 -1
  166. package/src/kernel/runtime/graphpack/manifest.ts +114 -12
  167. package/src/kernel/runtime/graphpack/read.ts +4 -9
  168. package/src/kernel/runtime/graphpack/schemas.ts +74 -0
  169. package/src/kernel/runtime/graphpack/stories.ts +84 -0
  170. package/src/kernel/runtime/graphpack/types.ts +61 -48
  171. package/src/kernel/runtime/paths.ts +64 -15
  172. package/src/kernel/runtime/probe.test.ts +8 -4
  173. package/src/kernel/runtime/readEntity.ts +6 -5
  174. package/src/kernel/runtime/refs/gitOps.ts +1 -19
  175. package/src/kernel/runtime/refs/parseRef.ts +7 -7
  176. package/src/kernel/runtime/refs/resolve.ts +78 -62
  177. package/src/kernel/runtime/resolveSources.test.ts +63 -24
  178. package/src/kernel/runtime/resolveSources.ts +36 -25
  179. package/src/kernel/runtime/storeReadiness.ts +9 -8
  180. package/src/kernel/runtime/storeless.test.ts +1 -1
  181. package/src/kernel/runtime/wasmEmbed.test.ts +22 -19
  182. package/src/kernel/spec/emitReference.test.ts +3 -3
  183. package/src/kernel/spec/emitReference.ts +144 -11
  184. package/src/kernel/spec/emitSurface.ts +9 -5
  185. package/src/kernel/spec/surfaceConformance.test.ts +2 -2
  186. package/src/kernel/spec/surfaceConformance.ts +8 -0
  187. package/src/kernel/spec/types.ts +29 -9
  188. package/src/kernel/spec/validate.ts +2 -1
  189. package/src/kernel/vocabulary.test.ts +164 -0
  190. package/src/kernel/vocabulary.ts +146 -0
  191. package/src/testing/PARITY_GAPS.ts +8 -2
  192. package/src/testing/behavioral/README.md +3 -3
  193. package/src/testing/behavioral/completion.test.ts +19 -6
  194. package/src/testing/behavioral/errorMatrix.mcp.test.ts +4 -3
  195. package/src/testing/behavioral/errorMatrix.storeless.test.ts +1 -1
  196. package/src/testing/behavioral/exitCodes.e2e.test.ts +6 -5
  197. package/src/testing/behavioral/harness.foundation.test.ts +14 -5
  198. package/src/testing/behavioral/journeys.cli.test.ts +116 -75
  199. package/src/testing/behavioral/journeys.defaultPack.test.ts +82 -176
  200. package/src/testing/behavioral/journeys.firstInstall.test.ts +167 -0
  201. package/src/testing/behavioral/journeys.mcpLifecycle.mcp.test.ts +9 -6
  202. package/src/testing/behavioral/journeys.packageStories.test.ts +188 -0
  203. package/src/testing/behavioral/liveReadSurface.ts +4 -3
  204. package/src/testing/behavioral/mcpSurface.test.ts +9 -4
  205. package/src/testing/behavioral/staleCommands.test.ts +10 -0
  206. package/src/testing/eval/__snapshots__/eval.test.ts.snap +7 -7
  207. package/src/testing/eval/cases/assembly.ts +13 -6
  208. package/src/testing/eval/cases/effectful.ts +25 -13
  209. package/src/testing/eval/cases/readNouns.ts +22 -20
  210. package/src/testing/eval/cases/stable.ts +5 -1
  211. package/src/testing/eval/eval.test.ts +7 -1
  212. package/src/testing/fixtures/fixtureCapability.ts +45 -1
  213. package/src/testing/fixtures/graph/canonical.ts +6 -3
  214. package/src/testing/fixtures/graph/defaultPack.ts +6 -5
  215. package/src/testing/fixtures/graph/owlExport.ts +7 -5
  216. package/src/testing/helpers/fixtureGraph.ts +33 -16
  217. package/src/testing/helpers/packRuntime.ts +2 -3
  218. package/src/testing/helpers/projectCli.ts +2 -1
  219. package/src/testing/helpers/projectMcp.ts +6 -0
  220. package/src/testing/perf/budgets.test.ts +4 -0
  221. package/src/testing/perf/budgets.ts +22 -7
  222. package/src/testing/perf/globalSetup.ts +121 -6
  223. package/src/types.ts +2 -2
  224. package/src/capabilities/block/blockList.test.ts +0 -79
  225. package/src/capabilities/block/blockList.verb.ts +0 -163
  226. package/src/capabilities/block/index.ts +0 -25
  227. package/src/capabilities/block/pack.ts +0 -166
  228. package/src/capabilities/block/parity.test.ts +0 -187
  229. package/src/capabilities/block/tierChain.ts +0 -78
  230. package/src/capabilities/colophon/pragmaColophon.ts +0 -66
  231. package/src/capabilities/modifier/index.ts +0 -15
  232. package/src/capabilities/modifier/pack.ts +0 -64
  233. package/src/capabilities/standard/index.ts +0 -18
  234. package/src/capabilities/standard/pack.ts +0 -128
  235. package/src/capabilities/tier/index.ts +0 -19
  236. package/src/capabilities/tier/lookup.render.test.ts +0 -72
  237. package/src/capabilities/tier/lookup.render.ts +0 -59
  238. package/src/capabilities/tier/lookup.test.ts +0 -83
  239. package/src/capabilities/tier/lookup.verb.ts +0 -44
  240. package/src/capabilities/tier/pack.ts +0 -28
  241. package/src/capabilities/tier/runLookup.ts +0 -87
  242. package/src/capabilities/token/addConfig.render.ts +0 -27
  243. package/src/capabilities/token/addConfig.test.ts +0 -62
  244. package/src/capabilities/token/addConfig.verb.ts +0 -40
  245. package/src/capabilities/token/index.ts +0 -19
  246. package/src/capabilities/token/pack.ts +0 -58
  247. package/src/capabilities/token/runAddConfig.ts +0 -95
  248. package/src/kernel/completion/bashDrive.test.ts +0 -73
  249. package/src/kernel/runtime/graphpack/embedded/sample.ttl +0 -39
  250. package/src/kernel/runtime/lock.ts +0 -90
@@ -1,11 +1,18 @@
1
+ import { existsSync, mkdtempSync, readdirSync, writeFileSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
1
4
  import { fail, succeed } from "@canonical/task";
2
5
  import { afterEach, describe, expect, it, vi } from "vitest";
6
+ import { fixturePreviewModule } from "../../../testing/fixtures/fixtureCapability.js";
3
7
  import { PragmaError } from "../../error/PragmaError.js";
4
8
  import { bootRuntime } from "../../runtime/boot.js";
5
9
  import type { GlobalFlags } from "../../runtime/types.js";
6
10
  import type { ParamSpec, VerbSpec } from "../../spec/types.js";
7
11
  import { dispatch, executeVerb, extractParams } from "./dispatch.js";
8
12
 
13
+ /** The read-then-write mutation the honest-preview guards drive. */
14
+ const graft = fixturePreviewModule.verbs[0] as VerbSpec;
15
+
9
16
  const PLAIN: GlobalFlags = {
10
17
  llm: false,
11
18
  autoLlm: false,
@@ -233,6 +240,66 @@ describe("dispatch — errors", () => {
233
240
  });
234
241
  });
235
242
 
243
+ describe("--dry-run is honest (PRA-104)", () => {
244
+ const savedExit = process.exitCode;
245
+ afterEach(() => {
246
+ process.exitCode = savedExit;
247
+ });
248
+
249
+ /** Capture stderr for one dispatch. */
250
+ const dispatchCapturing = async (
251
+ positionals: readonly string[],
252
+ opts: Record<string, unknown>,
253
+ ): Promise<string> => {
254
+ const errs: string[] = [];
255
+ const spy = vi
256
+ .spyOn(process.stderr, "write")
257
+ .mockImplementation((chunk: string | Uint8Array) => {
258
+ errs.push(String(chunk));
259
+ return true;
260
+ });
261
+ process.exitCode = 0;
262
+ try {
263
+ await dispatch(graft, positionals, opts, PLAIN);
264
+ } finally {
265
+ spy.mockRestore();
266
+ }
267
+ return errs.join("");
268
+ };
269
+
270
+ it("exits NONZERO when the mutation's first read would fail for real", async () => {
271
+ // The structural false positive: this same call used to print a full plan
272
+ // and exit 0, while the run without --dry-run died on the missing file.
273
+ const missing = join(mkdtempSync(join(tmpdir(), "pragma-graft-")), "gone");
274
+ const out = await dispatchCapturing([missing], { dryRun: true });
275
+ expect(process.exitCode).toBe(1);
276
+ expect(out).toContain(missing);
277
+ expect(existsSync(`${missing}.grafted`)).toBe(false);
278
+ });
279
+
280
+ it("still plans, exits 0, and writes nothing when the read would succeed", async () => {
281
+ const dir = mkdtempSync(join(tmpdir(), "pragma-graft-"));
282
+ const source = join(dir, "present.txt");
283
+ writeFileSync(source, "content\n");
284
+ const outs: string[] = [];
285
+ const spy = vi
286
+ .spyOn(process.stdout, "write")
287
+ .mockImplementation((chunk: string | Uint8Array) => {
288
+ outs.push(String(chunk));
289
+ return true;
290
+ });
291
+ process.exitCode = 0;
292
+ try {
293
+ await dispatch(graft, [source], { dryRun: true }, PLAIN);
294
+ } finally {
295
+ spy.mockRestore();
296
+ }
297
+ expect(process.exitCode).toBe(0);
298
+ expect(outs.join("")).toContain(`Write file: ${source}.grafted`);
299
+ expect(readdirSync(dir)).toEqual(["present.txt"]);
300
+ });
301
+ });
302
+
236
303
  describe("executeVerb — interactivity gate (H3)", () => {
237
304
  const setTTY = (
238
305
  stream: NodeJS.ReadStream | NodeJS.WriteStream,
@@ -11,8 +11,8 @@
11
11
  * exit code.
12
12
  */
13
13
 
14
- import { describeEffect, dryRun, type Task } from "@canonical/task";
15
- import { runTask, runUndo } from "@canonical/task/node";
14
+ import { describeEffect, type Task } from "@canonical/task";
15
+ import { runPreview, runTask, runUndo } from "@canonical/task/node";
16
16
  import {
17
17
  asPragmaError,
18
18
  CANCELLED_MESSAGE,
@@ -221,8 +221,11 @@ export async function executeVerb(
221
221
  // Also hand it the interaction context so an interactive verb can pick its
222
222
  // prompt strategy. The verb's `run` sets `mutationRuntime.exec` (the runner
223
223
  // options) as its last act; the projector reads it back on the real-run
224
- // branch only — the dry-run/undo branches stay handler-free (they mock
225
- // prompts), so `--dry-run`/`--undo` are unchanged by this seam.
224
+ // branch AND on the dry-run branch, which takes `cwd` (so the preview reads
225
+ // the tree the run would write into) and `onEffectStart` (so the stamping
226
+ // transform runs, and planned byte counts match written ones). It never
227
+ // takes the prompt handler: a preview auto-answers prompts and so can never
228
+ // block on input. `--undo` stays handler-free and untouched by this seam.
226
229
  const controller = new AbortController();
227
230
  const interaction: InteractionRuntime = {
228
231
  // Gate on STDERR (H3): the Ink wizard renders to stderr and reads stdin,
@@ -252,14 +255,33 @@ export async function executeVerb(
252
255
  | Promise<Task<unknown>>,
253
256
  );
254
257
  if (mutation.dryRun) {
255
- // A plan is the effects a mutation WOULD apply — a `Prompt` is not one, so
256
- // the interactive confirm gate / answer prompts never clutter the preview.
257
- return renderPlan(
258
- flags,
259
- dryRun(task)
260
- .effects.filter((effect) => effect._tag !== "Prompt")
261
- .map(describeEffect),
262
- );
258
+ // The HONEST preview (PR7): reads hit the real filesystem, writes are
259
+ // recorded and never executed. A mutation whose real run would die on its
260
+ // first template read now fails HERE too, so `--dry-run` exits nonzero
261
+ // exactly when the run would — the plan is a prediction, not a wish.
262
+ // `exec.cwd` is the same write root the real run resolves paths against,
263
+ // and `exec.onEffectStart` carries summon's stamping transform, so the
264
+ // planned byte counts are the bytes the run would actually write.
265
+ const previewExec = mutationRuntime.exec ?? {};
266
+ try {
267
+ const { effects } = await runPreview(task, {
268
+ cwd: previewExec.cwd,
269
+ onEffectStart: previewExec.onEffectStart,
270
+ onLog: logToStderr,
271
+ });
272
+ // A plan is the effects a mutation WOULD apply — a `Prompt` is not one,
273
+ // so the interactive confirm gate / answer prompts never clutter it.
274
+ return renderPlan(
275
+ flags,
276
+ effects
277
+ .filter((effect) => effect._tag !== "Prompt")
278
+ .map(describeEffect),
279
+ );
280
+ } finally {
281
+ // A verb that mounted an interactive session before returning its Task
282
+ // must be torn down on this branch too, exactly as on the real run.
283
+ await previewExec.dispose?.();
284
+ }
263
285
  }
264
286
  if (mutation.undo) {
265
287
  const { undoCount } = await runUndo(task, { onLog: logToStderr });
@@ -17,7 +17,7 @@ const INDENT = " ";
17
17
  /** Two-space gap between a term column and its description. */
18
18
  const GAP = " ";
19
19
 
20
- /** A bold section header (`Flags`, `Verbs`, `Explore the design system`). */
20
+ /** A bold section header (`Flags`, `Verbs`, `Set up & maintain`). */
21
21
  export function helpHeading(text: string): string {
22
22
  return chalk.bold(text);
23
23
  }
@@ -1,12 +1,13 @@
1
1
  /**
2
- * Curated root `--help` for `pragma`.
2
+ * The curated root `--help` page.
3
3
  *
4
- * The front door is hand-curated, not auto-generated: nouns are grouped into
5
- * task-oriented sections with real one-line summaries, and each section is
6
- * reconciled against the *live* nouns (from the registered verbs) so a noun
7
- * that is not built yet is dropped and an uncatalogued one still surfaces under
8
- * "Other" rather than vanishing. Ported from the v1 root help, retargeted at
9
- * the grammar and the v2 noun set (no `llm`/`graphql`/`trace`/`tokens`).
4
+ * The kernel curates the nouns it ships — and only those. It names no domain:
5
+ * the nouns it does NOT curate lead the page untitled (the header one line up
6
+ * already carries the distribution's blurb) and describe themselves through
7
+ * their own verbs, so the front door follows the content instead of naming it.
8
+ * Every section is reconciled against the *live* nouns (from the registered
9
+ * verbs), so a noun that is not built yet is dropped and one the kernel has
10
+ * never heard of still surfaces.
10
11
  */
11
12
 
12
13
  import type { VerbSpec } from "../../spec/types.js";
@@ -24,88 +25,86 @@ interface NounSummary {
24
25
  }
25
26
 
26
27
  interface HelpGroup {
27
- readonly title: string;
28
+ /**
29
+ * Absent for the leading uncurated group: its rows sit directly under the
30
+ * usage line, since repeating the header's blurb as a heading three lines
31
+ * below it says the same thing twice.
32
+ */
33
+ readonly title?: string;
28
34
  readonly nouns: readonly NounSummary[];
29
35
  }
30
36
 
31
37
  /**
32
- * Task-oriented grouping with curated summaries. Order is intentional: what
33
- * people reach for most (exploring, generating) comes first; agent tooling
34
- * comes last.
38
+ * The kernel's own nouns, grouped by task with curated summaries. Order is
39
+ * intentional: what people reach for most comes first; agent tooling comes last.
40
+ *
41
+ * @param programName - The CLI binary name (the distribution's `name`).
42
+ * @returns The task-oriented groups, in display order.
35
43
  */
36
- const HELP_GROUPS: readonly HelpGroup[] = [
37
- {
38
- title: "Explore the design system",
39
- nouns: [
40
- {
41
- noun: "block",
42
- summary: "Inspect components & patterns and their anatomy",
43
- },
44
- { noun: "token", summary: "Look up design tokens and their values" },
45
- { noun: "modifier", summary: "Inspect modifiers and modifier families" },
46
- { noun: "standard", summary: "Browse code standards and categories" },
47
- {
48
- noun: "ontology",
49
- summary: "Explore the loaded ontologies (classes, properties)",
50
- },
51
- { noun: "tier", summary: "List the design-system tiers" },
52
- ],
53
- },
54
- {
55
- title: "Generate code",
56
- nouns: [
57
- { noun: "create", summary: "Scaffold components, packages, and apps" },
58
- ],
59
- },
60
- {
61
- title: "Query & serve the graph",
62
- nouns: [
63
- {
64
- noun: "graph",
65
- summary: "Run SPARQL queries or inspect a URI directly",
66
- },
67
- ],
68
- },
69
- {
70
- title: "Set up & maintain",
71
- nouns: [
72
- { noun: "doctor", summary: "Check environment health" },
73
- {
74
- noun: "setup",
75
- summary: "Configure MCP, skills, completions, and the LSP",
76
- },
77
- {
78
- noun: "sources",
79
- summary: "Build and refresh the local store from packages",
80
- },
81
- { noun: "config", summary: "Read and write pragma configuration" },
82
- { noun: "info", summary: "Show version, config, and update status" },
83
- {
84
- noun: "upgrade",
85
- summary: "Upgrade the pragma CLI to the latest version",
86
- },
87
- ],
88
- },
89
- {
90
- title: "For AI agents",
91
- nouns: [
92
- {
93
- noun: "capabilities",
94
- summary: "Discover conventions, tools, and the discovery sequence",
95
- },
96
- {
97
- noun: "colophon",
98
- summary: "Read how pragma and the active domain are made",
99
- },
100
- {
101
- noun: "skill",
102
- summary: "Browse agent skills from design-system packages",
103
- },
104
- { noun: "prompt", summary: "Browse reusable prompt templates" },
105
- { noun: "mcp", summary: "Start the MCP server over stdio" },
106
- ],
107
- },
108
- ];
44
+ function buildKernelGroups(programName: string): readonly HelpGroup[] {
45
+ return [
46
+ {
47
+ title: "Generate code",
48
+ nouns: [
49
+ { noun: "create", summary: "Scaffold components, packages, and apps" },
50
+ ],
51
+ },
52
+ {
53
+ title: "Query & serve the graph",
54
+ nouns: [
55
+ {
56
+ noun: "graph",
57
+ summary: "Run SPARQL queries or inspect a URI directly",
58
+ },
59
+ {
60
+ noun: "ontology",
61
+ summary: "Explore the loaded ontologies (classes, properties)",
62
+ },
63
+ ],
64
+ },
65
+ {
66
+ title: "Set up & maintain",
67
+ nouns: [
68
+ { noun: "doctor", summary: "Check environment health" },
69
+ {
70
+ noun: "setup",
71
+ summary: "Configure MCP, skills, completions, and the LSP",
72
+ },
73
+ {
74
+ noun: "sources",
75
+ summary: "Build and refresh the local store from packs",
76
+ },
77
+ {
78
+ noun: "config",
79
+ summary: `Read and write ${programName} configuration`,
80
+ },
81
+ { noun: "info", summary: "Show version, config, and update status" },
82
+ {
83
+ noun: "upgrade",
84
+ summary: `Upgrade the ${programName} CLI to the latest version`,
85
+ },
86
+ ],
87
+ },
88
+ {
89
+ title: "For AI agents",
90
+ nouns: [
91
+ {
92
+ noun: "capabilities",
93
+ summary: "Discover conventions, tools, and the discovery sequence",
94
+ },
95
+ {
96
+ noun: "colophon",
97
+ summary: `Read how ${programName} and the active domain are made`,
98
+ },
99
+ { noun: "skill", summary: "Browse agent skills from the active packs" },
100
+ { noun: "prompt", summary: "Browse reusable prompt templates" },
101
+ // The one noun with no verb to speak for it (the bin special-cases it),
102
+ // so its summary is hand-written rather than derived.
103
+ { noun: "mcp", summary: "Start the MCP server over stdio" },
104
+ ],
105
+ },
106
+ ];
107
+ }
109
108
 
110
109
  /** All distinct, non-hidden top-level nouns present in the registered verbs. */
111
110
  function nounsFrom(verbs: readonly VerbSpec[]): Set<string> {
@@ -117,16 +116,21 @@ function nounsFrom(verbs: readonly VerbSpec[]): Set<string> {
117
116
  return nouns;
118
117
  }
119
118
 
120
- /** The summary to show for an uncatalogued but live noun. */
121
- function fallbackSummary(noun: string, verbs: readonly VerbSpec[]): string {
119
+ /**
120
+ * Summarise an uncurated noun from its first live verb — which is the pack's own
121
+ * `description` whenever the pack ships a `list` (`compilePack` compiles that
122
+ * verb first). The terminal period a pack sentence carries is dropped so the
123
+ * column reads as one voice with the kernel's own fragments.
124
+ */
125
+ function summarizeNoun(noun: string, verbs: readonly VerbSpec[]): string {
122
126
  const first = verbs.find((v) => v.path[0] === noun && !v.hidden);
123
- return first?.summary ?? `${noun} commands`;
127
+ return (first?.summary ?? `${noun} commands`).replace(/\.$/, "");
124
128
  }
125
129
 
126
130
  /**
127
131
  * Build the curated root help string.
128
132
  *
129
- * @param programName - The CLI binary name (`pragma`).
133
+ * @param programName - The CLI binary name (the distribution's `name`).
130
134
  * @param description - The program description shown in the header.
131
135
  * @param verbs - All registered verbs, used to derive the live noun set.
132
136
  * @returns The formatted, colorized help text.
@@ -140,27 +144,23 @@ export function formatRootHelp(
140
144
  // `mcp` is served by the bin's special-case, not a projected verb, but is
141
145
  // always available — surface it so the front door is complete.
142
146
  present.add("mcp");
143
- const catalogued = new Set(
144
- HELP_GROUPS.flatMap((g) => g.nouns.map((n) => n.noun)),
145
- );
147
+ const kernel = buildKernelGroups(programName);
148
+ const curated = new Set(kernel.flatMap((g) => g.nouns.map((n) => n.noun)));
146
149
 
147
- const uncatalogued = [...present].filter((n) => !catalogued.has(n)).sort();
148
150
  const groups: HelpGroup[] = [
149
- ...HELP_GROUPS.map((g) => ({
151
+ // Everything the kernel's curated table does not claim — in the shipped
152
+ // binary the bundled domain packs, in a fork whatever it ships. Untitled
153
+ // and first, so the domain leads the page without the kernel naming it.
154
+ {
155
+ nouns: [...present]
156
+ .filter((n) => !curated.has(n))
157
+ .sort()
158
+ .map((noun) => ({ noun, summary: summarizeNoun(noun, verbs) })),
159
+ },
160
+ ...kernel.map((g) => ({
150
161
  ...g,
151
162
  nouns: g.nouns.filter((n) => present.has(n.noun)),
152
163
  })),
153
- ...(uncatalogued.length > 0
154
- ? [
155
- {
156
- title: "Other",
157
- nouns: uncatalogued.map((noun) => ({
158
- noun,
159
- summary: fallbackSummary(noun, verbs),
160
- })),
161
- },
162
- ]
163
- : []),
164
164
  ].filter((g) => g.nouns.length > 0);
165
165
 
166
166
  // One column width across ALL groups so the noun column aligns section to
@@ -180,7 +180,7 @@ export function formatRootHelp(
180
180
  ];
181
181
 
182
182
  for (const group of groups) {
183
- lines.push(helpHeading(group.title));
183
+ if (group.title) lines.push(helpHeading(group.title));
184
184
  lines.push(
185
185
  ...helpColumns(
186
186
  group.nouns.map((n) => [n.noun, n.summary] as const),
@@ -1,12 +1,17 @@
1
1
  /**
2
2
  * Build the MCP server from the grammar.
3
3
  *
4
- * Constructs an {@link McpServer} under the stable `pragma` identity (matching
5
- * the CLI bin, so existing agent wiring resolves it), then registers every
6
- * exposed verb as a tool and each module's optional `mcpResources` surface (the
7
- * graph resource browser, wired below via `module.mcpResources?.register`). One
8
- * runtime is booted for the server's lifetime and shared by every tool; the
9
- * store stays lazy, booting only when a resource read needs it.
4
+ * Constructs an {@link McpServer} whose wire identity PROJECTS from the
5
+ * distribution: `serverInfo.name` is the declared distribution name (the same
6
+ * projection that names the CLI bin, so existing agent wiring resolves it) and
7
+ * `serverInfo.version` the package version — a fork's server introduces itself
8
+ * under the fork's own name with no code change. The covenant records the rule
9
+ * (`surface.v2.json` `mcpSurface.serverInfo`) and `identity.test.ts` pins it on
10
+ * the wire from a fork's config. Then registers every exposed verb as a tool
11
+ * and each module's optional `mcpResources` surface (the graph resource
12
+ * browser, wired below via `module.mcpResources?.register`). One runtime is
13
+ * booted for the server's lifetime and shared by every tool; the store stays
14
+ * lazy, booting only when a resource read needs it.
10
15
  */
11
16
 
12
17
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
@@ -4,7 +4,8 @@
4
4
  *
5
5
  * It carries the same content the retired `llm` tool front-loaded: the
6
6
  * conventions + the discovery sequence. To guarantee it never diverges from the
7
- * `capabilities` tool, it derives BOTH from the SAME source — `CONVENTIONS` and
7
+ * `capabilities` tool, the WHOLE orientation — the opening line included —
8
+ * derives from the SAME source: `CONVENTIONS` and
8
9
  * `buildDiscoverySequence` in `capabilities/capabilities/catalog.ts`. Live
9
10
  * numbers (tier/channel/entity counts) are deliberately DROPPED (they would need
10
11
  * a store boot at handshake); agents fetch those via `info`/`config show`/
@@ -15,8 +16,8 @@
15
16
  import {
16
17
  buildDiscoverySequence,
17
18
  CONVENTIONS,
18
- liveTools,
19
19
  } from "../../../capabilities/capabilities/catalog.js";
20
+ import { emitSurface } from "../../spec/emitSurface.js";
20
21
  import type { CapabilityModule } from "../../spec/types.js";
21
22
 
22
23
  /** Hard ceiling on the instructions length (asserted by the protected test). */
@@ -31,18 +32,22 @@ export const INSTRUCTIONS_MAX_CHARS = 1500;
31
32
  export function buildInstructions(
32
33
  modules: readonly CapabilityModule[],
33
34
  ): string {
34
- const discovery = buildDiscoverySequence(liveTools(modules));
35
- const steps = discovery
35
+ // Tools AND resource templates come from the one emitted surface: the
36
+ // `<scheme>:{+uri}` template is declared by the module that serves it and
37
+ // frozen in the covenant, so quoting it here rather than deriving it would be
38
+ // a second copy of a string the kernel does not own.
39
+ const { tools, resources } = emitSurface(modules).mcpSurface;
40
+ const steps = buildDiscoverySequence(tools)
36
41
  .map((stage) => `${stage.stage}. ${stage.tool} — ${stage.purpose}`)
37
42
  .join("\n");
43
+ const templates = resources.map((template) => `\`${template}\``).join(", ");
38
44
 
39
45
  return [
40
- "pragma is a CLI and MCP server over a design-system knowledge graph.",
41
46
  `${CONVENTIONS.system} ${CONVENTIONS.model} ${CONVENTIONS.querying} ${CONVENTIONS.mutations}`,
42
47
  "",
43
48
  "Discovery sequence:",
44
49
  steps,
45
50
  "",
46
- "Call the `capabilities` tool for the full annotated tool catalog; read `pragma:{+uri}` resources (or `graph_inspect`) for entity detail.",
51
+ `Call the \`capabilities\` tool for the full annotated tool catalog; read ${templates} resources (or \`graph_inspect\`) for entity detail.`,
47
52
  ].join("\n");
48
53
  }
@@ -1,7 +1,10 @@
1
- import { existsSync } from "node:fs";
1
+ import { existsSync, mkdtempSync, readdirSync, writeFileSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
2
4
  import { describe, expect, it } from "vitest";
3
5
  import {
4
6
  fixtureEffectsModule,
7
+ fixturePreviewModule,
5
8
  touchPath,
6
9
  } from "../../../testing/fixtures/fixtureCapability.js";
7
10
  import { projectMcp } from "../../../testing/helpers/projectMcp.js";
@@ -70,3 +73,38 @@ describe("MCP mutation plan-first / confirm (PROTECTED)", () => {
70
73
  expect(existsSync(touchPath(name))).toBe(true);
71
74
  });
72
75
  });
76
+
77
+ describe("MCP plan-first is honest (PRA-104)", () => {
78
+ it("returns the ERROR a confirmed run would hit, not a confident plan", async () => {
79
+ // The CLI twin of this guard lives in cli/dispatch.test.ts: the same
80
+ // mutation, the same missing source, exiting nonzero instead of printing a
81
+ // plan. Both surfaces run one interpreter, so they cannot drift.
82
+ const dir = mkdtempSync(join(tmpdir(), "pragma-graft-mcp-"));
83
+ const missing = join(dir, "gone");
84
+
85
+ const mcp = await projectMcp([fixturePreviewModule], dir);
86
+ const planned = await mcp.callTool("probe_graft", { source: missing });
87
+ await mcp.cleanup();
88
+
89
+ expect(planned.ok).toBe(false);
90
+ expect(JSON.stringify(planned.error)).toContain(missing);
91
+ expect(existsSync(`${missing}.grafted`)).toBe(false);
92
+ });
93
+
94
+ it("returns a plan, and writes nothing, when the read would succeed", async () => {
95
+ const dir = mkdtempSync(join(tmpdir(), "pragma-graft-mcp-"));
96
+ const source = join(dir, "present.txt");
97
+ writeFileSync(source, "content\n");
98
+
99
+ const mcp = await projectMcp([fixturePreviewModule], dir);
100
+ const planned = await mcp.callTool("probe_graft", { source });
101
+ await mcp.cleanup();
102
+
103
+ expect(planned.ok).toBe(true);
104
+ expect(planned.meta).toEqual({ planOnly: true, confirmRequired: true });
105
+ expect((planned.data as { plan: string[] }).plan).toContain(
106
+ `Write file: ${source}.grafted (8 bytes)`,
107
+ );
108
+ expect(readdirSync(dir)).toEqual(["present.txt"]);
109
+ });
110
+ });