@canonical/pragma-cli 0.38.0 → 0.40.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 (330) hide show
  1. package/README.md +1 -1
  2. package/dist/package.json +14 -14
  3. package/dist/pragma.conf.js +273 -176
  4. package/dist/pragma.conf.js.map +1 -1
  5. package/dist/src/capabilities/capabilities/capabilities.verb.js +2 -0
  6. package/dist/src/capabilities/capabilities/capabilities.verb.js.map +1 -1
  7. package/dist/src/capabilities/capabilities/catalog.js +47 -51
  8. package/dist/src/capabilities/capabilities/catalog.js.map +1 -1
  9. package/dist/src/capabilities/capabilities/index.js +2 -0
  10. package/dist/src/capabilities/capabilities/index.js.map +1 -1
  11. package/dist/src/capabilities/capabilities/types.js +5 -5
  12. package/dist/src/capabilities/colophon/colophon.render.js +7 -4
  13. package/dist/src/capabilities/colophon/colophon.render.js.map +1 -1
  14. package/dist/src/capabilities/colophon/colophon.verb.js +1 -0
  15. package/dist/src/capabilities/colophon/colophon.verb.js.map +1 -1
  16. package/dist/src/capabilities/config/get.verb.js +2 -0
  17. package/dist/src/capabilities/config/get.verb.js.map +1 -1
  18. package/dist/src/capabilities/config/runField.js +2 -2
  19. package/dist/src/capabilities/config/runField.js.map +1 -1
  20. package/dist/src/capabilities/config/set.verb.js +2 -0
  21. package/dist/src/capabilities/config/set.verb.js.map +1 -1
  22. package/dist/src/capabilities/config/show.verb.js +1 -0
  23. package/dist/src/capabilities/config/show.verb.js.map +1 -1
  24. package/dist/src/capabilities/config/unset.verb.js +2 -0
  25. package/dist/src/capabilities/config/unset.verb.js.map +1 -1
  26. package/dist/src/capabilities/create/constants.js +2 -2
  27. package/dist/src/capabilities/create/create.verb.js +14 -4
  28. package/dist/src/capabilities/create/create.verb.js.map +1 -1
  29. package/dist/src/capabilities/distribution.js +3 -1
  30. package/dist/src/capabilities/distribution.js.map +1 -1
  31. package/dist/src/capabilities/doctor/checks/checkPackageRefs.js +10 -2
  32. package/dist/src/capabilities/doctor/checks/checkPackageRefs.js.map +1 -1
  33. package/dist/src/capabilities/doctor/doctor.verb.js +2 -0
  34. package/dist/src/capabilities/doctor/doctor.verb.js.map +1 -1
  35. package/dist/src/capabilities/graph/connect.verb.js +2 -0
  36. package/dist/src/capabilities/graph/connect.verb.js.map +1 -1
  37. package/dist/src/capabilities/graph/inspect.render.js +7 -1
  38. package/dist/src/capabilities/graph/inspect.render.js.map +1 -1
  39. package/dist/src/capabilities/graph/inspect.verb.js +2 -0
  40. package/dist/src/capabilities/graph/inspect.verb.js.map +1 -1
  41. package/dist/src/capabilities/graph/query.render.js +37 -5
  42. package/dist/src/capabilities/graph/query.render.js.map +1 -1
  43. package/dist/src/capabilities/graph/query.verb.js +2 -0
  44. package/dist/src/capabilities/graph/query.verb.js.map +1 -1
  45. package/dist/src/capabilities/graph/queryText.js +28 -0
  46. package/dist/src/capabilities/graph/queryText.js.map +1 -1
  47. package/dist/src/capabilities/graph/runConnect.js +3 -3
  48. package/dist/src/capabilities/graph/runConnect.js.map +1 -1
  49. package/dist/src/capabilities/graph/runQuery.js +8 -6
  50. package/dist/src/capabilities/graph/runQuery.js.map +1 -1
  51. package/dist/src/capabilities/info/info.verb.js +2 -0
  52. package/dist/src/capabilities/info/info.verb.js.map +1 -1
  53. package/dist/src/capabilities/meta/mcp.verb.js +1 -0
  54. package/dist/src/capabilities/meta/mcp.verb.js.map +1 -1
  55. package/dist/src/capabilities/ontology/verbs.js +7 -10
  56. package/dist/src/capabilities/ontology/verbs.js.map +1 -1
  57. package/dist/src/capabilities/prompt/verbs.js +5 -4
  58. package/dist/src/capabilities/prompt/verbs.js.map +1 -1
  59. package/dist/src/capabilities/resources/provider.js +6 -3
  60. package/dist/src/capabilities/resources/provider.js.map +1 -1
  61. package/dist/src/capabilities/setup/setup.verb.js +13 -5
  62. package/dist/src/capabilities/setup/setup.verb.js.map +1 -1
  63. package/dist/src/capabilities/shared/calls.js +16 -0
  64. package/dist/src/capabilities/shared/calls.js.map +1 -0
  65. package/dist/src/capabilities/skill/render.js +7 -5
  66. package/dist/src/capabilities/skill/render.js.map +1 -1
  67. package/dist/src/capabilities/skill/verbs.js +5 -4
  68. package/dist/src/capabilities/skill/verbs.js.map +1 -1
  69. package/dist/src/capabilities/sources/collectStatus.js +6 -1
  70. package/dist/src/capabilities/sources/collectStatus.js.map +1 -1
  71. package/dist/src/capabilities/sources/index.js +4 -3
  72. package/dist/src/capabilities/sources/index.js.map +1 -1
  73. package/dist/src/capabilities/sources/reset.render.js +37 -0
  74. package/dist/src/capabilities/sources/reset.render.js.map +1 -0
  75. package/dist/src/capabilities/sources/reset.verb.js +51 -0
  76. package/dist/src/capabilities/sources/reset.verb.js.map +1 -0
  77. package/dist/src/capabilities/sources/runReset.js +59 -0
  78. package/dist/src/capabilities/sources/runReset.js.map +1 -0
  79. package/dist/src/capabilities/sources/runUpdate.js +7 -6
  80. package/dist/src/capabilities/sources/runUpdate.js.map +1 -1
  81. package/dist/src/capabilities/sources/status.render.js +24 -8
  82. package/dist/src/capabilities/sources/status.render.js.map +1 -1
  83. package/dist/src/capabilities/sources/status.verb.js +1 -0
  84. package/dist/src/capabilities/sources/status.verb.js.map +1 -1
  85. package/dist/src/capabilities/sources/update.verb.js +2 -0
  86. package/dist/src/capabilities/sources/update.verb.js.map +1 -1
  87. package/dist/src/capabilities/upgrade/upgrade.verb.js +1 -0
  88. package/dist/src/capabilities/upgrade/upgrade.verb.js.map +1 -1
  89. package/dist/src/capabilities/version/version.verb.js +1 -0
  90. package/dist/src/capabilities/version/version.verb.js.map +1 -1
  91. package/dist/src/constants.js +4 -3
  92. package/dist/src/constants.js.map +1 -1
  93. package/dist/src/kernel/error/index.js +1 -1
  94. package/dist/src/kernel/error/index.js.map +1 -1
  95. package/dist/src/kernel/error/recovery.js +24 -42
  96. package/dist/src/kernel/error/recovery.js.map +1 -1
  97. package/dist/src/kernel/packs/collect.js +123 -9
  98. package/dist/src/kernel/packs/collect.js.map +1 -1
  99. package/dist/src/kernel/packs/compile.js +74 -15
  100. package/dist/src/kernel/packs/compile.js.map +1 -1
  101. package/dist/src/kernel/packs/graphql/fetchGraphqlLookup.js +3 -3
  102. package/dist/src/kernel/packs/graphql/fetchGraphqlLookup.js.map +1 -1
  103. package/dist/src/kernel/packs/iri.js +2 -4
  104. package/dist/src/kernel/packs/iri.js.map +1 -1
  105. package/dist/src/kernel/packs/renderPack.js +90 -50
  106. package/dist/src/kernel/packs/renderPack.js.map +1 -1
  107. package/dist/src/kernel/packs/resolveEntity.js +202 -90
  108. package/dist/src/kernel/packs/resolveEntity.js.map +1 -1
  109. package/dist/src/kernel/packs/runBodies.js +89 -30
  110. package/dist/src/kernel/packs/runBodies.js.map +1 -1
  111. package/dist/src/kernel/packs/schema.js +66 -12
  112. package/dist/src/kernel/packs/schema.js.map +1 -1
  113. package/dist/src/kernel/packs/sparql/buildListQuery.js +80 -13
  114. package/dist/src/kernel/packs/sparql/buildListQuery.js.map +1 -1
  115. package/dist/src/kernel/packs/sparql/buildLookupQuery.js +41 -13
  116. package/dist/src/kernel/packs/sparql/buildLookupQuery.js.map +1 -1
  117. package/dist/src/kernel/packs/sparql/filterValues.js +32 -23
  118. package/dist/src/kernel/packs/sparql/filterValues.js.map +1 -1
  119. package/dist/src/kernel/packs/sparql/runSelect.js +3 -5
  120. package/dist/src/kernel/packs/sparql/runSelect.js.map +1 -1
  121. package/dist/src/kernel/packs/storyRules.js +31 -4
  122. package/dist/src/kernel/packs/storyRules.js.map +1 -1
  123. package/dist/src/kernel/packs/tierScope.js +3 -3
  124. package/dist/src/kernel/packs/tierScope.js.map +1 -1
  125. package/dist/src/kernel/packs/types.js +4 -0
  126. package/dist/src/kernel/packs/types.js.map +1 -1
  127. package/dist/src/kernel/project/cli/buildProgram.js +4 -0
  128. package/dist/src/kernel/project/cli/buildProgram.js.map +1 -1
  129. package/dist/src/kernel/project/cli/dispatch.js +32 -8
  130. package/dist/src/kernel/project/cli/dispatch.js.map +1 -1
  131. package/dist/src/kernel/project/cli/suggestNames.js +44 -1
  132. package/dist/src/kernel/project/cli/suggestNames.js.map +1 -1
  133. package/dist/src/kernel/project/cli/verbHelp.js +17 -18
  134. package/dist/src/kernel/project/cli/verbHelp.js.map +1 -1
  135. package/dist/src/kernel/project/mcp/buildServer.js +11 -4
  136. package/dist/src/kernel/project/mcp/buildServer.js.map +1 -1
  137. package/dist/src/kernel/project/mcp/envelope.js +3 -2
  138. package/dist/src/kernel/project/mcp/envelope.js.map +1 -1
  139. package/dist/src/kernel/project/mcp/instructions.js +101 -31
  140. package/dist/src/kernel/project/mcp/instructions.js.map +1 -1
  141. package/dist/src/kernel/project/mcp/registerVerb.js +59 -23
  142. package/dist/src/kernel/project/mcp/registerVerb.js.map +1 -1
  143. package/dist/src/kernel/render/renderers.js +3 -3
  144. package/dist/src/kernel/render/turtle.js +21 -4
  145. package/dist/src/kernel/render/turtle.js.map +1 -1
  146. package/dist/src/kernel/runtime/graphpack/build.js +54 -1
  147. package/dist/src/kernel/runtime/graphpack/build.js.map +1 -1
  148. package/dist/src/kernel/runtime/graphpack/manifest.js +14 -0
  149. package/dist/src/kernel/runtime/graphpack/manifest.js.map +1 -1
  150. package/dist/src/kernel/runtime/graphpack/read.js +3 -5
  151. package/dist/src/kernel/runtime/graphpack/read.js.map +1 -1
  152. package/dist/src/kernel/runtime/graphpack/schemas.js +1 -0
  153. package/dist/src/kernel/runtime/graphpack/schemas.js.map +1 -1
  154. package/dist/src/kernel/runtime/loadSession.js +8 -1
  155. package/dist/src/kernel/runtime/loadSession.js.map +1 -1
  156. package/dist/src/kernel/runtime/packVersion.js +75 -0
  157. package/dist/src/kernel/runtime/packVersion.js.map +1 -0
  158. package/dist/src/kernel/runtime/readEntity.js +43 -4
  159. package/dist/src/kernel/runtime/readEntity.js.map +1 -1
  160. package/dist/src/kernel/runtime/resolveSources.js +61 -9
  161. package/dist/src/kernel/runtime/resolveSources.js.map +1 -1
  162. package/dist/src/kernel/runtime/storeReadiness.js +3 -2
  163. package/dist/src/kernel/runtime/storeReadiness.js.map +1 -1
  164. package/dist/src/kernel/spec/call.js +167 -0
  165. package/dist/src/kernel/spec/call.js.map +1 -0
  166. package/dist/src/kernel/spec/emitReference.js +9 -13
  167. package/dist/src/kernel/spec/emitReference.js.map +1 -1
  168. package/dist/src/kernel/spec/emitSurface.js +8 -1
  169. package/dist/src/kernel/spec/emitSurface.js.map +1 -1
  170. package/dist/src/kernel/spec/guidance.js +63 -0
  171. package/dist/src/kernel/spec/guidance.js.map +1 -0
  172. package/dist/src/kernel/spec/wireType.js +36 -0
  173. package/dist/src/kernel/spec/wireType.js.map +1 -0
  174. package/package.json +14 -14
  175. package/pragma.conf.ts +294 -177
  176. package/src/capabilities/block.parity.test.ts +1 -1
  177. package/src/capabilities/callRule.test.ts +147 -0
  178. package/src/capabilities/capabilities/capabilities.test.ts +47 -19
  179. package/src/capabilities/capabilities/capabilities.verb.ts +2 -0
  180. package/src/capabilities/capabilities/catalog.ts +60 -55
  181. package/src/capabilities/capabilities/index.ts +2 -0
  182. package/src/capabilities/capabilities/types.ts +16 -19
  183. package/src/capabilities/colophon/colophon.render.ts +9 -5
  184. package/src/capabilities/colophon/colophon.verb.ts +1 -0
  185. package/src/capabilities/config/get.verb.ts +2 -0
  186. package/src/capabilities/config/runField.ts +3 -3
  187. package/src/capabilities/config/set.verb.ts +3 -0
  188. package/src/capabilities/config/show.verb.ts +2 -0
  189. package/src/capabilities/config/unset.verb.ts +2 -0
  190. package/src/capabilities/create/constants.ts +2 -2
  191. package/src/capabilities/create/create.verb.ts +16 -0
  192. package/src/capabilities/create/createGrammar.test.ts +1 -1
  193. package/src/capabilities/distribution.test.ts +6 -3
  194. package/src/capabilities/distribution.ts +6 -0
  195. package/src/capabilities/doctor/checks/checkPackageRefs.ts +14 -2
  196. package/src/capabilities/doctor/doctor.test.ts +38 -1
  197. package/src/capabilities/doctor/doctor.verb.ts +2 -0
  198. package/src/capabilities/emptyRecovery.test.ts +5 -0
  199. package/src/capabilities/graph/connect.verb.ts +2 -0
  200. package/src/capabilities/graph/inspect.render.ts +9 -1
  201. package/src/capabilities/graph/inspect.test.ts +73 -0
  202. package/src/capabilities/graph/inspect.verb.ts +2 -0
  203. package/src/capabilities/graph/query.render.test.ts +35 -0
  204. package/src/capabilities/graph/query.render.ts +45 -7
  205. package/src/capabilities/graph/query.shipped.exec.test.ts +44 -0
  206. package/src/capabilities/graph/query.verb.ts +2 -0
  207. package/src/capabilities/graph/queryText.test.ts +55 -1
  208. package/src/capabilities/graph/queryText.ts +47 -0
  209. package/src/capabilities/graph/runConnect.ts +4 -5
  210. package/src/capabilities/graph/runQuery.ts +23 -9
  211. package/src/capabilities/info/info.verb.ts +2 -0
  212. package/src/capabilities/links.shipped.exec.test.ts +398 -0
  213. package/src/capabilities/listBudget.shipped.exec.test.ts +2 -2
  214. package/src/capabilities/listLookup.shipped.exec.test.ts +122 -13
  215. package/src/capabilities/listQuery.shipped.exec.test.ts +39 -16
  216. package/src/capabilities/lookupBudget.shipped.exec.test.ts +208 -0
  217. package/src/capabilities/meta/mcp.verb.ts +2 -0
  218. package/src/capabilities/ontology/ontology.test.ts +24 -0
  219. package/src/capabilities/ontology/verbs.ts +16 -12
  220. package/src/capabilities/prompt/verbs.ts +10 -4
  221. package/src/capabilities/resources/provider.ts +7 -3
  222. package/src/capabilities/resources/resources.test.ts +2 -1
  223. package/src/capabilities/setup/setup.verb.ts +16 -0
  224. package/src/capabilities/shared/calls.ts +20 -0
  225. package/src/capabilities/skill/render.ts +9 -6
  226. package/src/capabilities/skill/verbs.ts +8 -4
  227. package/src/capabilities/sources/collectStatus.ts +6 -1
  228. package/src/capabilities/sources/index.ts +4 -3
  229. package/src/capabilities/sources/reset.render.test.ts +33 -0
  230. package/src/capabilities/sources/reset.render.ts +43 -0
  231. package/src/capabilities/sources/reset.verb.ts +59 -0
  232. package/src/capabilities/sources/runReset.ts +65 -0
  233. package/src/capabilities/sources/runUpdate.ts +12 -11
  234. package/src/capabilities/sources/sources.test.ts +158 -3
  235. package/src/capabilities/sources/status.render.test.ts +63 -1
  236. package/src/capabilities/sources/status.render.ts +25 -8
  237. package/src/capabilities/sources/status.verb.ts +2 -0
  238. package/src/capabilities/sources/types.ts +26 -0
  239. package/src/capabilities/sources/update.verb.ts +3 -0
  240. package/src/capabilities/surface.test.ts +2 -2
  241. package/src/capabilities/token.parity.test.ts +3 -2
  242. package/src/capabilities/tokenGraph.shipped.exec.test.ts +14 -2
  243. package/src/capabilities/upgrade/upgrade.verb.ts +1 -0
  244. package/src/capabilities/variable.parity.test.ts +2 -1
  245. package/src/capabilities/version/version.verb.ts +1 -0
  246. package/src/capabilities/wireContract.test.ts +110 -0
  247. package/src/constants.ts +4 -3
  248. package/src/kernel/copy.test.ts +1 -1
  249. package/src/kernel/error/PragmaError.test.ts +7 -3
  250. package/src/kernel/error/index.ts +1 -1
  251. package/src/kernel/error/recovery.test.ts +52 -18
  252. package/src/kernel/error/recovery.ts +25 -46
  253. package/src/kernel/error/types.ts +1 -1
  254. package/src/kernel/packs/collect.test.ts +79 -0
  255. package/src/kernel/packs/collect.ts +151 -11
  256. package/src/kernel/packs/compile.test.ts +107 -10
  257. package/src/kernel/packs/compile.ts +100 -11
  258. package/src/kernel/packs/filteredEmpty.test.ts +22 -5
  259. package/src/kernel/packs/graphql/fetchGraphqlLookup.ts +5 -6
  260. package/src/kernel/packs/graphqlSource.test.ts +42 -0
  261. package/src/kernel/packs/iri.ts +5 -4
  262. package/src/kernel/packs/listNotice.test.ts +1 -1
  263. package/src/kernel/packs/nounLink.test.ts +385 -0
  264. package/src/kernel/packs/renderPack.ts +137 -57
  265. package/src/kernel/packs/resolveEntity.test.ts +170 -1
  266. package/src/kernel/packs/resolveEntity.ts +281 -101
  267. package/src/kernel/packs/runBodies.ts +133 -31
  268. package/src/kernel/packs/schema.ts +77 -12
  269. package/src/kernel/packs/sparql/buildListQuery.test.ts +78 -1
  270. package/src/kernel/packs/sparql/buildListQuery.ts +106 -16
  271. package/src/kernel/packs/sparql/buildLookupQuery.ts +44 -13
  272. package/src/kernel/packs/sparql/filterValues.test.ts +36 -4
  273. package/src/kernel/packs/sparql/filterValues.ts +50 -31
  274. package/src/kernel/packs/sparql/runSelect.ts +4 -6
  275. package/src/kernel/packs/storyRules.ts +37 -4
  276. package/src/kernel/packs/tierScope.test.ts +120 -6
  277. package/src/kernel/packs/tierScope.ts +5 -7
  278. package/src/kernel/packs/toolDescription.test.ts +59 -74
  279. package/src/kernel/packs/types.ts +93 -11
  280. package/src/kernel/project/cli/buildProgram.ts +4 -0
  281. package/src/kernel/project/cli/dispatch.ts +34 -8
  282. package/src/kernel/project/cli/suggest.test.ts +77 -0
  283. package/src/kernel/project/cli/suggestNames.ts +47 -1
  284. package/src/kernel/project/cli/verbHelp.ts +16 -19
  285. package/src/kernel/project/mcp/buildServer.ts +15 -4
  286. package/src/kernel/project/mcp/envelope.ts +3 -2
  287. package/src/kernel/project/mcp/instructions.ts +123 -35
  288. package/src/kernel/project/mcp/prompts/prompts.test.ts +136 -20
  289. package/src/kernel/project/mcp/registerVerb.test.ts +28 -0
  290. package/src/kernel/project/mcp/registerVerb.ts +67 -29
  291. package/src/kernel/render/__snapshots__/renderers.test.ts.snap +1 -1
  292. package/src/kernel/render/renderers.test.ts +2 -2
  293. package/src/kernel/render/renderers.ts +3 -3
  294. package/src/kernel/render/turtle.ts +24 -4
  295. package/src/kernel/runtime/graphpack/build.ts +58 -1
  296. package/src/kernel/runtime/graphpack/graphpack.test.ts +20 -0
  297. package/src/kernel/runtime/graphpack/manifest.ts +15 -0
  298. package/src/kernel/runtime/graphpack/read.ts +4 -6
  299. package/src/kernel/runtime/graphpack/schemas.ts +1 -0
  300. package/src/kernel/runtime/graphpack/types.ts +13 -0
  301. package/src/kernel/runtime/loadSession.ts +8 -1
  302. package/src/kernel/runtime/packVersion.test.ts +61 -0
  303. package/src/kernel/runtime/packVersion.ts +84 -0
  304. package/src/kernel/runtime/readEntity.ts +61 -6
  305. package/src/kernel/runtime/resolveSources.test.ts +90 -7
  306. package/src/kernel/runtime/resolveSources.ts +91 -10
  307. package/src/kernel/runtime/storeReadiness.ts +4 -4
  308. package/src/kernel/runtime/types.ts +12 -0
  309. package/src/kernel/spec/call.test.ts +95 -0
  310. package/src/kernel/spec/call.ts +186 -0
  311. package/src/kernel/spec/emitReference.ts +9 -13
  312. package/src/kernel/spec/emitSurface.ts +9 -1
  313. package/src/kernel/spec/guidance.test.ts +49 -0
  314. package/src/kernel/spec/guidance.ts +68 -0
  315. package/src/kernel/spec/index.ts +4 -0
  316. package/src/kernel/spec/surfaceConformance.test.ts +2 -2
  317. package/src/kernel/spec/types.ts +64 -7
  318. package/src/kernel/spec/wireType.ts +53 -0
  319. package/src/testing/behavioral/errorMatrix.mcp.test.ts +2 -1
  320. package/src/testing/behavioral/journeys.firstInstall.test.ts +72 -2
  321. package/src/testing/behavioral/parity.test.ts +40 -0
  322. package/src/testing/eval/__snapshots__/eval.test.ts.snap +1 -1
  323. package/src/testing/eval/cases/assembly.ts +4 -3
  324. package/src/testing/eval/cases/stable.ts +2 -3
  325. package/src/testing/helpers/callSchema.ts +40 -0
  326. package/src/testing/helpers/parity.ts +35 -2
  327. package/src/testing/setupCallChecking.ts +30 -0
  328. package/dist/src/capabilities/capabilities/hints.js +0 -223
  329. package/dist/src/capabilities/capabilities/hints.js.map +0 -1
  330. package/src/capabilities/capabilities/hints.ts +0 -266
