@bevel-software/platform-core-backend 0.25.2 → 0.26.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 (248) hide show
  1. package/agent-guide/access-control.md +234 -0
  2. package/agent-guide/conventions.md +27 -0
  3. package/agent-guide/directory-structure.md +145 -0
  4. package/agent-guide/finding-things.md +7 -0
  5. package/agent-guide/introduction.md +27 -0
  6. package/agent-guide/skills.md +47 -0
  7. package/agent-guide/tool-manuals.md +217 -0
  8. package/agent-guide/where-a-new-file-goes.md +36 -0
  9. package/dist/assets.d.ts +7 -0
  10. package/dist/assets.d.ts.map +1 -1
  11. package/dist/assets.js +9 -0
  12. package/dist/assets.js.map +1 -1
  13. package/dist/core/core-ports.d.ts +11 -0
  14. package/dist/core/core-ports.d.ts.map +1 -1
  15. package/dist/core/core-ports.js.map +1 -1
  16. package/dist/core/create-core-server.d.ts.map +1 -1
  17. package/dist/core/create-core-server.js +13 -2
  18. package/dist/core/create-core-server.js.map +1 -1
  19. package/dist/core/create-core-services.d.ts +9 -0
  20. package/dist/core/create-core-services.d.ts.map +1 -1
  21. package/dist/core/create-core-services.js +14 -4
  22. package/dist/core/create-core-services.js.map +1 -1
  23. package/dist/index.d.ts +1 -1
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +2 -2
  26. package/dist/index.js.map +1 -1
  27. package/dist/modules/access/access-control.interface.d.ts +9 -0
  28. package/dist/modules/access/access-control.interface.d.ts.map +1 -1
  29. package/dist/modules/access/access-control.service.d.ts +1 -0
  30. package/dist/modules/access/access-control.service.d.ts.map +1 -1
  31. package/dist/modules/access/access-control.service.js +16 -0
  32. package/dist/modules/access/access-control.service.js.map +1 -1
  33. package/dist/modules/agent-guide/agent-guide.d.ts +139 -0
  34. package/dist/modules/agent-guide/agent-guide.d.ts.map +1 -0
  35. package/dist/modules/agent-guide/agent-guide.js +191 -0
  36. package/dist/modules/agent-guide/agent-guide.js.map +1 -0
  37. package/dist/modules/agent-guide/agent-guide.tools.d.ts +24 -0
  38. package/dist/modules/agent-guide/agent-guide.tools.d.ts.map +1 -0
  39. package/dist/modules/agent-guide/agent-guide.tools.js +100 -0
  40. package/dist/modules/agent-guide/agent-guide.tools.js.map +1 -0
  41. package/dist/modules/agent-guide/index.d.ts +4 -0
  42. package/dist/modules/agent-guide/index.d.ts.map +1 -0
  43. package/dist/modules/agent-guide/index.js +4 -0
  44. package/dist/modules/agent-guide/index.js.map +1 -0
  45. package/dist/modules/agent-instructions/agent-instructions.routes.d.ts +3 -2
  46. package/dist/modules/agent-instructions/agent-instructions.routes.d.ts.map +1 -1
  47. package/dist/modules/agent-instructions/agent-instructions.routes.js +3 -2
  48. package/dist/modules/agent-instructions/agent-instructions.routes.js.map +1 -1
  49. package/dist/modules/agent-instructions/compose.d.ts +9 -6
  50. package/dist/modules/agent-instructions/compose.d.ts.map +1 -1
  51. package/dist/modules/agent-instructions/compose.js +9 -6
  52. package/dist/modules/agent-instructions/compose.js.map +1 -1
  53. package/dist/modules/agent-instructions/index.d.ts +1 -1
  54. package/dist/modules/agent-instructions/index.d.ts.map +1 -1
  55. package/dist/modules/agent-instructions/index.js +1 -1
  56. package/dist/modules/agent-instructions/index.js.map +1 -1
  57. package/dist/modules/agent-instructions/shared-file-rules.d.ts +10 -50
  58. package/dist/modules/agent-instructions/shared-file-rules.d.ts.map +1 -1
  59. package/dist/modules/agent-instructions/shared-file-rules.js +32 -85
  60. package/dist/modules/agent-instructions/shared-file-rules.js.map +1 -1
  61. package/dist/modules/mcp/mcp.service.d.ts +29 -2
  62. package/dist/modules/mcp/mcp.service.d.ts.map +1 -1
  63. package/dist/modules/mcp/mcp.service.js +113 -16
  64. package/dist/modules/mcp/mcp.service.js.map +1 -1
  65. package/dist/modules/mcp/tool-schema-guard.d.ts +105 -0
  66. package/dist/modules/mcp/tool-schema-guard.d.ts.map +1 -0
  67. package/dist/modules/mcp/tool-schema-guard.js +171 -0
  68. package/dist/modules/mcp/tool-schema-guard.js.map +1 -0
  69. package/dist/modules/plugins/plugins.tools.d.ts +36 -2
  70. package/dist/modules/plugins/plugins.tools.d.ts.map +1 -1
  71. package/dist/modules/plugins/plugins.tools.js +71 -14
  72. package/dist/modules/plugins/plugins.tools.js.map +1 -1
  73. package/dist/modules/settings/deployment-settings.service.d.ts +0 -7
  74. package/dist/modules/settings/deployment-settings.service.d.ts.map +1 -1
  75. package/dist/modules/settings/deployment-settings.service.js +14 -53
  76. package/dist/modules/settings/deployment-settings.service.js.map +1 -1
  77. package/dist/modules/settings/setup.routes.d.ts.map +1 -1
  78. package/dist/modules/settings/setup.routes.js +3 -6
  79. package/dist/modules/settings/setup.routes.js.map +1 -1
  80. package/dist/modules/skills/skills.tools.d.ts.map +1 -1
  81. package/dist/modules/skills/skills.tools.js +58 -16
  82. package/dist/modules/skills/skills.tools.js.map +1 -1
  83. package/dist/modules/tool-manuals/tool-manuals.contract.d.ts +23 -4
  84. package/dist/modules/tool-manuals/tool-manuals.contract.d.ts.map +1 -1
  85. package/dist/modules/tool-manuals/tool-manuals.contract.js.map +1 -1
  86. package/dist/modules/tool-manuals/tool-manuals.service.d.ts +4 -0
  87. package/dist/modules/tool-manuals/tool-manuals.service.d.ts.map +1 -1
  88. package/dist/modules/tool-manuals/tool-manuals.service.js +14 -0
  89. package/dist/modules/tool-manuals/tool-manuals.service.js.map +1 -1
  90. package/dist/modules/tool-manuals/tool-manuals.tools.d.ts +7 -0
  91. package/dist/modules/tool-manuals/tool-manuals.tools.d.ts.map +1 -1
  92. package/dist/modules/tool-manuals/tool-manuals.tools.js +66 -36
  93. package/dist/modules/tool-manuals/tool-manuals.tools.js.map +1 -1
  94. package/dist/modules/tool-registry/description-length.d.ts +14 -14
  95. package/dist/modules/tool-registry/description-length.d.ts.map +1 -1
  96. package/dist/modules/tool-registry/description-length.js +24 -26
  97. package/dist/modules/tool-registry/description-length.js.map +1 -1
  98. package/dist/modules/tool-registry/guide-first.d.ts +23 -0
  99. package/dist/modules/tool-registry/guide-first.d.ts.map +1 -0
  100. package/dist/modules/tool-registry/guide-first.js +32 -0
  101. package/dist/modules/tool-registry/guide-first.js.map +1 -0
  102. package/dist/modules/tool-registry/tool-registry.d.ts +6 -0
  103. package/dist/modules/tool-registry/tool-registry.d.ts.map +1 -1
  104. package/dist/modules/tool-registry/tool-registry.js +9 -2
  105. package/dist/modules/tool-registry/tool-registry.js.map +1 -1
  106. package/dist/modules/workflow/agent-tools/change-request-read-shape.d.ts +449 -0
  107. package/dist/modules/workflow/agent-tools/change-request-read-shape.d.ts.map +1 -0
  108. package/dist/modules/workflow/agent-tools/change-request-read-shape.js +481 -0
  109. package/dist/modules/workflow/agent-tools/change-request-read-shape.js.map +1 -0
  110. package/dist/modules/workflow/agent-tools/change-request-read.tools.d.ts +73 -0
  111. package/dist/modules/workflow/agent-tools/change-request-read.tools.d.ts.map +1 -0
  112. package/dist/modules/workflow/agent-tools/change-request-read.tools.js +582 -0
  113. package/dist/modules/workflow/agent-tools/change-request-read.tools.js.map +1 -0
  114. package/dist/modules/workflow/agent-tools/change-request-summary.d.ts +12 -1
  115. package/dist/modules/workflow/agent-tools/change-request-summary.d.ts.map +1 -1
  116. package/dist/modules/workflow/agent-tools/change-request-summary.js +5 -1
  117. package/dist/modules/workflow/agent-tools/change-request-summary.js.map +1 -1
  118. package/dist/modules/workflow/agent-tools/workflow.tools.d.ts.map +1 -1
  119. package/dist/modules/workflow/agent-tools/workflow.tools.js +9 -0
  120. package/dist/modules/workflow/agent-tools/workflow.tools.js.map +1 -1
  121. package/dist/modules/workflow/git/git.service.d.ts +210 -13
  122. package/dist/modules/workflow/git/git.service.d.ts.map +1 -1
  123. package/dist/modules/workflow/git/git.service.js +456 -91
  124. package/dist/modules/workflow/git/git.service.js.map +1 -1
  125. package/dist/modules/workflow/git/merge-commit.d.ts +73 -0
  126. package/dist/modules/workflow/git/merge-commit.d.ts.map +1 -0
  127. package/dist/modules/workflow/git/merge-commit.js +89 -0
  128. package/dist/modules/workflow/git/merge-commit.js.map +1 -0
  129. package/dist/modules/workflow/git/pull-request.service.d.ts +94 -1
  130. package/dist/modules/workflow/git/pull-request.service.d.ts.map +1 -1
  131. package/dist/modules/workflow/git/pull-request.service.js +332 -37
  132. package/dist/modules/workflow/git/pull-request.service.js.map +1 -1
  133. package/dist/modules/workflow/review-workflow/review-workflow.service.d.ts +35 -0
  134. package/dist/modules/workflow/review-workflow/review-workflow.service.d.ts.map +1 -1
  135. package/dist/modules/workflow/review-workflow/review-workflow.service.js +178 -12
  136. package/dist/modules/workflow/review-workflow/review-workflow.service.js.map +1 -1
  137. package/dist/modules/workflow/workflow.routes.d.ts +6 -2
  138. package/dist/modules/workflow/workflow.routes.d.ts.map +1 -1
  139. package/dist/modules/workflow/workflow.routes.js +7 -2
  140. package/dist/modules/workflow/workflow.routes.js.map +1 -1
  141. package/dist/modules/workflow/workflow.service.d.ts +4 -0
  142. package/dist/modules/workflow/workflow.service.d.ts.map +1 -1
  143. package/dist/modules/workflow/workflow.service.js +3 -0
  144. package/dist/modules/workflow/workflow.service.js.map +1 -1
  145. package/dist/modules/workspace/startup/steps/seed-tree.d.ts.map +1 -1
  146. package/dist/modules/workspace/startup/steps/seed-tree.js +22 -27
  147. package/dist/modules/workspace/startup/steps/seed-tree.js.map +1 -1
  148. package/dist/modules/workspace/startup/steps/template-files.step.d.ts +58 -52
  149. package/dist/modules/workspace/startup/steps/template-files.step.d.ts.map +1 -1
  150. package/dist/modules/workspace/startup/steps/template-files.step.js +209 -223
  151. package/dist/modules/workspace/startup/steps/template-files.step.js.map +1 -1
  152. package/dist/modules/workspace/startup/steps/template-source.d.ts +5 -3
  153. package/dist/modules/workspace/startup/steps/template-source.d.ts.map +1 -1
  154. package/dist/modules/workspace/startup/steps/template-source.js +5 -3
  155. package/dist/modules/workspace/startup/steps/template-source.js.map +1 -1
  156. package/dist/modules/workspace/workspace.tools.d.ts +10 -1
  157. package/dist/modules/workspace/workspace.tools.d.ts.map +1 -1
  158. package/dist/modules/workspace/workspace.tools.js +211 -18
  159. package/dist/modules/workspace/workspace.tools.js.map +1 -1
  160. package/dist/shared/domain-errors.d.ts +11 -0
  161. package/dist/shared/domain-errors.d.ts.map +1 -1
  162. package/dist/shared/domain-errors.js +14 -0
  163. package/dist/shared/domain-errors.js.map +1 -1
  164. package/dist/shared/hidden-tools.d.ts +44 -0
  165. package/dist/shared/hidden-tools.d.ts.map +1 -0
  166. package/dist/shared/hidden-tools.js +13 -0
  167. package/dist/shared/hidden-tools.js.map +1 -0
  168. package/kb-template/.bevelignore +0 -5
  169. package/package.json +4 -3
  170. package/src/__tests__/kb-layout-config.test.ts +10 -100
  171. package/src/__tests__/packaged-assets-ship.test.ts +54 -0
  172. package/src/assets.ts +10 -0
  173. package/src/core/core-ports.ts +11 -0
  174. package/src/core/create-core-server.ts +13 -2
  175. package/src/core/create-core-services.ts +28 -4
  176. package/src/index.ts +2 -2
  177. package/src/modules/access/__tests__/access-control.atref-batch.test.ts +58 -0
  178. package/src/modules/access/__tests__/access-control.platform-restore.test.ts +8 -7
  179. package/src/modules/access/__tests__/access-personal-plugin.test.ts +1 -18
  180. package/src/modules/access/access-control.interface.ts +15 -0
  181. package/src/modules/access/access-control.service.ts +21 -0
  182. package/src/modules/agent-guide/__tests__/agent-guide.test.ts +328 -0
  183. package/src/modules/agent-guide/__tests__/agent-guide.tools.test.ts +189 -0
  184. package/src/modules/agent-guide/agent-guide.tools.ts +122 -0
  185. package/src/modules/agent-guide/agent-guide.ts +291 -0
  186. package/src/modules/agent-guide/index.ts +21 -0
  187. package/src/modules/agent-instructions/__tests__/shared-file-rules.test.ts +28 -121
  188. package/src/modules/agent-instructions/agent-instructions.routes.ts +3 -2
  189. package/src/modules/agent-instructions/compose.ts +9 -6
  190. package/src/modules/agent-instructions/index.ts +0 -3
  191. package/src/modules/agent-instructions/shared-file-rules.ts +31 -93
  192. package/src/modules/mcp/__tests__/fake-downstream-mcp-server.ts +14 -3
  193. package/src/modules/mcp/__tests__/mcp.e2e.test.ts +250 -0
  194. package/src/modules/mcp/__tests__/mcp.service.test.ts +31 -23
  195. package/src/modules/mcp/__tests__/tool-schema-guard.test.ts +266 -0
  196. package/src/modules/mcp/mcp.service.ts +137 -19
  197. package/src/modules/mcp/tool-schema-guard.ts +196 -0
  198. package/src/modules/plugins/__tests__/plugins.tools.test.ts +154 -4
  199. package/src/modules/plugins/plugins.tools.ts +75 -15
  200. package/src/modules/settings/__tests__/deployment-settings.service.test.ts +26 -55
  201. package/src/modules/settings/deployment-settings.service.ts +13 -54
  202. package/src/modules/settings/setup.routes.ts +3 -6
  203. package/src/modules/skills/__tests__/skills.tools.description.test.ts +91 -0
  204. package/src/modules/skills/skills.tools.ts +62 -16
  205. package/src/modules/tool-manuals/__tests__/tool-manuals.detail.route.test.ts +57 -0
  206. package/src/modules/tool-manuals/__tests__/tool-manuals.tools.test.ts +73 -4
  207. package/src/modules/tool-manuals/tool-manuals.contract.ts +24 -4
  208. package/src/modules/tool-manuals/tool-manuals.service.ts +17 -0
  209. package/src/modules/tool-manuals/tool-manuals.tools.ts +74 -36
  210. package/src/modules/tool-registry/__tests__/own-tool-schemas.test.ts +160 -0
  211. package/src/modules/tool-registry/__tests__/tool-description-length.test.ts +61 -59
  212. package/src/modules/tool-registry/description-length.ts +24 -26
  213. package/src/modules/tool-registry/guide-first.ts +34 -0
  214. package/src/modules/tool-registry/tool-registry.ts +9 -2
  215. package/src/modules/workflow/__tests__/apply-failure.test.ts +6 -1
  216. package/src/modules/workflow/agent-tools/__tests__/change-request-read-shape.test.ts +705 -0
  217. package/src/modules/workflow/agent-tools/__tests__/change-request-read.tools.test.ts +1518 -0
  218. package/src/modules/workflow/agent-tools/__tests__/workflow.tools.test.ts +23 -2
  219. package/src/modules/workflow/agent-tools/change-request-read-shape.ts +712 -0
  220. package/src/modules/workflow/agent-tools/change-request-read.tools.ts +724 -0
  221. package/src/modules/workflow/agent-tools/change-request-summary.ts +5 -1
  222. package/src/modules/workflow/agent-tools/workflow.tools.ts +8 -0
  223. package/src/modules/workflow/git/__tests__/git.service.appliedChange.test.ts +285 -0
  224. package/src/modules/workflow/git/__tests__/git.service.changedFilesForPr.test.ts +124 -0
  225. package/src/modules/workflow/git/__tests__/git.service.mergeChangeRequest.test.ts +334 -0
  226. package/src/modules/workflow/git/__tests__/pull-request.service.list-fetch.test.ts +72 -2
  227. package/src/modules/workflow/git/__tests__/pull-request.service.placeholder.test.ts +24 -2
  228. package/src/modules/workflow/git/__tests__/pull-request.service.test.ts +620 -1
  229. package/src/modules/workflow/git/git.service.ts +537 -94
  230. package/src/modules/workflow/git/merge-commit.ts +88 -0
  231. package/src/modules/workflow/git/pull-request.service.ts +380 -54
  232. package/src/modules/workflow/review-workflow/__tests__/approval-states.test.ts +7 -1
  233. package/src/modules/workflow/review-workflow/__tests__/merge-records-own-commit.test.ts +407 -0
  234. package/src/modules/workflow/review-workflow/review-workflow.service.ts +189 -11
  235. package/src/modules/workflow/workflow.routes.ts +7 -2
  236. package/src/modules/workflow/workflow.service.ts +7 -0
  237. package/src/modules/workspace/__tests__/escape-sequences.routes.test.ts +4 -3
  238. package/src/modules/workspace/__tests__/workspace.routes.move-platform-files.test.ts +21 -10
  239. package/src/modules/workspace/__tests__/workspace.tools.agents-file.test.ts +33 -55
  240. package/src/modules/workspace/__tests__/workspace.tools.test.ts +255 -22
  241. package/src/modules/workspace/startup/steps/__tests__/steps.test.ts +191 -489
  242. package/src/modules/workspace/startup/steps/seed-tree.ts +21 -27
  243. package/src/modules/workspace/startup/steps/template-files.step.ts +217 -249
  244. package/src/modules/workspace/startup/steps/template-source.ts +5 -3
  245. package/src/modules/workspace/workspace.tools.ts +226 -16
  246. package/src/shared/domain-errors.ts +15 -0
  247. package/src/shared/hidden-tools.ts +45 -0
  248. package/kb-template/AGENTS.md +0 -730
