@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
@@ -12,10 +12,13 @@
12
12
  * formatters).
13
13
  */
14
14
 
15
+ import { BIN_NAME } from "../../constants.js";
15
16
  import { PragmaError } from "../../kernel/error/PragmaError.js";
17
+ import { cliRecovery } from "../../kernel/error/recovery.js";
16
18
  import type { PragmaRuntime } from "../../kernel/runtime/types.js";
17
19
  import { asVerb } from "../../kernel/spec/asVerb.js";
18
20
  import type { VerbSpec } from "../../kernel/spec/types.js";
21
+ import { VOCABULARY } from "../../kernel/vocabulary.js";
19
22
  import {
20
23
  promptListFormatters,
21
24
  promptLookupFormatters,
@@ -34,10 +37,10 @@ const READ_CAPABILITY = {
34
37
  const listVerb: VerbSpec<Record<string, unknown>, PromptListData> = {
35
38
  path: ["prompt", "list"],
36
39
  summary: "List the workflow prompt templates the design system offers.",
37
- doc: "Browse the ds:Prompt entities in the active graph — name, description, and argument names. The same prompts are offered natively over MCP prompts/list; use prompt_lookup for the full template body.",
40
+ doc: `Browse the prompt entities the active graph declares (${VOCABULARY.prompt.type} in this distribution) — name, description, and argument names. This distribution's graph carries none today. The same prompts are offered natively over MCP prompts/list; use prompt_lookup for the full template body.`,
38
41
  params: [],
39
42
  output: { formatters: promptListFormatters },
40
- examples: [{ cmd: "pragma prompt list" }],
43
+ examples: [{ cmd: `${BIN_NAME} prompt list` }],
41
44
  capability: READ_CAPABILITY,
42
45
  run: (_params: Record<string, unknown>, rt: PragmaRuntime) =>
43
46
  import("../../kernel/project/mcp/prompts/source.js").then(async (m) => {
@@ -55,19 +58,31 @@ const listVerb: VerbSpec<Record<string, unknown>, PromptListData> = {
55
58
  const lookupVerb: VerbSpec<Record<string, unknown>, PromptLookupData> = {
56
59
  path: ["prompt", "lookup"],
57
60
  summary: "Show one workflow prompt template's body and arguments by name.",
58
- doc: "Fetch a single ds:Prompt entity's full template body (with {{arg}} placeholders) and its declared arguments.",
61
+ doc: "Fetch a single prompt entity's full template body (with {{arg}} placeholders) and its declared arguments. A prompt is addressed by its label; prompt_list names the ones the active graph carries.",
59
62
  params: [
60
63
  {
61
64
  kind: "string",
62
65
  name: "name",
63
- doc: "The prompt name (e.g. build-a-block).",
66
+ doc: "The prompt name, as `prompt list` reports it.",
64
67
  positional: true,
65
68
  required: true,
66
- complete: { kind: "names", source: { from: "prompts" } },
69
+ // A prompt is addressed by its `rdfs:label`, which the index carries as
70
+ // `label`, and the index reader offers no substitute for a prompt that
71
+ // carries none. Not a total guarantee: `label` is the first of several
72
+ // label predicates the index accepts, so an entity labelled ONLY by
73
+ // `skos:prefLabel` would still complete without resolving. That gap is
74
+ // the index's label preference order, not this ref.
75
+ complete: {
76
+ kind: "names",
77
+ source: { from: "index", type: VOCABULARY.prompt.type, field: "label" },
78
+ },
67
79
  },
68
80
  ],
69
81
  output: { formatters: promptLookupFormatters },
70
- examples: [{ cmd: "pragma prompt lookup build-a-block" }],
82
+ // No example command: this distribution's graph declares no prompt, so any
83
+ // name quoted here exits 1 with ENTITY_NOT_FOUND — and this is the GENERATED
84
+ // reference, published as machine-derived truth.
85
+ examples: [],
71
86
  capability: READ_CAPABILITY,
72
87
  run: (params: Record<string, unknown>, rt: PragmaRuntime) =>
73
88
  import("../../kernel/project/mcp/prompts/source.js").then(async (m) => {
@@ -77,11 +92,9 @@ const lookupVerb: VerbSpec<Record<string, unknown>, PromptLookupData> = {
77
92
  const available = await m.readPrompts(rt);
78
93
  throw PragmaError.notFound("prompt", name, {
79
94
  suggestions: available.map((prompt) => prompt.name),
80
- recovery: {
81
- message: "List available prompts.",
82
- cli: "pragma prompt list",
83
- mcp: { tool: "prompt_list" },
84
- },
95
+ recovery: cliRecovery("prompt list", "List available prompts.", {
96
+ tool: "prompt_list",
97
+ }),
85
98
  });
86
99
  }
87
100
  return entry;
@@ -3,9 +3,11 @@
3
3
  *
4
4
  * Listing and autocomplete are STORELESS: they read the enriched `index.json`
5
5
  * (readPackIndex) off disk, so the resource surface costs nothing until an agent
6
- * reads one. A read is STORE-BACKED through the SHARED {@link readEntity} — the
7
- * same reader `graph inspect` uses — so a resource read and a `graph inspect` of
8
- * the same URI return identical content (the mirror contract).
6
+ * reads one. They read it for the pack the BOOT DECISION names, so the listing
7
+ * can never advertise entities whose reads then fail STORE_UNAVAILABLE. A read
8
+ * is STORE-BACKED through the SHARED {@link readEntity} — the same reader
9
+ * `graph inspect` uses — so a resource read and a `graph inspect` of the same
10
+ * URI return identical content (the mirror contract).
9
11
  *
10
12
  * Degradation (Risk4): on a missing or legacy (pre-v2) index the listing returns
11
13
  * a single "run `pragma sources update`" hint — never a live re-index of the
@@ -16,12 +18,14 @@
16
18
  * the `--help`/`__complete` fast path.
17
19
  */
18
20
 
21
+ import { BIN_NAME } from "../../constants.js";
19
22
  import { readPackIndex } from "../../kernel/completion/entitySource.js";
20
23
  import { asPragmaError } from "../../kernel/error/fromTaskError.js";
21
24
  import { PragmaError } from "../../kernel/error/PragmaError.js";
22
25
  import { mcpErrorFrom } from "../../kernel/project/mcp/mcpError.js";
23
26
  import type { PackIndex } from "../../kernel/runtime/graphpack/types.js";
24
27
  import { readEntity } from "../../kernel/runtime/readEntity.js";
28
+ import { resolveSources } from "../../kernel/runtime/resolveSources.js";
25
29
  import type { McpResourceProvider } from "../../kernel/spec/types.js";
26
30
 
27
31
  /** Max autocomplete suggestions returned for a partial URI. */
@@ -32,13 +36,25 @@ const CLASS_PRIORITY = 0.9;
32
36
  /** MCP annotation priority for an individual (ABox) entry. */
33
37
  const INDIVIDUAL_PRIORITY = 0.3;
34
38
 
35
- /** The `pragma:` URI scheme + the single reserved-expansion template variable. */
39
+ /**
40
+ * The `pragma:` URI scheme + the single reserved-expansion template variable.
41
+ *
42
+ * A LITERAL, and it must stay one until the wire identifiers move together.
43
+ * `surface/surface.v2.json` freezes `pragma:{+uri}` as protocol identity, and
44
+ * `buildResourceList` mints `pragma:<prefixed>` for every entry it lists —
45
+ * three writings of one decision. Deriving only this one made a fork advertise
46
+ * `recipes:{+uri}` over a list of 653 `pragma:` URIs: every resource the server
47
+ * offered became unreadable, and the readable form was never advertised.
48
+ * `resources.test.ts` pins the pair through {@link resourceProvider}'s declared
49
+ * surface; `identity.test.ts` masks this template out of its leak scan for the
50
+ * same reason.
51
+ */
36
52
  const URI_TEMPLATE = "pragma:{+uri}";
37
53
 
38
54
  const TEMPLATE_DESCRIPTION =
39
55
  "Knowledge-graph entities from the local pack. Read any entry by its prefixed " +
40
56
  "URI (e.g. pragma:ds:button); autocomplete matches URI or label substrings. " +
41
- "Content mirrors `pragma graph inspect <uri>`.";
57
+ `Content mirrors \`${BIN_NAME} graph inspect <uri>\`.`;
42
58
 
43
59
  /** Agent-navigability annotations mirrored onto a listed resource. */
44
60
  export interface ResourceAnnotations {
@@ -80,8 +96,7 @@ export function buildResourceList(
80
96
  {
81
97
  uri: "pragma:sources",
82
98
  name: "Store not indexed",
83
- description:
84
- "No enriched entity index. Run `pragma sources update` to build it.",
99
+ description: `No enriched entity index. Run \`${BIN_NAME} sources update\` to build it.`,
85
100
  mimeType: "application/json",
86
101
  },
87
102
  ];
@@ -197,14 +212,18 @@ export const resourceProvider: McpResourceProvider = {
197
212
  const { McpError, ErrorCode } = await import(
198
213
  "@modelcontextprotocol/sdk/types.js"
199
214
  );
215
+ // The index of the pack the boot decision names — `undefined` when the
216
+ // store is unavailable, which `buildResourceList` renders as the recovery
217
+ // entry rather than a catalogue of unreadable URIs.
218
+ const activeIndex = async (): Promise<PackIndex | undefined> =>
219
+ readPackIndex(resolveSources(await runtime.loadConfig(), runtime.cwd));
220
+
200
221
  const template = new ResourceTemplate(URI_TEMPLATE, {
201
- list: async () => ({
202
- resources: buildResourceList(readPackIndex(runtime.cwd)),
203
- }),
222
+ list: async () => ({ resources: buildResourceList(await activeIndex()) }),
204
223
  complete: {
205
224
  uri: async (value: string) =>
206
225
  rankUriCompletions(
207
- readPackIndex(runtime.cwd)?.entities ?? [],
226
+ (await activeIndex())?.entities ?? [],
208
227
  value,
209
228
  COMPLETION_LIMIT,
210
229
  ),
@@ -7,7 +7,7 @@
7
7
  * degrades to a `pragma sources update` hint (never a live re-index).
8
8
  */
9
9
 
10
- import { mkdtempSync } from "node:fs";
10
+ import { mkdtempSync, writeFileSync } from "node:fs";
11
11
  import { tmpdir } from "node:os";
12
12
  import { join } from "node:path";
13
13
  import { ErrorCode } from "@modelcontextprotocol/sdk/types.js";
@@ -16,13 +16,40 @@ import { readPackIndex } from "../../kernel/completion/entitySource.js";
16
16
  import { verbKey } from "../../kernel/packs/uniqueness.js";
17
17
  import { bootRuntime } from "../../kernel/runtime/boot.js";
18
18
  import type { PackIndex } from "../../kernel/runtime/graphpack/types.js";
19
- import { writeLock } from "../../kernel/runtime/lock.js";
20
19
  import type { InspectResult } from "../../kernel/runtime/readEntity.js";
21
20
  import type { VerbSpec } from "../../kernel/spec/types.js";
22
21
  import { TEST_FLAGS } from "../../testing/helpers/projectCli.js";
23
22
  import { projectMcp } from "../../testing/helpers/projectMcp.js";
24
23
  import { graphModule } from "../graph/index.js";
25
- import { buildResourceList, rankUriCompletions } from "./provider.js";
24
+ import {
25
+ buildResourceList,
26
+ rankUriCompletions,
27
+ resourceProvider,
28
+ } from "./provider.js";
29
+
30
+ /**
31
+ * Two entities of the committed embedded pack, chosen for what they PROVE: an
32
+ * abox individual and the tbox class it instantiates, so the schema-before-
33
+ * individual ordering has both sides. Named, not counted — the roster moves
34
+ * whenever the design system does.
35
+ */
36
+ const BUTTON_NAME = "ds:global.component.button";
37
+ const BUTTON_URI = `pragma:${BUTTON_NAME}`;
38
+ const COMPONENT_URI = "pragma:ds:Component";
39
+
40
+ /**
41
+ * Blank out blank-node labels.
42
+ *
43
+ * Oxigraph re-mints blank-node identifiers every time it loads a store, so two
44
+ * independently booted sessions over the SAME pack name the same blank node
45
+ * differently. Those labels are store-local handles, not content — and this
46
+ * test's subject is that both surfaces return the same CONTENT for the same
47
+ * entity. (That the labels are not stable across processes is a real property
48
+ * of exposing blank nodes through `graph inspect`; the toy pack this suite used
49
+ * to run against simply had none.)
50
+ */
51
+ const withoutBlankNodeLabels = (value: InspectResult): unknown =>
52
+ JSON.parse(JSON.stringify(value).replace(/_:[0-9a-f]+/g, "_:b"));
26
53
 
27
54
  describe("resource listing (storeless, over the pack index)", () => {
28
55
  it("degrades to a recovery entry on a missing or legacy index", () => {
@@ -37,15 +64,13 @@ describe("resource listing (storeless, over the pack index)", () => {
37
64
  });
38
65
 
39
66
  it("lists the enriched (v2) embedded index, schema entries first", () => {
40
- const index = readPackIndex(process.cwd());
67
+ const index = readPackIndex({ kind: "embedded" });
41
68
  expect(index?.version).toBe(2);
42
69
  const resources = buildResourceList(index);
43
- expect(resources.some((r) => r.uri === "pragma:ex:Button")).toBe(true);
70
+ expect(resources.some((r) => r.uri === BUTTON_URI)).toBe(true);
44
71
  // A class (tbox) sorts before an individual (abox).
45
- const componentIdx = resources.findIndex(
46
- (r) => r.uri === "pragma:ex:Component",
47
- );
48
- const buttonIdx = resources.findIndex((r) => r.uri === "pragma:ex:Button");
72
+ const componentIdx = resources.findIndex((r) => r.uri === COMPONENT_URI);
73
+ const buttonIdx = resources.findIndex((r) => r.uri === BUTTON_URI);
49
74
  expect(componentIdx).toBeLessThan(buttonIdx);
50
75
  });
51
76
 
@@ -92,24 +117,46 @@ describe("resource listing (storeless, over the pack index)", () => {
92
117
  });
93
118
 
94
119
  it("enriches each entry with the _meta taxonomy (pragma/box + priority)", () => {
95
- const resources = buildResourceList(readPackIndex(process.cwd()));
96
- const component = resources.find((r) => r.uri === "pragma:ex:Component");
97
- const button = resources.find((r) => r.uri === "pragma:ex:Button");
120
+ const resources = buildResourceList(readPackIndex({ kind: "embedded" }));
121
+ const component = resources.find((r) => r.uri === COMPONENT_URI);
122
+ const button = resources.find((r) => r.uri === BUTTON_URI);
98
123
  // A schema class: tbox, higher priority, carries its instance count.
99
124
  expect(component?._meta?.["pragma/box"]).toBe("tbox");
100
125
  expect(component?.annotations?.audience).toEqual(["assistant"]);
101
126
  expect(component?.annotations?.priority).toBe(0.9);
102
- expect(component?._meta?.["pragma/instanceCount"]).toBe(3);
127
+ expect(component?._meta?.["pragma/instanceCount"]).toBeGreaterThan(0);
103
128
  // An individual: abox, lower priority, no instance count.
104
129
  expect(button?._meta?.["pragma/box"]).toBe("abox");
105
130
  expect(button?.annotations?.priority).toBe(0.3);
106
131
  expect(button?._meta?.["pragma/instanceCount"]).toBeUndefined();
107
132
  });
108
133
 
134
+ it("mints every URI in the scheme the declared template routes", () => {
135
+ // Two writings of one decision: the template `register` installs (and the
136
+ // covenant publishes as `mcpSurface.resources`) and the scheme every listed
137
+ // URI carries. Nothing compared them, and a half-derivation left a fork
138
+ // advertising `recipes:{+uri}` over a list of `pragma:` URIs — every
139
+ // resource offered was unreadable and the readable form was unadvertised.
140
+ // DERIVED from the declared surface, so this cannot be satisfied by two
141
+ // literals that agree today.
142
+ const declared = resourceProvider.surface?.templates?.[0];
143
+ const scheme = `${declared?.split(":")[0]}:`;
144
+ const listed = [
145
+ ...buildResourceList(readPackIndex({ kind: "embedded" })),
146
+ ...buildResourceList(undefined),
147
+ ];
148
+ expect(listed.length).toBeGreaterThan(1);
149
+ expect(listed.filter((r) => !r.uri.startsWith(scheme))).toEqual([]);
150
+ });
151
+
109
152
  it("ranks autocomplete over prefixed URI and label", () => {
110
- const index = readPackIndex(process.cwd());
111
- const hits = rankUriCompletions(index?.entities ?? [], "but", 10);
112
- expect(hits).toContain("ex:Button");
153
+ const index = readPackIndex({ kind: "embedded" });
154
+ const hits = rankUriCompletions(
155
+ index?.entities ?? [],
156
+ "global.component.button",
157
+ 10,
158
+ );
159
+ expect(hits).toContain(BUTTON_NAME);
113
160
  });
114
161
  });
115
162
 
@@ -124,7 +171,7 @@ describe("resource surface over the server (embedded pack)", () => {
124
171
 
125
172
  it("registers the {+uri} template and lists resources (not tools)", async () => {
126
173
  const resources = await harness.listResources();
127
- expect(resources.some((r) => r.uri === "pragma:ex:Button")).toBe(true);
174
+ expect(resources.some((r) => r.uri === BUTTON_URI)).toBe(true);
128
175
  // Resources are NOT tools — the graph module's tools (inspect + the PR6
129
176
  // SPARQL escape hatch) are what appear in the tool surface.
130
177
  const tools = await harness.listTools();
@@ -135,12 +182,12 @@ describe("resource surface over the server (embedded pack)", () => {
135
182
  });
136
183
 
137
184
  it("autocompletes a partial URI through the template", async () => {
138
- const values = await harness.completeResource("But");
139
- expect(values).toContain("ex:Button");
185
+ const values = await harness.completeResource("global.component.butt");
186
+ expect(values).toContain(BUTTON_NAME);
140
187
  });
141
188
 
142
189
  it("reads an entity, mirroring `graph inspect` content", async () => {
143
- const read = await harness.readResource("pragma:ex:Button");
190
+ const read = await harness.readResource(BUTTON_URI);
144
191
  expect(read.mimeType).toBe("application/json");
145
192
  const fromResource = JSON.parse(read.text) as InspectResult;
146
193
 
@@ -150,21 +197,25 @@ describe("resource surface over the server (embedded pack)", () => {
150
197
  (v) => verbKey(v.path) === "graph inspect",
151
198
  ) as VerbSpec;
152
199
  const fromCli = (await inspect.run(
153
- { uri: "ex:Button" },
200
+ { uri: BUTTON_NAME },
154
201
  rt,
155
202
  )) as InspectResult;
156
203
  (await rt.store.get()).store.dispose();
157
204
 
158
- expect(fromResource).toEqual(fromCli);
159
- expect(fromResource.uri).toBe("https://pragma.canonical.com/sample#Button");
205
+ expect(withoutBlankNodeLabels(fromResource)).toEqual(
206
+ withoutBlankNodeLabels(fromCli),
207
+ );
208
+ expect(fromResource.uri).toBe(
209
+ "https://ds.canonical.com/global.component.button",
210
+ );
160
211
  });
161
212
 
162
213
  it("rejects a read of an absent entity as InvalidParams carrying ENTITY_NOT_FOUND", async () => {
163
- // The warm embedded pack resolves the `ex:` prefix but holds no `Nonexistent`
214
+ // The warm embedded pack resolves the `ds:` prefix but holds no `Nonexistent`
164
215
  // subject, so the read fails ENTITY_NOT_FOUND — the caller's fault, which
165
216
  // `mcpErrorFrom` maps to JSON-RPC InvalidParams, NOT the InternalError a cold
166
217
  // store (STORE_UNAVAILABLE) earns.
167
- const error = await harness.readResource("pragma:ex:Nonexistent").then(
218
+ const error = await harness.readResource("pragma:ds:Nonexistent").then(
168
219
  () => undefined,
169
220
  (caught: unknown) => caught,
170
221
  );
@@ -181,14 +232,19 @@ describe("resource surface over the server (embedded pack)", () => {
181
232
  * D3 — a resource-read failure surfaces as a JSON-RPC error (the read analogue of
182
233
  * a tool result's `isError`) carrying the recovery, NOT swallowed into a
183
234
  * `text/plain` "success" body that drops the recovery and reads as though the
184
- * entity itself were malformed. A lock pointing at an evicted pack is the cold
185
- * store; the read boots the store, so it must refuse with STORE_UNAVAILABLE.
235
+ * entity itself were malformed. A project that declares its own packs and has
236
+ * never built them is the cold store — it must NOT be served the distribution's
237
+ * embedded graph; the read boots the store, so it refuses with
238
+ * STORE_UNAVAILABLE.
186
239
  */
187
240
  describe("resource read — cold-store failure surfaces isError + recovery (D3)", () => {
188
241
  let harness: Awaited<ReturnType<typeof projectMcp>>;
189
242
  beforeAll(async () => {
190
243
  const cwd = mkdtempSync(join(tmpdir(), "pragma-resource-cold-"));
191
- writeLock(cwd, { version: 1, contentHash: "b".repeat(64), packs: [] });
244
+ writeFileSync(
245
+ join(cwd, "pragma.config.ts"),
246
+ `export default { packs: [{ name: "unbuilt", source: "file:///pragma-never-built" }] };\n`,
247
+ );
192
248
  harness = await projectMcp([graphModule], cwd);
193
249
  });
194
250
  afterAll(async () => {
@@ -196,7 +252,7 @@ describe("resource read — cold-store failure surfaces isError + recovery (D3)"
196
252
  });
197
253
 
198
254
  it("rejects (does not return text/plain content) with the STORE_UNAVAILABLE recovery", async () => {
199
- const error = await harness.readResource("pragma:ex:Button").then(
255
+ const error = await harness.readResource(BUTTON_URI).then(
200
256
  () => undefined,
201
257
  (caught: unknown) => caught,
202
258
  );
@@ -28,6 +28,7 @@ import type {
28
28
  PromptDefinition,
29
29
  } from "@canonical/summon-core";
30
30
  import { sequence_, type Task, when } from "@canonical/task";
31
+ import { BIN_NAME } from "../../../constants.js";
31
32
  import type { PragmaRuntime } from "../../../kernel/runtime/types.js";
32
33
  import { guardMissingBinary } from "../../shared/assertExecOk.js";
33
34
  import type {
@@ -125,7 +126,7 @@ const buildMeta = (
125
126
  ): GeneratorDefinition["meta"] => ({
126
127
  name: title,
127
128
  displayName: title,
128
- description: "Configure pragma for this project",
129
+ description: `Configure ${BIN_NAME} for this project`,
129
130
  version: rt.version,
130
131
  });
131
132
 
@@ -135,7 +136,7 @@ const buildCustomizePrompt = (
135
136
  ): PromptDefinition => ({
136
137
  name: "customize",
137
138
  type: "confirm",
138
- message: "Customize which files pragma configures?",
139
+ message: `Customize which files ${BIN_NAME} configures?`,
139
140
  default: false,
140
141
  when,
141
142
  });
@@ -282,7 +283,7 @@ function buildRunAllPlan(
282
283
  // reads already happened up front). Composing an unchosen step is harmless:
283
284
  // `when(false, …)` discards the (side-effect-free) task it was handed.
284
285
  const generator: GeneratorDefinition = {
285
- meta: buildMeta(rt, "pragma setup"),
286
+ meta: buildMeta(rt, `${BIN_NAME} setup`),
286
287
  prompts,
287
288
  generate: (answers) => {
288
289
  const chosen = readSteps(answers);
@@ -342,8 +343,11 @@ export async function buildSetupPlan(
342
343
  case "completions": {
343
344
  const d = await detectCompletions(rt.cwd);
344
345
  return {
345
- generator: buildSingleStep(rt, "pragma setup completions", [], () =>
346
- composeCompletions(d),
346
+ generator: buildSingleStep(
347
+ rt,
348
+ `${BIN_NAME} setup completions`,
349
+ [],
350
+ () => composeCompletions(d),
347
351
  ),
348
352
  toResult: () => ({
349
353
  kind: "completions",
@@ -358,7 +362,7 @@ export async function buildSetupPlan(
358
362
  case "lsp": {
359
363
  const d = await detectLsp(rt.cwd);
360
364
  return {
361
- generator: buildSingleStep(rt, "pragma setup lsp", [], () =>
365
+ generator: buildSingleStep(rt, `${BIN_NAME} setup lsp`, [], () =>
362
366
  composeGuardedLsp(rt, d.state),
363
367
  ),
364
368
  toResult: () => ({ kind: "lsp", state: d.state }),
@@ -375,8 +379,11 @@ export async function buildSetupPlan(
375
379
  ]
376
380
  : [];
377
381
  return {
378
- generator: buildSingleStep(rt, "pragma setup mcp", prompts, (answers) =>
379
- composeMcp(d, selectChosenGroups(d, answers)),
382
+ generator: buildSingleStep(
383
+ rt,
384
+ `${BIN_NAME} setup mcp`,
385
+ prompts,
386
+ (answers) => composeMcp(d, selectChosenGroups(d, answers)),
380
387
  ),
381
388
  toResult: (answers) => {
382
389
  const sel = selectChosenGroups(d, answers);
@@ -393,7 +400,7 @@ export async function buildSetupPlan(
393
400
  const d = await detectSkills(rt);
394
401
  if (!d.available) throw skillsEmptyError();
395
402
  return {
396
- generator: buildSingleStep(rt, "pragma setup skills", [], () =>
403
+ generator: buildSingleStep(rt, `${BIN_NAME} setup skills`, [], () =>
397
404
  composeSkills(d),
398
405
  ),
399
406
  toResult: () => ({ kind: "skills", result: toSkillsResult(d) }),
@@ -63,7 +63,7 @@ export interface McpDetection {
63
63
  * @returns The pragma {@link McpServerConfig}.
64
64
  */
65
65
  export function pragmaMcpEntry(cwd: string): McpServerConfig {
66
- return { command: "pragma", args: ["mcp"], cwd };
66
+ return { command: MCP_SERVER_NAME, args: ["mcp"], cwd };
67
67
  }
68
68
 
69
69
  /**
@@ -228,7 +228,7 @@ export function composeMcp(
228
228
  : "→";
229
229
  tasks.push(
230
230
  info(
231
- `[${group.scope}] pragma MCP server ${verb} ${group.path} (${names})`,
231
+ `[${group.scope}] ${MCP_SERVER_NAME} MCP server ${verb} ${group.path} (${names})`,
232
232
  ),
233
233
  );
234
234
  }
@@ -19,7 +19,7 @@ import {
19
19
  symlink,
20
20
  type Task,
21
21
  } from "@canonical/task";
22
- import { RECOVERY_CLI_PREFIX } from "../../../constants.js";
22
+ import { BIN_NAME } from "../../../constants.js";
23
23
  import { PragmaError } from "../../../kernel/error/PragmaError.js";
24
24
  import { cliRecovery } from "../../../kernel/error/recovery.js";
25
25
  import type { PragmaRuntime } from "../../../kernel/runtime/types.js";
@@ -198,8 +198,8 @@ export function skillsEmptyError(): PragmaError {
198
198
  return PragmaError.emptyResults("skill", {
199
199
  message: "No skills found to link.",
200
200
  recovery: cliRecovery(
201
- `${RECOVERY_CLI_PREFIX}sources update`,
202
- "Add a design-system package that ships skills to your config, then run `pragma sources update` to install them.",
201
+ "sources update",
202
+ `Add a design-system package that ships skills to your config, then run \`${BIN_NAME} sources update\` to install them.`,
203
203
  ),
204
204
  });
205
205
  }
@@ -21,6 +21,7 @@
21
21
 
22
22
  import type { GeneratorResult } from "@canonical/summon-core";
23
23
  import { $, gen, type Task } from "@canonical/task";
24
+ import { BIN_NAME } from "../../constants.js";
24
25
  import type { PragmaRuntime } from "../../kernel/runtime/types.js";
25
26
  import { asVerb } from "../../kernel/spec/asVerb.js";
26
27
  import type {
@@ -207,20 +208,26 @@ const setupAllVerb = setupVerb(
207
208
  params: SCOPE_PARAMS,
208
209
  doc: "Runs the shell-completions, LSP, MCP, and skills installers as a single wizard: pick the steps, review the recap, then apply. The scope option targets the project band, the user/home band, or both.",
209
210
  examples: [
210
- { cmd: "pragma setup" },
211
- { cmd: "pragma setup --dry-run", note: "preview every step's effects" },
211
+ { cmd: `${BIN_NAME} setup` },
212
212
  {
213
- cmd: "pragma setup --global",
213
+ cmd: `${BIN_NAME} setup --dry-run`,
214
+ note: "preview every step's effects",
215
+ },
216
+ {
217
+ cmd: `${BIN_NAME} setup --global`,
214
218
  note: "configure only the user/home band",
215
219
  },
216
- { cmd: "pragma setup mcp", note: "just the MCP server registration" },
220
+ {
221
+ cmd: `${BIN_NAME} setup mcp`,
222
+ note: "just the MCP server registration",
223
+ },
217
224
  ],
218
225
  },
219
226
  );
220
227
 
221
228
  const mcpVerb = setupVerb(
222
229
  ["setup", "mcp"],
223
- "Register the pragma MCP server in detected AI harnesses.",
230
+ `Register the ${BIN_NAME} MCP server in detected AI harnesses.`,
224
231
  "mcp",
225
232
  SUB_CAPABILITY,
226
233
  { params: SCOPE_PARAMS },
@@ -9,6 +9,7 @@
9
9
 
10
10
  import { homedir } from "node:os";
11
11
  import { join } from "node:path";
12
+ import { BIN_NAME } from "../../constants.js";
12
13
 
13
14
  /** The shells `setup completions` can install for. */
14
15
  export type ShellId = "zsh" | "bash" | "fish";
@@ -34,6 +35,31 @@ export function detectShell(): ShellId | null {
34
35
  /**
35
36
  * The standard install path for a shell's completion script.
36
37
  *
38
+ * The BASENAME is {@link BIN_NAME}, never a literal, because `emitScripts`
39
+ * derives the script's own `#compdef` / `complete -F` / `complete -c` from that
40
+ * same name, and for two of the three shells the basename decides whether the
41
+ * file is ever loaded. Measured against real shells with the distribution
42
+ * renamed to `widget9`:
43
+ *
44
+ * - **fish** autoloads `completions/<cmd>.fish` BY NAME. A correct `widget9`
45
+ * script in a file called `pragma.fish` is never loaded — TAB silently falls
46
+ * back to filenames. Verified: renaming that one file is the entire
47
+ * difference between candidates and no candidates.
48
+ * - **bash-completion** looks up `completions/<cmd>` by the command name the
49
+ * same way. Verified against bash-completion 2.11's `__load_completion`:
50
+ * with the file named `widget9`, `complete -p widget9` reports
51
+ * `complete -F _widget9 widget9`; with the identical file named `pragma`,
52
+ * `_widget9` is never defined and bash falls back to `_minimal`.
53
+ * - **zsh** does NOT work that way: `compinit` binds by the `#compdef` tag
54
+ * inside the file, so `~/.zfunc/_pragma` holding `#compdef widget9` does
55
+ * complete `widget9` (verified — `_comps[widget9]` resolves to `_pragma`).
56
+ * The basename still matters, for a different reason: it names the function
57
+ * zsh autoloads, and two distributions both writing `_pragma` would silently
58
+ * overwrite each other's completions.
59
+ *
60
+ * Either way `setup completions` and `doctor` report success, so nothing else
61
+ * would tell the user.
62
+ *
37
63
  * @param shell - The target shell.
38
64
  * @returns The absolute path the completion script is written to.
39
65
  * @note Impure — reads the home directory.
@@ -42,7 +68,7 @@ export function completionScriptPath(shell: ShellId): string {
42
68
  const home = homedir();
43
69
  switch (shell) {
44
70
  case "zsh":
45
- return join(home, ".zfunc", "_pragma");
71
+ return join(home, ".zfunc", `_${BIN_NAME}`);
46
72
  case "bash":
47
73
  return join(
48
74
  home,
@@ -50,10 +76,10 @@ export function completionScriptPath(shell: ShellId): string {
50
76
  "share",
51
77
  "bash-completion",
52
78
  "completions",
53
- "pragma",
79
+ BIN_NAME,
54
80
  );
55
81
  case "fish":
56
- return join(home, ".config", "fish", "completions", "pragma.fish");
82
+ return join(home, ".config", "fish", "completions", `${BIN_NAME}.fish`);
57
83
  }
58
84
  }
59
85
 
@@ -13,6 +13,7 @@
13
13
  import { readdirSync, readFileSync, statSync } from "node:fs";
14
14
  import { homedir } from "node:os";
15
15
  import { basename, join } from "node:path";
16
+ import { BIN_NAME } from "../../constants.js";
16
17
 
17
18
  /** Parsed SKILL.md frontmatter. */
18
19
  export interface SkillFrontmatter {
@@ -103,26 +104,31 @@ export function parseFrontmatter(content: string): SkillFrontmatter | null {
103
104
  }
104
105
 
105
106
  /**
106
- * The installed-skills root: `$XDG_DATA_HOME/pragma/skills`. This is where
107
+ * The installed-skills root: `$XDG_DATA_HOME/<bin>/skills`. This is where
107
108
  * `sources update` INSTALLS package-provided skills (U10) — a symlink per skill
108
109
  * — and the second discovery root below reads them back. The single source of
109
110
  * truth for both, so the install target and the discovery root can never drift.
111
+ *
112
+ * Namespaced by {@link BIN_NAME}, like config, state and cache: two
113
+ * distributions installed side by side owned ONE skills root, so a `recipes`
114
+ * build served skills a `pragma` build had installed and could not install its
115
+ * own without colliding.
110
116
  */
111
117
  export function installedSkillsDir(): string {
112
118
  const dataHome =
113
119
  process.env.XDG_DATA_HOME ?? join(homedir(), ".local", "share");
114
- return join(dataHome, "pragma", "skills");
120
+ return join(dataHome, BIN_NAME, "skills");
115
121
  }
116
122
 
117
123
  /**
118
124
  * The conventional roots skills are discovered from, in PRECEDENCE order: the
119
- * project root (`<cwd>/.pragma/skills`) FIRST so a project-local skill overrides
125
+ * project root (`<cwd>/.<bin>/skills`) FIRST so a project-local skill overrides
120
126
  * an installed skill of the same name (`discoverSkills` dedups first-seen-wins),
121
- * then installed skills under `$XDG_DATA_HOME/pragma/skills` (where package
127
+ * then installed skills under `$XDG_DATA_HOME/<bin>/skills` (where package
122
128
  * skills land on `sources update`).
123
129
  */
124
130
  export function skillRoots(cwd: string): string[] {
125
- return [join(cwd, ".pragma", "skills"), installedSkillsDir()];
131
+ return [join(cwd, `.${BIN_NAME}`, "skills"), installedSkillsDir()];
126
132
  }
127
133
 
128
134
  /** Immediate subdirectories of `root` (each a candidate skill folder). */