@@ -34,6 +34,7 @@ import type {
34
34
  GlobalFlags,
35
35
  InteractionRuntime,
36
36
  PragmaRuntime,
37
+ StoreSession,
37
38
  } from "../../runtime/types.js";
38
39
  import { kebabCase, type ParamSpec, type VerbSpec } from "../../spec/index.js";
39
40
  import { EXIT, mapExitCode } from "./exitCodes.js";
@@ -175,9 +176,10 @@ function renderData(
175
176
  // is distinguishable from an unbuilt store or a mistyped filter, and an
176
177
  // ambiguous lookup hit from an unambiguous one. `data` keeps its uniform
177
178
  // shape, and MCP builds the same key from the same seam
178
- // (`mcp/registerVerb.ts#noticeMeta`) — the two machine surfaces stay
179
- // byte-equal.
180
- const notice = verb.output.formatters.notice?.(data);
179
+ // (`mcp/registerVerb.ts#noticeMeta`). The two envelopes are equal EXCEPT
180
+ // for spelling inside this sentence: a next step reads as a command here
181
+ // and as a tool call there, because each is printed where it will be used.
182
+ const notice = verb.output.formatters.notice?.(data, "cli");
181
183
  // The machine half of the same seam: facts the read wants to hand an agent
182
184
  // as data rather than prose (`meta.scope`). MCP merges the same seam into
183
185
  // the same keys (`mcp/registerVerb.ts#readMeta`).
@@ -204,7 +206,9 @@ function renderData(
204
206
  const text = verb.output.formatters.plain(data, context);
205
207
  // The calm notice is success-path guidance — `--quiet` mutes it.
206
208
  const notice =
207
- flags.quiet === true ? undefined : verb.output.formatters.notice?.(data);
209
+ flags.quiet === true
210
+ ? undefined
211
+ : verb.output.formatters.notice?.(data, "cli");
208
212
  return {
209
213
  stdout: text ? `${text}\n` : "",
210
214
  ...(notice ? { stderr: `${notice}\n` } : {}),
@@ -320,9 +324,9 @@ export async function executeVerb(
320
324
  // The lazy-store seam: boot the store (once, memoized) only for verbs that
321
325
  // declare they need it. A storeless verb never reaches the store factory, so
322
326
  // the storeless guarantee holds by construction (no STORE_SKIP triage).
323
- if (verb.capability.needsStore) {
324
- await runtime.store.get();
325
- }
327
+ const session = verb.capability.needsStore
328
+ ? await runtime.store.get()
329
+ : undefined;
326
330
 
327
331
  if (verb.capability.mutates) {
328
332
  // Tell the verb whether this is a plan-only preview (`--dry-run`) or a real
@@ -468,7 +472,29 @@ export async function executeVerb(
468
472
  const data = await Promise.resolve(
469
473
  verb.run(params, runtime) as Promise<unknown>,
470
474
  );
471
- return renderData(verb, flags, data, {});
475
+ return renderData(verb, flags, data, storeMeta(session));
476
+ }
477
+
478
+ /**
479
+ * The envelope facts about the STORE this read was answered from, as data.
480
+ *
481
+ * One fact so far: the project's own built pack, when the boot passed over it
482
+ * because an older CLI built it. An agent that reads `block lookup button` and
483
+ * gets the snapshot's answer has no way to learn from the payload that this
484
+ * directory also holds a pack — so the fact rides `meta`, from the session the
485
+ * answer came from, on every read verb at once.
486
+ *
487
+ * `meta` ONLY, deliberately: the read's answer is not wrong (the snapshot is the
488
+ * current graph, which is the whole point of passing the pack over), so the
489
+ * plain and llm renderings of every read stay byte for byte what they were, and
490
+ * the sentence a person reads stays where a person goes for it — `sources
491
+ * status` and `doctor`.
492
+ *
493
+ * @param session - The booted session, or `undefined` for a storeless verb.
494
+ * @returns The base `meta` for the envelope (empty for an ordinary read).
495
+ */
496
+ function storeMeta(session: StoreSession | undefined): Record<string, unknown> {
497
+ return session?.ignoredPack ? { ignoredPack: session.ignoredPack } : {};
472
498
  }
473
499
 
474
500
  /**
@@ -118,4 +118,81 @@ describe("suggestNames", () => {
118
118
  "Timeline ",
119
119
  ]);
120
120
  });
121
+
122
+ describe("a candidate that is a whole-segment part of the query", () => {
123
+ it("is offered for a name with a segment too many", () => {
124
+ // Recorded: `token lookup color.text.primary` suggested NOTHING although
125
+ // `color.text` exists — it is no prefix of the query's, and eight extra
126
+ // characters is past any edit-distance threshold.
127
+ expect(
128
+ suggestNames("color.text.primary", ["color.text", "color.border"]),
129
+ ).toEqual(["color.text"]);
130
+ });
131
+
132
+ it("is offered for the last segment of a pasted local name", () => {
133
+ expect(
134
+ suggestNames("apps_lxd.component.meter", ["Meter", "Metric"]),
135
+ ).toEqual(["Meter"]);
136
+ });
137
+
138
+ it("ranks first, longest run first, ahead of a candidate the query prefixes", () => {
139
+ expect(
140
+ suggestNames("color.text.muted", [
141
+ "color.text.muted.hover",
142
+ "color",
143
+ "color.text",
144
+ "text.muted",
145
+ ]),
146
+ ).toEqual([
147
+ "color.text",
148
+ "text.muted",
149
+ "color",
150
+ "color.text.muted.hover",
151
+ ]);
152
+ });
153
+
154
+ it("cuts at segment boundaries only", () => {
155
+ expect(
156
+ suggestNames("color.text.primary", ["color.te", "lor.text"]),
157
+ ).toEqual([]);
158
+ });
159
+
160
+ it("reads a slash-separated path the same way", () => {
161
+ expect(
162
+ suggestNames("react/component/tsdoc/extra", ["react/component/tsdoc"]),
163
+ ).toEqual(["react/component/tsdoc"]);
164
+ });
165
+
166
+ it("reads past a prefix, so a prefixed name suggests like its bare form", () => {
167
+ expect(
168
+ suggestNames("dt:color.text.zzz", ["color.text", "color"]),
169
+ ).toEqual(["color.text", "color"]);
170
+ });
171
+
172
+ it("builds no runs for a query too long to be a name", () => {
173
+ const parts = Array.from({ length: 3000 }, (_, i) => `s${i}`);
174
+ const started = performance.now();
175
+ expect(suggestNames(parts.join("."), ["s1.s2"])).toEqual([]);
176
+ expect(performance.now() - started).toBeLessThan(100);
177
+ });
178
+
179
+ it("keeps the five-result cap", () => {
180
+ const parts = ["a1", "b2", "c3", "d4", "e5", "f6", "g7"];
181
+ expect(suggestNames(parts.join("."), parts)).toHaveLength(5);
182
+ });
183
+
184
+ it("stays cheap against a pool the size of the shipped pack", () => {
185
+ // The length prefilter exists because ranking 4,380 names once cost
186
+ // 1.2 s; this branch is a map lookup per candidate and must not undo
187
+ // it. The pool is names the prefilter rejects on length, so what is
188
+ // timed is the branch and the prefilter, not the edit distances.
189
+ const pool = Array.from(
190
+ { length: 5000 },
191
+ (_, i) => `color.group${i}.tone.on.a.path.far.longer.than.the.query.is`,
192
+ );
193
+ const started = performance.now();
194
+ expect(suggestNames("apps_lxd.component.meter", pool)).toEqual([]);
195
+ expect(performance.now() - started).toBeLessThan(100);
196
+ });
197
+ });
121
198
  });
@@ -6,6 +6,40 @@
6
6
  * by the unknown-command suggester to turn a typo into "Did you mean: …?".
7
7
  */
8
8
 
9
+ /** How many suggestions are returned unless a caller asks for fewer or more. */
10
+ export const MAX_SUGGESTIONS = 5;
11
+
12
+ /** What separates the segments of a path-shaped name (`:` ends a prefix). */
13
+ const SEGMENT_SEPARATOR = /[./:]/;
14
+
15
+ /** Past this many segments a query is not a name, and its runs are not built. */
16
+ const MAX_RUN_SEGMENTS = 16;
17
+
18
+ /**
19
+ * Every contiguous run of whole segments of `query` shorter than the query,
20
+ * mapped to the number of segments it spans; empty for a query of one segment
21
+ * or of more than {@link MAX_RUN_SEGMENTS}.
22
+ */
23
+ function segmentRuns(query: string): ReadonlyMap<string, number> {
24
+ if (query.split(SEGMENT_SEPARATOR).length > MAX_RUN_SEGMENTS) {
25
+ return new Map();
26
+ }
27
+ const starts = [0];
28
+ for (const [index, char] of [...query].entries()) {
29
+ if (SEGMENT_SEPARATOR.test(char)) starts.push(index + 1);
30
+ }
31
+ const ends = [...starts.slice(1).map((start) => start - 1), query.length];
32
+ const runs = new Map<string, number>();
33
+ for (const [from, start] of starts.entries()) {
34
+ for (const [to, end] of ends.entries()) {
35
+ const run = query.slice(start, end);
36
+ if (to < from || run === "" || run === query) continue;
37
+ runs.set(run, to - from + 1);
38
+ }
39
+ }
40
+ return runs;
41
+ }
42
+
9
43
  /**
10
44
  * Return up to {@link maxResults} candidates most similar to `query`.
11
45
  *
@@ -19,6 +53,10 @@
19
53
  * if a candidate really is the query, the miss is the bug and the suggestion
20
54
  * would only hide it.
21
55
  *
56
+ * Ranking, best first: a candidate that is a run of the query's whole segments
57
+ * (`color.text` for `color.text.primary`), longest first; then a candidate the
58
+ * query is a prefix of; then edit distance.
59
+ *
22
60
  * Scoring reads the same trimmed forms, so padding costs a candidate no edit
23
61
  * distance either; what is RETURNED is the candidate verbatim, padding and
24
62
  * casing intact, because a suggestion names a value the graph holds.
@@ -34,7 +72,7 @@ export function suggestNames(
34
72
  candidates: readonly string[],
35
73
  opts?: { maxResults?: number; threshold?: number },
36
74
  ): string[] {
37
- const maxResults = opts?.maxResults ?? 5;
75
+ const maxResults = opts?.maxResults ?? MAX_SUGGESTIONS;
38
76
  const threshold = opts?.threshold ?? 0.4;
39
77
  const queryLower = query.trim().toLowerCase();
40
78
 
@@ -44,12 +82,20 @@ export function suggestNames(
44
82
 
45
83
  type Scored = { name: string; score: number };
46
84
  const scored: Scored[] = [];
85
+ const runs = segmentRuns(queryLower);
47
86
 
48
87
  for (const candidate of candidates) {
49
88
  const candidateLower = candidate.trim().toLowerCase();
50
89
 
51
90
  if (candidateLower === queryLower) continue;
52
91
 
92
+ // Below zero: every run outranks a prefix match, the longest run first.
93
+ const spanned = runs.get(candidateLower);
94
+ if (spanned !== undefined) {
95
+ scored.push({ name: candidate, score: -spanned });
96
+ continue;
97
+ }
98
+
53
99
  if (candidateLower.startsWith(queryLower)) {
54
100
  scored.push({ name: candidate, score: 0 });
55
101
  continue;
@@ -8,7 +8,9 @@
8
8
  * `formatVerbHelp`, retargeted at the grammar.
9
9
  */
10
10
 
11
- import type { ParamSpec, VerbSpec } from "../../spec/index.js";
11
+ import { renderCall } from "../../spec/call.js";
12
+ import { exampleCall, useWhenSentence } from "../../spec/guidance.js";
13
+ import type { Example, ParamSpec, VerbSpec } from "../../spec/index.js";
12
14
  import { kebabCase } from "../../spec/index.js";
13
15
  import { MUTATION_FLAG_DOCS, negationFlagDoc } from "./constants.js";
14
16
  import {
@@ -19,18 +21,6 @@ import {
19
21
  helpUsage,
20
22
  } from "./helpFormat.js";
21
23
 
22
- /**
23
- * A verb's `doc` doubles as its MCP tool description, so a pack's authored
24
- * `toolDescription` may end with an MCP tool-call example — `Example:
25
- * token_lookup { names: ["…"] }`. That `noun_verb {…}` syntax is MCP-transport
26
- * shape, meaningless on the CLI, so the CLI projector drops the trailing example
27
- * sentence; the MCP projector keeps the authored text whole. Hand-written docs
28
- * carry no such example and pass through untouched.
29
- */
30
- function stripToolCallExample(doc: string): string {
31
- return doc.replace(/\s*Example:\s+[a-z][a-z0-9_]*\s*\{[^{}]*\}\.?\s*$/, "");
32
- }
33
-
34
24
  /** The positional usage token for a param (`<name>` / `[name]`, `...` variadic). */
35
25
  function positionalToken(param: ParamSpec): string {
36
26
  const variadic = param.kind === "string[]" ? "..." : "";
@@ -69,10 +59,9 @@ export function formatVerbHelp(programName: string, verb: VerbSpec): string {
69
59
  verb.summary,
70
60
  ];
71
61
 
72
- if (verb.doc) {
73
- const cliDoc = stripToolCallExample(verb.doc);
74
- if (cliDoc) lines.push("", cliDoc);
75
- }
62
+ if (verb.doc) lines.push("", verb.doc);
63
+ const question = useWhenSentence(verb);
64
+ if (question) lines.push("", question);
76
65
 
77
66
  // Every flag the command PARSES is rendered, from the same spec facts
78
67
  // registration reads: the declared params, each default-true boolean's
@@ -97,9 +86,17 @@ export function formatVerbHelp(programName: string, verb: VerbSpec): string {
97
86
  lines.push("", helpHeading("Flags"), ...helpColumns(flagRows));
98
87
  }
99
88
 
100
- if (verb.examples && verb.examples.length > 0) {
89
+ // The verb's declared example leads, spelled as a command by the same
90
+ // renderer that spells it as a tool call in the MCP description.
91
+ const declaredExample = exampleCall(verb);
92
+ const generated = declaredExample && renderCall(declaredExample, "cli");
93
+ const examples = [
94
+ ...(generated ? [{ cmd: generated } as Example] : []),
95
+ ...(verb.examples ?? []).filter((example) => example.cmd !== generated),
96
+ ];
97
+ if (examples.length > 0) {
101
98
  lines.push("", helpHeading("Examples"));
102
- for (const example of verb.examples) {
99
+ for (const example of examples) {
103
100
  lines.push(` ${helpTerm(example.cmd)}`);
104
101
  if (example.note) lines.push(` ${helpDim(example.note)}`);
105
102
  }
@@ -18,8 +18,9 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
18
18
  import { MCP_SERVER_NAME, VERSION } from "../../../constants.js";
19
19
  import type { GlobalFlags } from "../../runtime/index.js";
20
20
  import { bootRuntime } from "../../runtime/index.js";
21
+ import { declareVerbs } from "../../spec/call.js";
21
22
  import type { CapabilityModule } from "../../spec/index.js";
22
- import { buildInstructions } from "./instructions.js";
23
+ import { buildInstructions, INSTRUCTIONS_MAX_CHARS } from "./instructions.js";
23
24
  import { registerVerb } from "./registerVerb.js";
24
25
 
25
26
  /**
@@ -53,11 +54,21 @@ export async function buildServer(
53
54
  ): Promise<McpServer> {
54
55
  const server = new McpServer(
55
56
  { name: MCP_SERVER_NAME, version: VERSION },
56
- // Handshake orientation (sent once at initialize), derived from the SAME
57
- // conventions/discovery as the `capabilities` tool so the two never diverge.
58
- { instructions: buildInstructions(modules) },
57
+ // Handshake orientation (sent once at initialize): the distribution's
58
+ // conventions plus a question → tool index generated from the registry. It
59
+ // is FITTED to the length clients keep; when a project's packs push it over,
60
+ // the dropped index lines are noted on stderr (stdout is the JSON-RPC frame).
61
+ {
62
+ instructions: buildInstructions(modules, (dropped) =>
63
+ process.stderr.write(
64
+ `MCP instructions: ${dropped} index line(s) did not fit ${INSTRUCTIONS_MAX_CHARS} characters and were left to the capabilities catalogue.\n`,
65
+ ),
66
+ ),
67
+ },
59
68
  );
60
69
  const runtime = bootRuntime(MCP_FLAGS, cwd);
70
+ // Tells the call renderer which verbs exist (see `spec/call.ts`).
71
+ declareVerbs(modules.flatMap((module) => module.verbs));
61
72
 
62
73
  for (const module of modules) {
63
74
  for (const verb of module.verbs) {
@@ -4,8 +4,9 @@
4
4
  * The MCP projector returns the *same* `{ ok, data, meta }` / `{ ok, error }`
5
5
  * envelope the CLI emits under `--format json` — built by the shared
6
6
  * `successEnvelope` / `errorEnvelope` — serialized into a single text content
7
- * block (D4). Reusing the shared builders is what guarantees CLI-JSON and MCP
8
- * output are byte-identical.
7
+ * block (D4). Reusing the shared builders is what keeps CLI-JSON and MCP output
8
+ * the same envelope — equal in everything but the spelling of a next step
9
+ * inside `meta.notice`, which follows the surface it is printed on.
9
10
  */
10
11
 
11
12
  import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
@@ -2,52 +2,140 @@
2
2
  * The MCP server `instructions` string — orientation sent ONCE in the
3
3
  * initialize handshake (not per tool call), so agents arrive oriented.
4
4
  *
5
- * It carries the same content the retired `llm` tool front-loaded: the
6
- * conventions + the discovery sequence. To guarantee it never diverges from the
7
- * `capabilities` tool, the WHOLE orientation — the opening line included —
8
- * derives from the SAME source: `CONVENTIONS` and
9
- * `buildDiscoverySequence` in `capabilities/capabilities/catalog.ts`. Live
10
- * numbers (tier/channel/entity counts) are deliberately DROPPED (they would need
11
- * a store boot at handshake); agents fetch those via `info`/`config show`/
12
- * `sources status`. Kept short (a hard length ceiling is asserted in the test),
13
- * since it counts against every session's context.
5
+ * A client may defer tools: the agent then sees only tool NAMES and this text
6
+ * until it searches for a tool by name, so a description it never loads cannot
7
+ * steer it. The instructions therefore carry a generated "question → tool"
8
+ * index, built from the SAME `useWhen` each verb declares for its description:
9
+ *
10
+ * - the distribution's conventions, handed over as data (`mcpOrientation` —
11
+ * they name its tiers and nouns, so the kernel writes none of them);
12
+ * - one line per read tool that is not part of a noun's list/lookup/sample
13
+ * trio, `tool — useWhen`;
14
+ * - the trio explained ONCE, with the nouns that have each verb enumerated;
15
+ * - the writes on one shared, plan-first line.
16
+ *
17
+ * Everything listed is derived from the registry: a new verb adds itself. Live
18
+ * numbers (tier/channel/entity counts) are deliberately absent — they would
19
+ * need a store boot at handshake.
20
+ */
21
+
22
+ import type { CapabilityModule, VerbSpec } from "../../spec/index.js";
23
+ import { emitSurface, toolName } from "../../spec/index.js";
24
+
25
+ /**
26
+ * HARD ceiling on the instructions length, unlike the catalogue budget: clients
27
+ * cap server instructions at about 2 KB and cut the rest, so text past this is
28
+ * text no agent reads. Fit by tightening sentences, never by raising it.
14
29
  */
30
+ export const INSTRUCTIONS_MAX_CHARS = 2000;
15
31
 
16
- import {
17
- buildDiscoverySequence,
18
- CONVENTIONS,
19
- } from "../../../capabilities/capabilities/catalog.js";
20
- import type { CapabilityModule } from "../../spec/index.js";
21
- import { emitSurface } from "../../spec/index.js";
32
+ /** The verbs every read story compiles to; explained once rather than per noun. */
33
+ const TRIO = ["list", "lookup", "sample"] as const;
22
34
 
23
- /** Hard ceiling on the instructions length (asserted by the protected test). */
24
- export const INSTRUCTIONS_MAX_CHARS = 1500;
35
+ /** How most `useWhen` clauses open; the index says it once. */
36
+ const ASKED = /^when asked /;
37
+
38
+ /** Which of the trio a verb is, or nothing for a verb that stands alone. */
39
+ function trioVerbOf(verb: VerbSpec): (typeof TRIO)[number] | undefined {
40
+ return TRIO.find((name) => verb.path[1] === name);
41
+ }
25
42
 
26
43
  /**
27
44
  * Build the handshake orientation string from the live capability modules.
28
45
  *
29
- * @param modules - The capability modules (for the live discovery-sample list).
30
- * @returns A short, single-source orientation string (≤ {@link INSTRUCTIONS_MAX_CHARS}).
46
+ * The registry is not fixed — a project's packs add verbs at runtime — so the
47
+ * text is FITTED, not merely measured: everything that is one line whatever the
48
+ * registry holds comes first, the index comes last, and index lines are dropped
49
+ * from the end until the whole fits, the last line saying how many were dropped
50
+ * and where the full list is. A client would otherwise cut the tail silently.
51
+ *
52
+ * @param modules - The capability modules; the one declaring `mcpOrientation`
53
+ * supplies the conventions, the registry everything else.
54
+ * @param onTruncate - Told how many index lines did not fit, when any did not.
55
+ * @returns The orientation (≤ {@link INSTRUCTIONS_MAX_CHARS}); empty when no
56
+ * module declares one.
31
57
  */
32
58
  export function buildInstructions(
33
59
  modules: readonly CapabilityModule[],
60
+ onTruncate?: (dropped: number) => void,
34
61
  ): string {
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)
41
- .map((stage) => `${stage.stage}. ${stage.tool} — ${stage.purpose}`)
42
- .join("\n");
43
- const templates = resources.map((template) => `\`${template}\``).join(", ");
44
-
45
- return [
46
- `${CONVENTIONS.system} ${CONVENTIONS.model} ${CONVENTIONS.querying} ${CONVENTIONS.mutations}`,
62
+ const orientation = modules.find(
63
+ (module) => module.mcpOrientation,
64
+ )?.mcpOrientation;
65
+ if (!orientation) return "";
66
+ const tools = modules
67
+ .flatMap((module) => module.verbs)
68
+ .filter((verb) => !verb.hidden && verb.capability.mcp.expose);
69
+ const reads = tools.filter((verb) => !verb.capability.mutates);
70
+
71
+ // The trio, once: every noun that has any of the three, marked for what it
72
+ // has beyond `list` — a noun is never promised a verb it lacks.
73
+ const has = (noun: string, name: (typeof TRIO)[number]): boolean =>
74
+ reads.some((verb) => verb.path[0] === noun && trioVerbOf(verb) === name);
75
+ const nouns = [
76
+ ...new Set(reads.filter(trioVerbOf).map((verb) => verb.path[0])),
77
+ ].map(
78
+ (noun) =>
79
+ `${noun}${has(noun, "lookup") ? "" : "†"}${has(noun, "sample") ? "*" : ""}`,
80
+ );
81
+ const writes = tools
82
+ .filter((verb) => verb.capability.mutates)
83
+ .map((verb) => toolName(verb.path))
84
+ .join(", ");
85
+ // The resource templates come from the emitted surface: the scheme is
86
+ // declared by the module that serves it and frozen in the covenant.
87
+ const templates = emitSurface(modules)
88
+ .mcpSurface.resources.map((template) => `\`${template}\``)
89
+ .join(", ");
90
+ const fixed = [
91
+ orientation.conventions.join(" "),
47
92
  "",
48
- "Discovery sequence:",
49
- steps,
93
+ `Per noun, <noun>_list finds entries and <noun>_lookup reads them by name († has no lookup); * also has <noun>_sample, which shows real data shapes: ${nouns.join(", ")}.`,
94
+ `Writes are plan-first — the first call returns a plan; repeat it with confirm: true to apply: ${writes}.`,
95
+ `Entity detail: read ${templates} resources.`,
50
96
  "",
51
- `Call the \`capabilities\` tool for the full annotated tool catalog; read ${templates} resources (or \`graph_inspect\`) for entity detail.`,
52
- ].join("\n");
97
+ ];
98
+
99
+ // The index. Verbs that read the store come first — they are the ones that
100
+ // answer questions about the data — so what an overflow drops is the
101
+ // administrative tail. A verb declaring no `useWhen` has no line to give.
102
+ const standalone = reads
103
+ .filter((verb) => !trioVerbOf(verb) && verb.useWhen)
104
+ .sort(
105
+ (a, b) =>
106
+ Number(b.capability.needsStore) - Number(a.capability.needsStore),
107
+ );
108
+ // Most clauses open "when asked …"; saying that once, as a heading, is what
109
+ // lets every question fit under the ceiling.
110
+ const lineFor = (verb: VerbSpec): string =>
111
+ `${toolName(verb.path)} — ${verb.useWhen?.replace(ASKED, "")}`;
112
+ const group = (heading: string, verbs: readonly VerbSpec[]): string[] =>
113
+ verbs.length > 0 ? [heading, ...verbs.map(lineFor)] : [];
114
+ const index = [
115
+ ...group(
116
+ "When asked:",
117
+ standalone.filter((verb) => ASKED.test(verb.useWhen ?? "")),
118
+ ),
119
+ ...group(
120
+ "Also:",
121
+ standalone.filter((verb) => !ASKED.test(verb.useWhen ?? "")),
122
+ ),
123
+ ];
124
+
125
+ // Where the full list lives: the distribution's own orientation tool.
126
+ const catalogue = tools.find((verb) => verb.category === "orientation");
127
+ const render = (kept: number): string => {
128
+ const dropped = index.length - kept;
129
+ const more =
130
+ dropped === 0
131
+ ? []
132
+ : [
133
+ `… and ${dropped} more${catalogue ? `: call ${toolName(catalogue.path)}` : ""}.`,
134
+ ];
135
+ return [...fixed, ...index.slice(0, kept), ...more].join("\n");
136
+ };
137
+ let kept = index.length;
138
+ while (kept > 0 && render(kept).length > INSTRUCTIONS_MAX_CHARS) kept--;
139
+ if (kept < index.length) onTruncate?.(index.length - kept);
140
+ return render(kept);
53
141
  }