@@ -7,6 +7,7 @@ import type { ToolHandlerFactory } from '../tool-helpers/tool-handler.js';
7
7
  import type { IAccessControl } from '../access/access-control.interface.js';
8
8
  import { utcpNamespacedKey } from '../../shared/utcp-namespace.js';
9
9
  import type { IToolManualService } from './tool-manuals.contract.js';
10
+ import type { HiddenToolSource } from '../../shared/hidden-tools.js';
10
11
 
11
12
  /**
12
13
  * What `list_tool_setup` needs from the secrets vault (structurally satisfied
@@ -43,6 +44,12 @@ export function registerToolManualsTools(
43
44
  variableStatus: VariableStatusPort;
44
45
  /** Which branch tools are served from, and its clone. */
45
46
  kb: Pick<KbContext, 'defaultBranch' | 'defaultWorkspaceId'>;
47
+ /**
48
+ * Where a hidden tool's schema finding comes from (the MCP proxy).
49
+ * Optional: a deployment with no MCP surface has nothing to report, and
50
+ * `hiddenTools` is then empty everywhere.
51
+ */
52
+ hiddenTools?: HiddenToolSource;
46
53
  },
47
54
  ): void {
48
55
  registry.registerExternalTool((ctx) => buildListLocalToolsDef(toolManualService, ctx.userEmail));
@@ -66,23 +73,30 @@ export function registerToolManualsTools(
66
73
  const listSetupDef = toolDef({
67
74
  name: 'list_tool_setup',
68
75
  // What each FIELD means is documented on the field, in `outputs` below:
69
- // `setup.kind`, `variables` and `invalid` each carried a paragraph here,
70
- // which made this description two thousand characters and so the first
71
- // thing a client cut. The description says what the tool answers and the
72
- // three things an agent cannot read off a field.
76
+ // `setup.kind`, `variables`, `invalid` and `hiddenTools` each carried a
77
+ // paragraph here, which made this description two thousand characters and
78
+ // so the first thing a client cut. The description says what the tool
79
+ // answers and the four things an agent cannot read off a field.
80
+ //
81
+ // It sits a few characters under `TOOL_DESCRIPTION_CAP` WITH the guide-first
82
+ // sentence the registry puts in front of it (`tool-registry/guide-first.ts`),
83
+ // which is why every sentence here is the short form: the `hiddenTools`
84
+ // clause and that opener both came out of the same budget, and what a
85
+ // hidden tool's entry CONTAINS is on the field below rather than here.
73
86
  description:
74
87
  'Configuration status of every `.tool` the current user can access: what each tool needs set up and what is ' +
75
- 'already configured, as `{ tools, invalid, onBranchOnly, note? }`. Scoped to the CALLER — a `.tool` it cannot ' +
76
- 'READ is absent entirely, and every flag is the caller\'s own state. ' +
77
- 'Secret VALUES are never returned and can never be set through a tool: an admin enters them in the tool editor, ' +
78
- 'and users sign in on /connect rather than typing a value. ' +
79
- 'What gates setting a tool\'s shared secrets is `canWrite` on the `.tool` FILE — per-file access from its ' +
80
- 'frontmatter `write:`/`owner:` verbs and the access.md chain, NOT a platform role: the people who manage the ' +
81
- 'file configure the tool. ' +
88
+ 'already configured, as `{ tools, invalid, onBranchOnly, note? }`. Scoped to the CALLER: a `.tool` it cannot ' +
89
+ 'READ is absent, and every flag is the caller\'s own. ' +
90
+ 'Secret VALUES are never returned and can never be set through a tool: an admin enters them in the tool ' +
91
+ 'editor; users sign in on /connect. ' +
92
+ 'Setting a tool\'s shared secrets is gated by `canWrite` on the `.tool` FILE (its frontmatter ' +
93
+ '`write:`/`owner:` verbs and the access.md chain), NOT by a platform role: who manages the file configures ' +
94
+ 'the tool. ' +
95
+ '`hiddenTools` names the tools Hexis hides from agents for an invalid schema. ' +
82
96
  'The listing is the RELEASED catalog, built from the default branch only: a server or `.tool` you declared on a ' +
83
- 'draft is not listed, not callable and not signed-in-able until that draft is merged. Pass `branch` (the draft ' +
84
- 'you wrote the declaration on) and `onBranchOnly` names every tool declared there that the default branch does ' +
85
- 'not serve yet — open a change request, then ask the user to review and merge it in the app to activate it.',
97
+ 'draft is not listed, callable or signed-in-able until it is merged. Pass `branch` (the draft you wrote the ' +
98
+ 'declaration on) and `onBranchOnly` names every tool declared there that the default branch does not serve ' +
99
+ 'yet — open a change request and ask the user to merge it in the app to activate it.',
86
100
  path: '/api/agent/tools/list_tool_setup',
87
101
  inputs: {
88
102
  type: 'object',
@@ -132,6 +146,22 @@ export function registerToolManualsTools(
132
146
  type: 'boolean',
133
147
  description: 'You may write this `.tool` FILE, which is what gates setting its shared secrets.',
134
148
  },
149
+ hiddenTools: {
150
+ type: 'array',
151
+ description:
152
+ 'Tools of this server kept off every agent surface for an invalid input schema. Empty ' +
153
+ 'unless the caller may write this tool.',
154
+ items: {
155
+ type: 'object',
156
+ properties: {
157
+ name: { type: 'string', description: 'The name the tool would have been offered under.' },
158
+ path: { type: 'string', description: 'JSON Pointer to the place in the schema that is not valid.' },
159
+ reason: { type: 'string', description: 'Why that place is not valid, in the validator\'s words.' },
160
+ marker: { type: 'string', description: 'The one sentence the tool page shows for this tool.' },
161
+ },
162
+ required: ['name', 'path', 'reason', 'marker'],
163
+ },
164
+ },
135
165
  variables: {
136
166
  type: 'array',
137
167
  description:
@@ -156,7 +186,7 @@ export function registerToolManualsTools(
156
186
  },
157
187
  },
158
188
  },
159
- required: ['slug', 'name', 'path', 'type', 'canWrite', 'variables'],
189
+ required: ['slug', 'name', 'path', 'type', 'canWrite', 'hiddenTools', 'variables'],
160
190
  },
161
191
  },
162
192
  invalid: {
@@ -219,27 +249,35 @@ export function registerToolManualsTools(
219
249
  const status = await deps.variableStatus.statusFor(ctx.user.id, allKeys);
220
250
  const statusByKey = new Map(status.map((s) => [s.key, s]));
221
251
  const tools = await Promise.all(
222
- manuals.map(async (m) => ({
223
- slug: m.slug,
224
- name: m.name,
225
- path: m.path,
226
- type: m.type,
227
- setup: m.setup ?? null,
228
- canWrite: await deps.accessControl.canWrite(defaultWs(), ctx.user.email, m.path),
229
- variables: (m.variables ?? []).map((v) => {
230
- const st = statusByKey.get(varKey(m.name, v.name));
231
- const isOAuth = v.oauth != null;
232
- return {
233
- name: v.name,
234
- scope: v.scope,
235
- label: v.label ?? null,
236
- oauth: isOAuth,
237
- adminConfigured: st?.adminConfigured ?? false,
238
- userConfigured: st?.userConfigured ?? false,
239
- authorized: isOAuth ? (st?.userAuthorized ?? false) : null,
240
- };
241
- }),
242
- })),
252
+ manuals.map(async (m) => {
253
+ const canWrite = await deps.accessControl.canWrite(defaultWs(), ctx.user.email, m.path);
254
+ return {
255
+ slug: m.slug,
256
+ name: m.name,
257
+ path: m.path,
258
+ type: m.type,
259
+ setup: m.setup ?? null,
260
+ canWrite,
261
+ // The marker goes only to the people who manage the server — the
262
+ // same verdict that gates setting its shared secrets. A caller who
263
+ // may only READ the tool is told nothing about it: they cannot fix
264
+ // the schema, and the tool is simply not among the ones they can call.
265
+ hiddenTools: canWrite ? (deps.hiddenTools?.hiddenFor(m.name) ?? []) : [],
266
+ variables: (m.variables ?? []).map((v) => {
267
+ const st = statusByKey.get(varKey(m.name, v.name));
268
+ const isOAuth = v.oauth != null;
269
+ return {
270
+ name: v.name,
271
+ scope: v.scope,
272
+ label: v.label ?? null,
273
+ oauth: isOAuth,
274
+ adminConfigured: st?.adminConfigured ?? false,
275
+ userConfigured: st?.userConfigured ?? false,
276
+ authorized: isOAuth ? (st?.userAuthorized ?? false) : null,
277
+ };
278
+ }),
279
+ };
280
+ }),
243
281
  );
244
282
  const onBranchOnly = pending.map((p) => ({ ...p, branch: branch! }));
245
283
  if (onBranchOnly.length === 0) return { tools, invalid, onBranchOnly };
@@ -0,0 +1,160 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import express from 'express';
3
+ import { describe, expect, it } from 'vitest';
4
+ import { inputSchemaDefect } from '@bevel-software/platform-mcp-core';
5
+ import { ToolRegistry } from '../tool-registry.js';
6
+ import { registerWorkflowTools } from '../../workflow/agent-tools/workflow.tools.js';
7
+ import { registerChangeRequestReadTools } from '../../workflow/agent-tools/change-request-read.tools.js';
8
+ import { registerWorkspaceTools } from '../../workspace/workspace.tools.js';
9
+ import { registerSkillsTools } from '../../skills/skills.tools.js';
10
+ import { registerPluginsTools } from '../../plugins/plugins.tools.js';
11
+ import { registerToolManualsTools } from '../../tool-manuals/tool-manuals.tools.js';
12
+ import { registerAgentGuideTool } from '../../agent-guide/agent-guide.tools.js';
13
+ import { ToolDescriptionNotes } from '../../workspace/agent-access.gate.js';
14
+ import { testKbContext } from '../../../__tests__/kb-context.js';
15
+
16
+ /**
17
+ * Every tool Hexis declares itself, checked against the same JSON Schema rules
18
+ * a connected server's tool is checked against.
19
+ *
20
+ * Hexis hides a connected tool whose input schema is invalid, because an AI
21
+ * client would otherwise drop it in silence. The platform's own tools go to
22
+ * those same clients through those same surfaces, so a mistake in one of these
23
+ * schemas costs the tool just as quietly — and it would be Hexis's mistake.
24
+ * This test is the thing that makes shipping one impossible.
25
+ *
26
+ * It registers the DEFS as `create-core-server` does, with stubs for the
27
+ * services (no def-building path touches one: the lazy providers are resolved
28
+ * with no caller context, which is their static form).
29
+ *
30
+ * Add a tool module to the server and this test FAILS until it is added here:
31
+ * `covers every tool module the server registers` reads the server's own source
32
+ * and compares the `register…Tools` calls in it with `MODULES` below. A test
33
+ * that silently skipped a whole module's schemas would be worse than no test,
34
+ * because it would read as if it had checked them.
35
+ */
36
+
37
+ /**
38
+ * A stub service: every method answers with an empty list. The def-building
39
+ * paths that do reach a service only ask it what exists (which skills, which
40
+ * local-only tools) to name them in a description, and "none" is a valid
41
+ * answer that keeps the SCHEMAS — what this test is about — untouched.
42
+ * `then` is excluded so the stub is never mistaken for a promise.
43
+ */
44
+ const nothing = new Proxy(
45
+ {},
46
+ {
47
+ get: (_target, key) => (key === 'then' ? undefined : async () => []),
48
+ },
49
+ ) as never;
50
+
51
+ const pass: express.RequestHandler = (_req, _res, next) => next();
52
+ const handler = (() => pass) as never;
53
+
54
+ /** Where the server registers its tool modules — read, not imported, see below. */
55
+ const CORE_SERVER_SOURCE = new URL('../../../core/create-core-server.ts', import.meta.url);
56
+
57
+ /** Every tool-registering module, by the name the server calls it under. */
58
+ const MODULES: ReadonlyArray<{ name: string; register: (registry: ToolRegistry) => void }> = [
59
+ {
60
+ name: 'registerWorkflowTools',
61
+ register: (registry) => registerWorkflowTools(registry, express.Router(), pass, handler, testKbContext()),
62
+ },
63
+ {
64
+ name: 'registerChangeRequestReadTools',
65
+ register: (registry) =>
66
+ registerChangeRequestReadTools(registry, express.Router(), pass, handler, nothing, testKbContext()),
67
+ },
68
+ {
69
+ name: 'registerWorkspaceTools',
70
+ register: (registry) =>
71
+ registerWorkspaceTools(
72
+ registry,
73
+ express.Router(),
74
+ pass,
75
+ handler,
76
+ nothing,
77
+ nothing,
78
+ nothing,
79
+ testKbContext(),
80
+ { recoveryBotEmail: 'recovery@bevel.software', hooks: nothing, notes: new ToolDescriptionNotes() },
81
+ nothing,
82
+ nothing,
83
+ undefined,
84
+ undefined,
85
+ // The upload store: the two upload tools are mounted only when one is
86
+ // supplied, and the server supplies one — so this harness does too,
87
+ // or their schemas would be the two it silently never checked.
88
+ nothing,
89
+ ),
90
+ },
91
+ {
92
+ name: 'registerSkillsTools',
93
+ register: (registry) => registerSkillsTools(registry, express.Router(), pass, handler, nothing),
94
+ },
95
+ { name: 'registerPluginsTools', register: (registry) => registerPluginsTools(registry, testKbContext()) },
96
+ {
97
+ name: 'registerToolManualsTools',
98
+ register: (registry) =>
99
+ registerToolManualsTools(registry, express.Router(), pass, handler, nothing, {
100
+ accessControl: nothing,
101
+ variableStatus: nothing,
102
+ kb: testKbContext(),
103
+ }),
104
+ },
105
+ {
106
+ // The one module registering a single tool: a provider on both surfaces,
107
+ // built from the sections the reader answers — none here, which still
108
+ // builds the def.
109
+ name: 'registerAgentGuideTool',
110
+ register: (registry) => registerAgentGuideTool(registry, express.Router(), pass, handler, async () => []),
111
+ },
112
+ ];
113
+
114
+ async function toolsOf(modules: ReadonlyArray<(typeof MODULES)[number]>) {
115
+ const registry = new ToolRegistry();
116
+ for (const module of modules) module.register(registry);
117
+ // Both surfaces: the external one is what reaches a connected AI client, the
118
+ // internal one is what the in-app agent calls. Either can carry a bad schema.
119
+ return [...(await registry.listExternal()), ...(await registry.listInternal())];
120
+ }
121
+
122
+ const allOwnTools = () => toolsOf(MODULES);
123
+
124
+ describe("Hexis's own tool schemas", () => {
125
+ /**
126
+ * The harness's own guard, and the reason it reads a source file: nothing in
127
+ * the registry can tell this test about a module nobody registered. A count
128
+ * cannot either — a new module leaves it larger either way. The server's call
129
+ * list can, and it is the only place that knows the whole set.
130
+ */
131
+ it('covers every tool module the server registers', () => {
132
+ const source = readFileSync(CORE_SERVER_SOURCE, 'utf8');
133
+ // `register…Tools` and `register…Tool` alike: a module registering one
134
+ // tool is a module whose schema this harness must check too.
135
+ const registered = [...source.matchAll(/\b(register\w+Tools?)\s*\(/g)].map((m) => m[1]);
136
+ expect([...new Set(registered)].sort()).toEqual(MODULES.map((m) => m.name).sort());
137
+ });
138
+
139
+ it.each(MODULES.map((m) => [m.name, m] as const))('%s contributes tools to check', async (_name, module) => {
140
+ // A module that registers nothing — a renamed registry method, a provider
141
+ // that threw away its defs — would otherwise pass every assertion below
142
+ // while exercising nothing.
143
+ expect(await toolsOf([module])).not.toEqual([]);
144
+ });
145
+
146
+ it('declares valid JSON Schema for every tool, on both surfaces', async () => {
147
+ const defects = (await allOwnTools())
148
+ .map((tool) => ({ tool: tool.name, defect: inputSchemaDefect(tool.inputs) }))
149
+ .filter((entry) => entry.defect !== null);
150
+ // Named, not counted: a failure has to say WHICH tool and WHERE.
151
+ expect(defects).toEqual([]);
152
+ });
153
+
154
+ it('declares an object schema for every tool, as MCP requires of an input schema', async () => {
155
+ const notObjects = (await allOwnTools())
156
+ .filter((tool) => (tool.inputs as { type?: unknown }).type !== 'object')
157
+ .map((tool) => tool.name);
158
+ expect(notObjects).toEqual([]);
159
+ });
160
+ });
@@ -23,13 +23,13 @@ import { UuidSessionSink } from '../../workspace/session-sink.js';
23
23
  import { RoutineWritePolicyService } from '../../workspace/routine-write-policy.js';
24
24
  import { CLIENT_SHORT_CUT, TOOL_DESCRIPTION_CAP, clientVisibleLength, firstSentenceEnd } from '../description-length.js';
25
25
  import {
26
- POINTER_GUIDE_NAME_BUDGET,
27
- SHARED_RULES_POINTER_MAX,
28
26
  TOOL_PREFIX_CAP,
29
27
  sharedFileRules,
30
- sharedRulesPointer,
31
28
  } from '../../agent-instructions/index.js';
32
29
  import { isPlatformFile, platformFilesByDepth } from '@bevel-software/platform-shared';
30
+ import { GET_AGENT_GUIDE_TOOL, GUIDE_FIRST_SENTENCE, guideFirstDescription } from '../guide-first.js';
31
+ import { registerAgentGuideTool } from '../../agent-guide/agent-guide.tools.js';
32
+ import { agentGuideSections } from '../../agent-guide/agent-guide.js';
33
33
 
34
34
  /**
35
35
  * The cap exists because clients cut a long tool description, and they cut it
@@ -62,16 +62,18 @@ const emptySkills = { listSkills: async () => [] } as unknown as Parameters<type
62
62
  /**
63
63
  * The meta-tools as the hosted endpoint builds them (`mcp.service.ts`):
64
64
  * examples written against the namespace it registers the knowledge-base tools
65
- * under and against the catalog it serves, and the chain ending in the pointer.
66
- * So the chain description measured here carries the worked call a client is
67
- * really sent, which is its longest form.
65
+ * under and against the catalog it serves, and the chain without the rules it
66
+ * states only in the shared places. So the chain description measured here
67
+ * carries the worked call a client is really sent, which is its longest form.
68
68
  */
69
- function servedMetaTools(external: readonly UtcpTool[], pointer = sharedRulesPointer(testKbContext().layout)) {
69
+ function servedMetaTools(external: readonly UtcpTool[]) {
70
70
  return codeModeMetaTools(
71
71
  EXTERNAL_KB_MANUAL_NAME,
72
72
  external.map((t) => ({ utcpName: `${EXTERNAL_KB_MANUAL_NAME}.${t.name}`, inputSchema: t.inputs })),
73
- { sharedRulesPointer: pointer },
74
- );
73
+ { sharedRulesPointer: '' },
74
+ // As the hosted endpoint serves them: opening with the guide-first
75
+ // sentence, through the one helper the endpoint itself uses.
76
+ ).map((t) => ({ ...t, description: guideFirstDescription(t.description) }));
75
77
  }
76
78
 
77
79
  /** Every tool Hexis itself registers, on both surfaces, deduplicated by name. */
@@ -103,18 +105,21 @@ async function hexisTools(): Promise<UtcpTool[]> {
103
105
  unused(),
104
106
  );
105
107
  registerWorkflowTools(registry, router, toolAuth, toolHandler, kb);
106
- registerPluginsTools(registry);
108
+ registerPluginsTools(registry, kb);
107
109
  registerSkillsTools(registry, router, toolAuth, toolHandler, emptySkills);
108
110
  registerToolManualsTools(registry, router, toolAuth, toolHandler, emptyManuals, {
109
111
  accessControl: unused(),
110
112
  variableStatus: unused(),
111
113
  kb,
112
114
  });
115
+ // The guide's own tool, with the platform's sections: the one description
116
+ // that lists the sections is measured with the list it really carries.
117
+ registerAgentGuideTool(registry, router, toolAuth, toolHandler, () => agentGuideSections(kb.layout));
113
118
 
114
119
  const byName = new Map<string, UtcpTool>();
115
- // The meta-tools as a client is served them: the chain carries its pointer
116
- // (`mcp.service.ts` appends it at the mount), so the catalog measured here is
117
- // the catalog that goes out rather than the unpointed constant.
120
+ // The meta-tools as a client is served them (`mcp.service.ts` opens each
121
+ // with the guide-first sentence at the mount), so the catalog measured here
122
+ // is the catalog that goes out rather than the bare constant.
118
123
  const external = (await registry.listExternal()) as UtcpTool[];
119
124
  const metaTools = servedMetaTools(external);
120
125
  for (const tool of [...(await registry.listInternal()), ...external, ...metaTools]) {
@@ -141,9 +146,11 @@ describe('no Hexis tool description is long enough to be cut', () => {
141
146
  // The cap does not answer the ~500-character cut; the ORDER of the text
142
147
  // does, and this is where that claim is checked rather than asserted in a
143
148
  // comment. A client that stops at 500 must still have the sentence saying
144
- // what the tool does — what it loses is the pointer tail, and the file the
145
- // pointer names is stated in the handshake instructions and in the guide
146
- // anyway. Lowering the cap to 500 would not buy this; only order does.
149
+ // what the tool does — what it loses is the tail. The shared rules are
150
+ // stated in the guide (which the opener sends every client to, including
151
+ // the description-only ones this cut is about) and, for the clients that
152
+ // honour it, in the handshake instructions as well. Lowering
153
+ // the cap to 500 would not buy this; only order does.
147
154
  const late = (await hexisTools())
148
155
  .map((t) => ({ tool: t.name, endsAt: firstSentenceEnd(t) }))
149
156
  .filter((m) => m.endsAt > CLIENT_SHORT_CUT)
@@ -156,30 +163,26 @@ describe('no Hexis tool description is long enough to be cut', () => {
156
163
  ).toEqual([]);
157
164
  });
158
165
 
159
- it('holds the cap on a deployment that renamed its guide, not only under the default', async () => {
160
- // The pointer ends every file tool's description and its length moves with
161
- // a deployment setting, so a cap checked only against the nine characters
162
- // of `AGENTS.md` guarantees nothing about the catalog a renamed deployment
163
- // serves. Two things make it hold: the pointer is bounded by construction
164
- // (past `POINTER_GUIDE_NAME_BUDGET` the guide is named by its role), and
165
- // `clientVisibleLength` charges the worst case rather than this layout's.
166
+ it('opens every tool but the guide\'s own with the guide-first sentence, and fits with it', async () => {
167
+ // The sentence is in the description the registry lists, so the catalog
168
+ // measured here is the catalog a client gets, sentence included.
166
169
  const tools = await hexisTools();
167
- const atWorst = tools
168
- .map((t) => ({ tool: t.name, chars: clientVisibleLength(t) }))
169
- .filter((m) => m.chars > TOOL_DESCRIPTION_CAP);
170
- expect(atWorst, atWorst.map((m) => `${m.tool}: ${m.chars}`).join('\n')).toEqual([]);
171
-
172
- // And measured literally, under the longest guide name a deployment can
173
- // actually configure: every description still fits.
174
- const longest = `${'x'.repeat(252)}.md`;
175
- const pointerHere = sharedRulesPointer(testKbContext().layout);
176
- const pointerThere = sharedRulesPointer({ ...testKbContext().layout, agentsFile: longest });
177
- expect(pointerThere.length).toBeLessThanOrEqual(SHARED_RULES_POINTER_MAX);
178
- for (const tool of tools) {
179
- if (!tool.description?.endsWith(pointerHere)) continue;
180
- const asRenamed = tool.description.slice(0, -pointerHere.length) + pointerThere;
181
- const chars = clientVisibleLength({ name: tool.name, description: asRenamed });
182
- expect(chars, `${tool.name} on a renamed deployment`).toBeLessThanOrEqual(TOOL_DESCRIPTION_CAP);
170
+ // ONE predicate for both halves, so no description falls between them: a
171
+ // tool whose own description is empty is served the sentence alone, and
172
+ // one with text after it has whitespace between — not `here.Read`.
173
+ const opensWithGuide = (t: UtcpTool): boolean => {
174
+ const description = t.description ?? '';
175
+ if (description === GUIDE_FIRST_SENTENCE) return true;
176
+ return description.startsWith(GUIDE_FIRST_SENTENCE) && /^\s/.test(description.slice(GUIDE_FIRST_SENTENCE.length));
177
+ };
178
+ const opened = tools.filter(opensWithGuide);
179
+ expect(opened.length).toBeGreaterThan(0);
180
+ // The guide's own tool is the one exception: it is what the sentence
181
+ // points at, and it is in the catalog measured here so the cap holds on
182
+ // it too (see the first test).
183
+ expect(tools.filter((t) => !opensWithGuide(t)).map((t) => t.name)).toEqual([GET_AGENT_GUIDE_TOOL]);
184
+ for (const tool of opened) {
185
+ expect(clientVisibleLength(tool), tool.name).toBeLessThanOrEqual(TOOL_DESCRIPTION_CAP);
183
186
  }
184
187
  });
185
188
 
@@ -190,12 +193,10 @@ describe('no Hexis tool description is long enough to be cut', () => {
190
193
  // the catalog the agent reads was over it by 300.
191
194
  const read = (await hexisTools()).find((t) => t.name === 'read_file');
192
195
  expect(read).toBeDefined();
193
- // Its own text, with the pointer charged at its worst case rather than at
194
- // this layout's, plus the prefix at ITS cap and the blank line between.
195
- const pointer = sharedRulesPointer();
196
- const ownAtWorstPointer = read!.description!.length - pointer.length + SHARED_RULES_POINTER_MAX;
197
- expect(clientVisibleLength(read!)).toBe(ownAtWorstPointer + TOOL_PREFIX_CAP + 2);
198
- expect(ownAtWorstPointer + TOOL_PREFIX_CAP + 2).toBeLessThanOrEqual(TOOL_DESCRIPTION_CAP);
196
+ // Its own text (the guide-first sentence included), plus the prefix at
197
+ // ITS cap and the blank line between, must fit — measured as the client
198
+ // sees it.
199
+ expect(clientVisibleLength(read!)).toBeLessThanOrEqual(TOOL_DESCRIPTION_CAP);
199
200
  });
200
201
 
201
202
  it('fails, naming the tool, when a paragraph takes a description over the cap', () => {
@@ -228,7 +229,7 @@ describe('no Hexis tool description is long enough to be cut', () => {
228
229
  });
229
230
  });
230
231
 
231
- describe('every file tool ends with the pointer and carries no shared paragraph', () => {
232
+ describe('every file tool opens with the guide-first sentence and carries no shared paragraph', () => {
232
233
  /** The tools that used to carry the shared paragraphs — every `mount`ed one. */
233
234
  const FILE_TOOLS = [
234
235
  'read_file',
@@ -248,13 +249,12 @@ describe('every file tool ends with the pointer and carries no shared paragraph'
248
249
  'apply_file_upload',
249
250
  ];
250
251
 
251
- it('ends each description with the one sentence naming the shared rules', async () => {
252
+ it('opens each description with the sentence sending the agent to the guide, and ends it as a sentence', async () => {
252
253
  const tools = await hexisTools();
253
- const pointer = sharedRulesPointer(testKbContext().layout);
254
254
  for (const name of FILE_TOOLS) {
255
255
  const def = tools.find((t) => t.name === name);
256
256
  expect(def, name).toBeDefined();
257
- expect(def!.description!.endsWith(pointer), `${name} must end with: ${pointer}`).toBe(true);
257
+ expect(def!.description!.startsWith(`${GUIDE_FIRST_SENTENCE} `), `${name} must open with: ${GUIDE_FIRST_SENTENCE}`).toBe(true);
258
258
  // Ends with a full sentence, so nothing reads as cut off mid-thought.
259
259
  expect(def!.description!.trimEnd().endsWith('.'), name).toBe(true);
260
260
  }
@@ -283,25 +283,27 @@ describe('every file tool ends with the pointer and carries no shared paragraph'
283
283
  });
284
284
 
285
285
  describe('the shared rules describe the tools they name', () => {
286
- it('ends the chain description with the pointer too, under the cap', async () => {
286
+ it('opens the chain description with the guide-first sentence too, under the cap', async () => {
287
287
  // What a chain does with a failure, a large result or an image is true of
288
288
  // every call, so it is stated in the shared rules — and the clients that
289
289
  // drop the handshake `instructions` see only descriptions, so the chain
290
- // gets the same pointer every file tool ends with. Composed at the mount,
291
- // because `mcp-core` may not spell a guide name that is a deployment
292
- // setting.
293
- const pointer = sharedRulesPointer(testKbContext().layout);
290
+ // opens with the same sentence every tool of the platform's own opens
291
+ // with, and ends with no second pointer: the one at the front is the way
292
+ // to the rules. Composed at the mount, because `mcp-core` builds the
293
+ // constant without knowing where this deployment states them.
294
294
  const served = (await hexisTools()).find((t) => t.name === CALL_TOOL_CHAIN_NAME)!;
295
- expect(served.description!.endsWith(pointer)).toBe(true);
295
+ expect(served.description!.startsWith(`${GUIDE_FIRST_SENTENCE} `)).toBe(true);
296
+ expect(served.description!.split(GUIDE_FIRST_SENTENCE)).toHaveLength(2);
297
+ expect(served.description!.trimEnd().endsWith('.')).toBe(true);
296
298
  expect(clientVisibleLength(served)).toBeLessThanOrEqual(TOOL_DESCRIPTION_CAP);
297
- // The other two describe the registry, not what a call does: no pointer.
299
+ // The other two open the same way: the guide comes before the registry too.
298
300
  for (const tool of servedMetaTools([]).filter((t) => t.name !== CALL_TOOL_CHAIN_NAME)) {
299
- expect(tool.description).not.toContain(pointer);
301
+ expect(tool.description!.startsWith(`${GUIDE_FIRST_SENTENCE} `), tool.name).toBe(true);
300
302
  }
301
303
  });
302
304
 
303
305
  it('states what a chain does in the shared rules, and not a second time on the chain', async () => {
304
- // The pointer is only honest if the rules it points at are THERE. These
306
+ // The opener is only honest if the rules it points at are THERE. These
305
307
  // two were paragraphs of the chain's description; they moved, whole, and a
306
308
  // description that kept them as well would be the long one a client cuts.
307
309
  const rule = sharedFileRules(testKbContext().layout).find((r) => r.id === 'tool-chain')!;
@@ -317,7 +319,7 @@ describe('the shared rules describe the tools they name', () => {
317
319
 
318
320
  it('keeps the rules on the chain itself where no shared rules are served', () => {
319
321
  // The standalone bridge proxies a deployment whose guide it cannot name, so
320
- // it passes no pointer — and an agent there must still be told what a chain
322
+ // it passes no pointer at all — and an agent there must still be told what a chain
321
323
  // that timed out, or answered too much, or read an image, does.
322
324
  const [chain] = codeModeMetaTools('hexis', []).filter((t) => t.name === CALL_TOOL_CHAIN_NAME);
323
325
  expect(chain.description).toContain(CHAIN_FAILURES_RULE);
@@ -5,14 +5,15 @@
5
5
  * where the text specific to the tool sits, after whatever shared preamble it
6
6
  * carried. Agents reported `file_stat`, `read_file`, `write_file` and
7
7
  * `write_files` arriving ending in "[truncated]". The rules those descriptions
8
- * shared now live in one place (see `agent-instructions/shared-file-rules.ts`)
9
- * and each description ends with one sentence pointing there, which is what
10
- * makes the cap below reachable rather than aspirational.
8
+ * shared now live in one place (see `agent-instructions/shared-file-rules.ts`,
9
+ * served in the guide `get_agent_guide` returns) and each description OPENS
10
+ * with the one sentence sending the agent there (`guide-first.ts`), which is
11
+ * what makes the cap below reachable rather than aspirational.
11
12
  */
12
13
 
13
14
  import { TOOL_PREFIX_CAP } from '@bevel-software/platform-shared';
14
15
  import { PREFIXED_TOOLS } from '../agent-instructions/compose.js';
15
- import { SHARED_RULES_POINTER_MAX, sharedRulesPointer } from '../agent-instructions/shared-file-rules.js';
16
+ import { GUIDE_FIRST_SENTENCE } from './guide-first.js';
16
17
  import type { UtcpTool } from './tool.contract.js';
17
18
 
18
19
  /**
@@ -25,8 +26,10 @@ import type { UtcpTool } from './tool.contract.js';
25
26
  * could answer: no useful description of `move_file` fits in 500. What answers
26
27
  * that one is the ORDER of the text, which is why the deployment's purpose line
27
28
  * is prepended rather than appended (see `prefixToolDescription`) and why every
28
- * description now leads with what the tool does and ends with the pointer: a
29
- * cut at 500 then takes the pointer and leaves the tool. The other cut is the
29
+ * description now opens with the guide-first sentence and then what the tool
30
+ * does, with the tool's own detail last: a cut at 500 keeps the sentence and
31
+ * the tool's first sentence (see `firstSentenceEnd`) and takes the detail,
32
+ * which the guide states in full anyway. The other cut is the
30
33
  * four-figure one agents reported on `file_stat`, `read_file`, `write_file` and
31
34
  * `write_files`, and 1,200 sits below it with room to spare.
32
35
  *
@@ -60,9 +63,10 @@ export const CLIENT_SHORT_CUT = 500;
60
63
 
61
64
  /**
62
65
  * Where the tool's OWN opening sentence ends in the text a client is handed:
63
- * the purpose prefix counted at its cap, as in {@link clientVisibleLength}, and
64
- * the pointer not counted at all, since it is the part a short cut is meant to
65
- * take.
66
+ * the purpose prefix counted at its cap, as in {@link clientVisibleLength},
67
+ * and the guide-first sentence every listed tool opens with counted as the
68
+ * text it is — a client that cuts at 500 must still reach the sentence saying
69
+ * what the tool does, past that one.
66
70
  *
67
71
  * A description with no sentence-ending punctuation counts whole — the honest
68
72
  * answer for text that never finishes a sentence.
@@ -71,10 +75,15 @@ export function firstSentenceEnd(tool: Pick<UtcpTool, 'name' | 'description'>):
71
75
  const prefix = PREFIXED_TOOLS.has(tool.name) ? TOOL_PREFIX_CAP + 2 : 0;
72
76
  const description = tool.description ?? '';
73
77
  if (description === '') return prefix;
74
- const pointer = sharedRulesPointer();
75
- const own = description.endsWith(pointer) ? description.slice(0, -pointer.length) : description;
78
+ // The opener and whatever whitespace follows it: `guideFirstDescription`
79
+ // joins with one space, and leaves a description that already opens with
80
+ // the sentence as it came — a newline after it is still the opener's.
81
+ const opener = description.startsWith(GUIDE_FIRST_SENTENCE)
82
+ ? GUIDE_FIRST_SENTENCE.length + (description.slice(GUIDE_FIRST_SENTENCE.length).match(/^\s*/)?.[0].length ?? 0)
83
+ : 0;
84
+ const own = description.slice(opener);
76
85
  const firstSentence = own.match(/^[\s\S]*?[.!?](?=\s|$)/)?.[0] ?? own;
77
- return prefix + firstSentence.length;
86
+ return prefix + opener + firstSentence.length;
78
87
  }
79
88
 
80
89
  /**
@@ -85,12 +94,8 @@ export function firstSentenceEnd(tool: Pick<UtcpTool, 'name' | 'description'>):
85
94
  * the cap is what an admin may grow their text to without being told, so a
86
95
  * description that only fits beside a short prefix does not really fit.
87
96
  *
88
- * The pointer sentence is measured the same way, for the same reason. It ends
89
- * every file tool's description and its length moves with a DEPLOYMENT SETTING
90
- * — the guide's file name — so a description measured beside the nine
91
- * characters of `AGENTS.md` would pass here and arrive cut on a deployment
92
- * that renamed its guide. Whatever pointer a description actually carries is
93
- * discounted and charged at {@link SHARED_RULES_POINTER_MAX} instead.
97
+ * The guide-first sentence every listed tool opens with is already in the
98
+ * description the registry lists, so it is measured as the text it is.
94
99
  */
95
100
  export function clientVisibleLength(tool: Pick<UtcpTool, 'name' | 'description'>): number {
96
101
  const own = tool.description?.length ?? 0;
@@ -99,13 +104,6 @@ export function clientVisibleLength(tool: Pick<UtcpTool, 'name' | 'description'>
99
104
  // blank line after it. Measuring that as zero would under-report the only
100
105
  // text the client got.
101
106
  if (own === 0) return PREFIXED_TOOLS.has(tool.name) ? TOOL_PREFIX_CAP : 0;
102
- // The pointer at its worst case rather than at this layout's: swap the one
103
- // it carries for the longest it could be. A description that does not end
104
- // with it (`start_session`, the proxied tools) is charged nothing.
105
- const pointer = sharedRulesPointer();
106
- const atWorstPointer = tool.description!.endsWith(pointer)
107
- ? own - pointer.length + SHARED_RULES_POINTER_MAX
108
- : own;
109
107
  // `+ 2` for the blank line `prefixToolDescription` puts between the two.
110
- return PREFIXED_TOOLS.has(tool.name) ? atWorstPointer + TOOL_PREFIX_CAP + 2 : atWorstPointer;
108
+ return PREFIXED_TOOLS.has(tool.name) ? own + TOOL_PREFIX_CAP + 2 : own;
111
109
  }