@bevel-software/platform-core-backend 0.25.0 → 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 (254) 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/kb-startup-runner.d.ts +70 -0
  146. package/dist/modules/workspace/startup/kb-startup-runner.d.ts.map +1 -1
  147. package/dist/modules/workspace/startup/kb-startup-runner.js +213 -20
  148. package/dist/modules/workspace/startup/kb-startup-runner.js.map +1 -1
  149. package/dist/modules/workspace/startup/steps/seed-tree.d.ts.map +1 -1
  150. package/dist/modules/workspace/startup/steps/seed-tree.js +22 -27
  151. package/dist/modules/workspace/startup/steps/seed-tree.js.map +1 -1
  152. package/dist/modules/workspace/startup/steps/template-files.step.d.ts +58 -52
  153. package/dist/modules/workspace/startup/steps/template-files.step.d.ts.map +1 -1
  154. package/dist/modules/workspace/startup/steps/template-files.step.js +209 -223
  155. package/dist/modules/workspace/startup/steps/template-files.step.js.map +1 -1
  156. package/dist/modules/workspace/startup/steps/template-source.d.ts +5 -3
  157. package/dist/modules/workspace/startup/steps/template-source.d.ts.map +1 -1
  158. package/dist/modules/workspace/startup/steps/template-source.js +5 -3
  159. package/dist/modules/workspace/startup/steps/template-source.js.map +1 -1
  160. package/dist/modules/workspace/workspace.tools.d.ts +10 -1
  161. package/dist/modules/workspace/workspace.tools.d.ts.map +1 -1
  162. package/dist/modules/workspace/workspace.tools.js +211 -18
  163. package/dist/modules/workspace/workspace.tools.js.map +1 -1
  164. package/dist/shared/domain-errors.d.ts +11 -0
  165. package/dist/shared/domain-errors.d.ts.map +1 -1
  166. package/dist/shared/domain-errors.js +14 -0
  167. package/dist/shared/domain-errors.js.map +1 -1
  168. package/dist/shared/hidden-tools.d.ts +44 -0
  169. package/dist/shared/hidden-tools.d.ts.map +1 -0
  170. package/dist/shared/hidden-tools.js +13 -0
  171. package/dist/shared/hidden-tools.js.map +1 -0
  172. package/kb-template/.bevelignore +0 -5
  173. package/package.json +4 -3
  174. package/src/__tests__/kb-layout-config.test.ts +10 -100
  175. package/src/__tests__/packaged-assets-ship.test.ts +54 -0
  176. package/src/assets.ts +10 -0
  177. package/src/core/core-ports.ts +11 -0
  178. package/src/core/create-core-server.ts +13 -2
  179. package/src/core/create-core-services.ts +28 -4
  180. package/src/index.ts +2 -2
  181. package/src/modules/access/__tests__/access-control.atref-batch.test.ts +58 -0
  182. package/src/modules/access/__tests__/access-control.platform-restore.test.ts +8 -7
  183. package/src/modules/access/__tests__/access-personal-plugin.test.ts +1 -18
  184. package/src/modules/access/access-control.interface.ts +15 -0
  185. package/src/modules/access/access-control.service.ts +21 -0
  186. package/src/modules/agent-guide/__tests__/agent-guide.test.ts +328 -0
  187. package/src/modules/agent-guide/__tests__/agent-guide.tools.test.ts +189 -0
  188. package/src/modules/agent-guide/agent-guide.tools.ts +122 -0
  189. package/src/modules/agent-guide/agent-guide.ts +291 -0
  190. package/src/modules/agent-guide/index.ts +21 -0
  191. package/src/modules/agent-instructions/__tests__/shared-file-rules.test.ts +28 -121
  192. package/src/modules/agent-instructions/agent-instructions.routes.ts +3 -2
  193. package/src/modules/agent-instructions/compose.ts +9 -6
  194. package/src/modules/agent-instructions/index.ts +0 -3
  195. package/src/modules/agent-instructions/shared-file-rules.ts +31 -93
  196. package/src/modules/mcp/__tests__/fake-downstream-mcp-server.ts +14 -3
  197. package/src/modules/mcp/__tests__/mcp.e2e.test.ts +250 -0
  198. package/src/modules/mcp/__tests__/mcp.service.test.ts +31 -23
  199. package/src/modules/mcp/__tests__/tool-schema-guard.test.ts +266 -0
  200. package/src/modules/mcp/mcp.service.ts +137 -19
  201. package/src/modules/mcp/tool-schema-guard.ts +196 -0
  202. package/src/modules/plugins/__tests__/plugins.tools.test.ts +154 -4
  203. package/src/modules/plugins/plugins.tools.ts +75 -15
  204. package/src/modules/settings/__tests__/deployment-settings.service.test.ts +26 -55
  205. package/src/modules/settings/deployment-settings.service.ts +13 -54
  206. package/src/modules/settings/setup.routes.ts +3 -6
  207. package/src/modules/skills/__tests__/skills.tools.description.test.ts +91 -0
  208. package/src/modules/skills/skills.tools.ts +62 -16
  209. package/src/modules/tool-manuals/__tests__/tool-manuals.detail.route.test.ts +57 -0
  210. package/src/modules/tool-manuals/__tests__/tool-manuals.tools.test.ts +73 -4
  211. package/src/modules/tool-manuals/tool-manuals.contract.ts +24 -4
  212. package/src/modules/tool-manuals/tool-manuals.service.ts +17 -0
  213. package/src/modules/tool-manuals/tool-manuals.tools.ts +74 -36
  214. package/src/modules/tool-registry/__tests__/own-tool-schemas.test.ts +160 -0
  215. package/src/modules/tool-registry/__tests__/tool-description-length.test.ts +61 -59
  216. package/src/modules/tool-registry/description-length.ts +24 -26
  217. package/src/modules/tool-registry/guide-first.ts +34 -0
  218. package/src/modules/tool-registry/tool-registry.ts +9 -2
  219. package/src/modules/workflow/__tests__/apply-failure.test.ts +6 -1
  220. package/src/modules/workflow/agent-tools/__tests__/change-request-read-shape.test.ts +705 -0
  221. package/src/modules/workflow/agent-tools/__tests__/change-request-read.tools.test.ts +1518 -0
  222. package/src/modules/workflow/agent-tools/__tests__/workflow.tools.test.ts +23 -2
  223. package/src/modules/workflow/agent-tools/change-request-read-shape.ts +712 -0
  224. package/src/modules/workflow/agent-tools/change-request-read.tools.ts +724 -0
  225. package/src/modules/workflow/agent-tools/change-request-summary.ts +5 -1
  226. package/src/modules/workflow/agent-tools/workflow.tools.ts +8 -0
  227. package/src/modules/workflow/git/__tests__/git.service.appliedChange.test.ts +285 -0
  228. package/src/modules/workflow/git/__tests__/git.service.changedFilesForPr.test.ts +124 -0
  229. package/src/modules/workflow/git/__tests__/git.service.mergeChangeRequest.test.ts +334 -0
  230. package/src/modules/workflow/git/__tests__/pull-request.service.list-fetch.test.ts +72 -2
  231. package/src/modules/workflow/git/__tests__/pull-request.service.placeholder.test.ts +24 -2
  232. package/src/modules/workflow/git/__tests__/pull-request.service.test.ts +620 -1
  233. package/src/modules/workflow/git/git.service.ts +537 -94
  234. package/src/modules/workflow/git/merge-commit.ts +88 -0
  235. package/src/modules/workflow/git/pull-request.service.ts +380 -54
  236. package/src/modules/workflow/review-workflow/__tests__/approval-states.test.ts +7 -1
  237. package/src/modules/workflow/review-workflow/__tests__/merge-records-own-commit.test.ts +407 -0
  238. package/src/modules/workflow/review-workflow/review-workflow.service.ts +189 -11
  239. package/src/modules/workflow/workflow.routes.ts +7 -2
  240. package/src/modules/workflow/workflow.service.ts +7 -0
  241. package/src/modules/workspace/__tests__/escape-sequences.routes.test.ts +4 -3
  242. package/src/modules/workspace/__tests__/workspace.routes.move-platform-files.test.ts +21 -10
  243. package/src/modules/workspace/__tests__/workspace.tools.agents-file.test.ts +33 -55
  244. package/src/modules/workspace/__tests__/workspace.tools.test.ts +255 -22
  245. package/src/modules/workspace/startup/__tests__/kb-startup-runner.test.ts +231 -1
  246. package/src/modules/workspace/startup/kb-startup-runner.ts +216 -19
  247. package/src/modules/workspace/startup/steps/__tests__/steps.test.ts +191 -489
  248. package/src/modules/workspace/startup/steps/seed-tree.ts +21 -27
  249. package/src/modules/workspace/startup/steps/template-files.step.ts +217 -249
  250. package/src/modules/workspace/startup/steps/template-source.ts +5 -3
  251. package/src/modules/workspace/workspace.tools.ts +226 -16
  252. package/src/shared/domain-errors.ts +15 -0
  253. package/src/shared/hidden-tools.ts +45 -0
  254. package/kb-template/AGENTS.md +0 -730
@@ -40,9 +40,11 @@ export const TEMPLATE_SOURCE_FALLBACKS: ReadonlyMap<string, string> = new Map([
40
40
  ]);
41
41
 
42
42
  /**
43
- * Where the managed guide asks for the rules every file tool shares. Not a
44
- * layout placeholder: the layout renderer lives in `platform-shared`, which
45
- * the frontend also reads, and the rules are backend text.
43
+ * Where a template file may ask for the rules every file tool shares. The
44
+ * packaged template no longer does — the guide is composed in code, see
45
+ * `modules/agent-guide` — but a distribution's template may. Not a layout
46
+ * placeholder: the layout renderer lives in `platform-shared`, which the
47
+ * frontend also reads, and the rules are backend text.
46
48
  */
47
49
  export const SHARED_FILE_RULES_PLACEHOLDER = '{{sharedFileRules}}';
48
50
 
@@ -58,9 +58,15 @@ import {
58
58
  } from '@bevel-software/platform-shared';
59
59
  import type { KbContext } from '../../shared/kb-context.js';
60
60
  import { AccessDeniedError } from '../access-model/access-errors.js';
61
+ import {
62
+ AGENT_GUIDE_FILE,
63
+ isAgentGuidePath,
64
+ isManagedGuide,
65
+ withPlatformGuideAppended,
66
+ type AgentGuideReader,
67
+ } from '../agent-guide/agent-guide.js';
61
68
  import { removeEmptyDirs } from './empty-dirs.js';
62
69
  import { rethrowAsWriteDenial } from './write-denial.js';
63
- import { sharedRulesPointer } from '../agent-instructions/shared-file-rules.js';
64
70
  import type { IChangeReadGate } from '../access-model/change-gate.js';
65
71
  import { notFound, orDeclaredNotFound, orNotFound } from './not-found.js';
66
72
  import { logger } from '../../shared/logging.js';
@@ -730,6 +736,8 @@ async function grepWalk(
730
736
  gate: ReadGate,
731
737
  notifyRead: (path: string) => Promise<void>,
732
738
  docs: DocGrepState,
739
+ /** A file the walk leaves to its caller: never opened, never counted against `max`. */
740
+ skip: (path: string) => boolean = () => false,
733
741
  ): Promise<void> {
734
742
  if (out.length >= max || depth > 12) return;
735
743
  let entries;
@@ -747,7 +755,9 @@ async function grepWalk(
747
755
  if (e.type !== 'directory' && isFolderPlaceholder(e.name)) continue;
748
756
  const p = dir ? `${dir}/${e.name}` : e.name;
749
757
  if (e.type === 'directory') {
750
- await grepWalk(fs, p, re, out, max, depth + 1, gate, notifyRead, docs);
758
+ await grepWalk(fs, p, re, out, max, depth + 1, gate, notifyRead, docs, skip);
759
+ } else if (skip(p)) {
760
+ continue;
751
761
  } else {
752
762
  // Opening a file is a read of it, even when the walk started at a root
753
763
  // the read hook was already told about — so every file the walk opens
@@ -857,6 +867,14 @@ export function registerWorkspaceTools(
857
867
  * two tools are not mounted at all rather than mounted and broken.
858
868
  */
859
869
  uploads?: AgentUploadStore,
870
+ /**
871
+ * The platform's agent guide (see `modules/agent-guide`), which `read_file`
872
+ * and `file_stat` answer at the guide's name in the repository root — after
873
+ * the knowledge base's own file of that name, when it has one. Optional for
874
+ * the harnesses that are about the file primitives; without it the two
875
+ * tools read the disk and nothing else.
876
+ */
877
+ agentGuide?: AgentGuideReader,
860
878
  ): void {
861
879
  const { kbDirName } = kb;
862
880
  /**
@@ -1490,8 +1508,7 @@ export function registerWorkspaceTools(
1490
1508
  const requiresBranch = ((spec.inputs as { required?: string[] }).required ?? []).includes('branch');
1491
1509
  const describe = (): string =>
1492
1510
  (typeof spec.description === 'function' ? spec.description() : spec.description) +
1493
- (spec.gated ? agentAccessGate.notes.gatedToolNote() : '') +
1494
- sharedRulesPointer(kb.layout);
1511
+ (spec.gated ? agentAccessGate.notes.gatedToolNote() : '');
1495
1512
  const def = toolDef({
1496
1513
  name: spec.name,
1497
1514
  description: describe(),
@@ -1623,7 +1640,7 @@ export function registerWorkspaceTools(
1623
1640
  const startSessionDef = toolDef({
1624
1641
  name: 'start_session',
1625
1642
  description:
1626
- 'Mint the id of this conversation, which the KnowledgeBase tools take as `sessionId`. Call this ONCE, at the start of your work and only once per run — minting a new id mid-run starts a second conversation as far as the server is concerned. The id is also a chat session in the app, so you can hand the SAME id to the `ask` tool: your reads and your questions are then one conversation. Pass the returned id explicitly as `sessionId` on every subsequent KnowledgeBase tool call (direct MCP calls and inside `call_tool_chain` alike). RETRYING IS SAFE: a call that fails created nothing, so retry it — there is no half-made session to clean up. If a retry lands after a success you simply hold two independent ids, which is harmless: keep passing the one id you have already used for the rest of the run and ignore the other. Returns `{ sessionId }`.',
1643
+ 'Mint the id of this conversation, which the KnowledgeBase tools take as `sessionId`. Call this ONCE, at the start of your work — minting a new id mid-run starts a second conversation as far as the server is concerned. The id is also a chat session in the app, so you can hand the SAME id to the `ask` tool: your reads and your questions are then one conversation. Pass the returned id as `sessionId` on every later KnowledgeBase tool call (direct MCP calls and inside `call_tool_chain` alike). RETRYING IS SAFE: a call that fails created nothing, so retry it. If a retry lands after a success you hold two independent ids, which is harmless: keep passing the one you already used and ignore the other. Returns `{ sessionId }`.',
1627
1644
  path: '/api/agent/tools/start_session',
1628
1645
  inputs: { type: 'object', properties: {}, additionalProperties: false },
1629
1646
  outputs: {
@@ -1689,6 +1706,22 @@ export function registerWorkspaceTools(
1689
1706
  if (spillStore.isSpillRef(p)) {
1690
1707
  return { path: p, content: await spillStore.read(p, offset, limit) };
1691
1708
  }
1709
+ const slice = (content: string): string => {
1710
+ const start = offset && offset > 0 ? offset : 0;
1711
+ return offset !== undefined || limit !== undefined
1712
+ ? content.slice(start, limit !== undefined ? start + limit : undefined)
1713
+ : content;
1714
+ };
1715
+ // The guide's name at the repository root answers with the platform's
1716
+ // guide, which is text the code owns and every agent may read: no gate
1717
+ // and no read hook for it. A file the knowledge base keeps under that
1718
+ // name is ITS OWN conventions page and is read as any file is — gated,
1719
+ // noted — and comes first, with the guide after it. A copy of the
1720
+ // guide an earlier release wrote to disk (still on a draft, say) is
1721
+ // recognised by its header and not served a second time.
1722
+ if (agentGuide && isAgentGuidePath(toKbRelative(p, kbDirName) ?? '')) {
1723
+ return { path: p, content: slice(await guideAt(a.branch as string, ctx, p)) };
1724
+ }
1692
1725
  await notifyAgentRead(agentAccessGate, ctx, a.branch as string, p);
1693
1726
  await assertCanRead(readGateFor(a.branch as string, ctx), p);
1694
1727
  const fs = await ctx.getFilesystem(a.branch as string);
@@ -1717,14 +1750,105 @@ export function registerWorkspaceTools(
1717
1750
  // document, unreadable binary, oversized image) IS the file's honest
1718
1751
  // textual answer, sliced like any other content.
1719
1752
  const content = result.kind === 'text' ? result.text : result.message;
1720
- const start = offset && offset > 0 ? offset : 0;
1721
- const sliced = offset !== undefined || limit !== undefined
1722
- ? content.slice(start, limit !== undefined ? start + limit : undefined)
1723
- : content;
1724
- return { path: p, content: sliced };
1753
+ return { path: p, content: slice(content) };
1725
1754
  },
1726
1755
  });
1727
1756
 
1757
+ /**
1758
+ * The knowledge base's OWN file at the guide's path, read as any file is —
1759
+ * through the read hook and the access gate — or null when there is none,
1760
+ * when the caller MAY NOT READ IT, or when what is there is a copy of the
1761
+ * platform's guide an earlier release wrote (recognised by its header),
1762
+ * which the guide served beside it would only repeat. A file that is not
1763
+ * text (a binary squatting the name) is read for what it is: its honest
1764
+ * textual answer.
1765
+ *
1766
+ * A file the caller may not read answers EXACTLY as no file does: the guide
1767
+ * alone, with nothing said. A refusal here would tell a caller the root
1768
+ * denies that a conventions file exists, which is the one thing the
1769
+ * platform never tells about a file someone may not read — a restricted
1770
+ * node is indistinguishable from an absent one on every other read.
1771
+ */
1772
+ /** What a read of the guide's path answers: the guide, after the knowledge base's own readable file when it has one. */
1773
+ const guideAt = async (branch: string, ctx: ToolContext, p: string): Promise<string> => {
1774
+ const guide = await agentGuide!();
1775
+ const own = await ownGuideFile(branch, ctx, p);
1776
+ return own === null ? guide : withPlatformGuideAppended(own, guide);
1777
+ };
1778
+
1779
+ const ownGuideFile = async (branch: string, ctx: ToolContext, p: string): Promise<string | null> => {
1780
+ const fs = await ctx.getFilesystem(branch);
1781
+ // Existence first, then the gate, then the hook and the bytes — nothing of
1782
+ // theirs is read, or noted as read, before they are allowed to read it.
1783
+ if (!(await ownGuideReadable(fs, branch, ctx, p))) return null;
1784
+ await notifyAgentRead(agentAccessGate, ctx, branch, p);
1785
+ // Gone between the probe and the read — a concurrent delete — is the
1786
+ // absent case: the guide alone, as a read a moment later would answer.
1787
+ const bytes = await fs.readFile(p).then(asBytes, (err: unknown) => {
1788
+ if (isAbsence(err)) return null;
1789
+ throw err;
1790
+ });
1791
+ if (bytes === null) return null;
1792
+ const result = await readers.readerFor(p).read(bytes, p);
1793
+ const text = result.kind === 'text' ? result.text : result.kind === 'image' ? result.note : result.message;
1794
+ return isManagedGuide(text) ? null : text;
1795
+ };
1796
+
1797
+ /**
1798
+ * Whether there is a file of the knowledge base's own at the guide's path
1799
+ * that THIS caller may read. False for nothing there and for a file the
1800
+ * access rules close to them, on purpose and without distinction (see
1801
+ * {@link ownGuideFile}).
1802
+ */
1803
+ const ownGuideReadable = async (fs: LocalFilesystem, branch: string, ctx: ToolContext, p: string): Promise<boolean> => {
1804
+ // The permission verdict BEFORE the filesystem is asked anything, as on
1805
+ // every other read: a caller the rules close the path to learns nothing
1806
+ // from it — not that something is there, and not what the filesystem
1807
+ // says about an entry it cannot stat.
1808
+ const gate = readGateFor(branch, ctx);
1809
+ const rel = toKbRelative(p, gate.kbDirName);
1810
+ if (rel !== null && !(await gate.accessControl.canRead(gate.workspaceId, gate.userEmail, rel))) return false;
1811
+ return existsAt(fs, p);
1812
+ };
1813
+
1814
+ /** Whether something is at `p` on `fs` — absence is false, any other failure is thrown. */
1815
+ const existsAt = async (fs: LocalFilesystem, p: string): Promise<boolean> =>
1816
+ fs.stat(p).then(
1817
+ () => true,
1818
+ (err: unknown) => {
1819
+ if (isAbsence(err)) return false;
1820
+ throw err;
1821
+ },
1822
+ );
1823
+
1824
+ /**
1825
+ * Whether what is at `p` is an entry of the knowledge base's own for the
1826
+ * ordinary stat to describe: anything there except a plain file that is a
1827
+ * copy of the guide an earlier release wrote (recognised by its header).
1828
+ * A folder at the guide's name is theirs and is never read — reading a
1829
+ * folder is an error, not an absence. Nothing there, at the stat or at the
1830
+ * read a moment later (a concurrent delete), is the absent case, which the
1831
+ * caller answers with the guide.
1832
+ */
1833
+ const isOwnEntryStill = async (fs: LocalFilesystem, p: string): Promise<boolean> => {
1834
+ const type = await fs.stat(p).then(
1835
+ (st) => st.type,
1836
+ (err: unknown) => {
1837
+ if (isAbsence(err)) return undefined;
1838
+ throw err;
1839
+ },
1840
+ );
1841
+ if (type === undefined) return false;
1842
+ if (type !== 'file') return true;
1843
+ const bytes = await fs.readFile(p).then(asBytes, (err: unknown) => {
1844
+ if (isAbsence(err)) return null;
1845
+ throw err;
1846
+ });
1847
+ if (bytes === null) return false;
1848
+ const result = await readers.readerFor(p).read(bytes, p);
1849
+ return !(result.kind === 'text' && isManagedGuide(result.text));
1850
+ };
1851
+
1728
1852
  mount({
1729
1853
  name: 'list_files',
1730
1854
  gated: true,
@@ -1849,6 +1973,11 @@ export function registerWorkspaceTools(
1849
1973
  },
1850
1974
  textEditable: { type: 'boolean', description: 'Files only: whether write_file/write_files/edit_file accept this file as it is now.' },
1851
1975
  mimeNote: str('Present when `mimeSource` is `fallback`: says the MIME type is a fallback, not a detected type.'),
1976
+ platformGuide: {
1977
+ type: 'boolean',
1978
+ description:
1979
+ "True at the agent guide's name in the repository root when the knowledge base has no file of its own there: what read_file answers is the platform's guide, which is not on disk and cannot be written, moved or deleted.",
1980
+ },
1852
1981
  },
1853
1982
  required: ['managed', 'movable', 'deletable', 'access'],
1854
1983
  additionalProperties: true,
@@ -1857,7 +1986,48 @@ export function registerWorkspaceTools(
1857
1986
  handler: async (a, ctx: ToolContext) => {
1858
1987
  const p = a.path as string;
1859
1988
  const branch = a.branch as string;
1860
- await notifyAgentRead(agentAccessGate, ctx, branch, p);
1989
+ // The guide's name with no file of the knowledge base's own under it —
1990
+ // or one the caller may not read, or a copy of the guide an earlier
1991
+ // release wrote, none of which `read_file` serves: what a read answers
1992
+ // there is the platform's guide, so stat says a text file is there to
1993
+ // read — ungated, like the read — and that nothing can be moved,
1994
+ // deleted or written at it through these tools. The three cases get
1995
+ // ONE answer on purpose: a different one for the file the caller may
1996
+ // not read would tell them it exists. A file of the knowledge base's
1997
+ // own that the caller may read is a file like any other, and the
1998
+ // ordinary answer below describes it.
1999
+ let noted = false;
2000
+ if (agentGuide && isAgentGuidePath(toKbRelative(p, kbDirName) ?? '')) {
2001
+ const fs = await ctx.getFilesystem(branch);
2002
+ const readable = await ownGuideReadable(fs, branch, ctx, p);
2003
+ // Telling the organisation's own file from a stale copy READS it, so
2004
+ // the read hook hears of it as it hears of a read_file there — after
2005
+ // the gate, never before, and once (the ordinary stat below is told).
2006
+ if (readable) {
2007
+ await notifyAgentRead(agentAccessGate, ctx, branch, p);
2008
+ noted = true;
2009
+ }
2010
+ const own = readable && (await isOwnEntryStill(fs, p));
2011
+ if (!own) {
2012
+ const guide = await agentGuide();
2013
+ return {
2014
+ name: p.slice(p.lastIndexOf('/') + 1),
2015
+ type: 'file',
2016
+ size: Buffer.byteLength(guide, 'utf8'),
2017
+ managed: true,
2018
+ movable: false,
2019
+ deletable: false,
2020
+ access: { read: true, write: false, download: false, owner: false },
2021
+ contentMode: 'text',
2022
+ kind: 'text',
2023
+ mime: 'text/markdown',
2024
+ mimeSource: 'extension',
2025
+ textEditable: false,
2026
+ platformGuide: true,
2027
+ };
2028
+ }
2029
+ }
2030
+ if (!noted) await notifyAgentRead(agentAccessGate, ctx, branch, p);
1861
2031
  await assertCanRead(readGateFor(branch, ctx), p);
1862
2032
  // Nothing there is a 404, and the placeholder — never content — gets
1863
2033
  // exactly that answer: the one every file tool gives (see not-found.ts).
@@ -2010,10 +2180,6 @@ export function registerWorkspaceTools(
2010
2180
  // (an empty path is the handler's to explain), and here it would
2011
2181
  // otherwise name the workspace directory by another spelling.
2012
2182
  const searchRoot = typeof a.path === 'string' && a.path.length > 0 ? a.path : kbDirName;
2013
- // The search root itself goes to the read hook here; each file the walk
2014
- // actually opens goes to it per-file below, so a hook sees every path a
2015
- // grep reached rather than only the root it started from.
2016
- await notifyAgentRead(agentAccessGate, ctx, a.branch as string, searchRoot);
2017
2183
  const fs = await ctx.getFilesystem(a.branch as string);
2018
2184
  const gate = readGateFor(a.branch as string, ctx);
2019
2185
  const out: { path: string; line: number; text: string }[] = [];
@@ -2031,7 +2197,51 @@ export function registerWorkspaceTools(
2031
2197
  : await searchRootKind(fs, searchRoot);
2032
2198
  /** Why a single-file search found nothing, when "no matches" would be a lie. */
2033
2199
  let fileNote: string | undefined;
2034
- if (kind === 'directory') {
2200
+ // The guide is searched where it is read: a search of the repository
2201
+ // root covers it, and a search of its own path is a search of what
2202
+ // `read_file` answers there. The composed text is what is searched —
2203
+ // the knowledge base's own readable file first, then the platform's
2204
+ // guide — under the guide's path and with the line numbers a read of
2205
+ // it gives. The walk leaves that one file to this: a match the walk
2206
+ // made there would be the same line again, and one it COUNTED against
2207
+ // `max_results` would be a file later in the tree never searched while
2208
+ // the answer says nothing was cut. A file the caller may not read is
2209
+ // absent from it, as it is from the read.
2210
+ const guidePath = `${kbDirName}/${AGENT_GUIDE_FILE}`;
2211
+ const rel = toKbRelative(searchRoot, kbDirName);
2212
+ const coversGuide =
2213
+ agentGuide !== undefined && (kind === 'directory' ? rel === null : isAgentGuidePath(rel ?? ''));
2214
+ // The search root goes to the read hook — once, and only when it is
2215
+ // repository content: a folder, or a file of the knowledge base's own.
2216
+ // The guide's own path is not told here, because `guideAt` tells the
2217
+ // hook of the organisation's file there exactly as `read_file` does
2218
+ // (after the gate), and the platform's guide alone is nobody's file to
2219
+ // note. Each file the walk opens goes to the hook per file below, so a
2220
+ // hook sees every path a grep reached rather than only its root.
2221
+ if (!(coversGuide && kind !== 'directory')) {
2222
+ await notifyAgentRead(agentAccessGate, ctx, a.branch as string, searchRoot);
2223
+ }
2224
+ if (coversGuide) {
2225
+ if (kind === 'directory') {
2226
+ await grepWalk(
2227
+ fs,
2228
+ searchRoot,
2229
+ re,
2230
+ out,
2231
+ max,
2232
+ 0,
2233
+ gate,
2234
+ (p) => notifyAgentRead(agentAccessGate, ctx, a.branch as string, p),
2235
+ docs,
2236
+ (p) => p === guidePath,
2237
+ );
2238
+ }
2239
+ const lines = (await guideAt(a.branch as string, ctx, guidePath)).split('\n');
2240
+ for (let i = 0; i < lines.length && out.length < max; i++) {
2241
+ re.lastIndex = 0;
2242
+ if (re.test(lines[i]!)) out.push({ path: guidePath, line: i + 1, text: lines[i]!.slice(0, 300) });
2243
+ }
2244
+ } else if (kind === 'directory') {
2035
2245
  await grepWalk(
2036
2246
  fs,
2037
2247
  searchRoot,
@@ -330,6 +330,21 @@ export class WorkflowValidationError extends WorkflowDomainError {
330
330
  }
331
331
  }
332
332
 
333
+ /**
334
+ * The commit an applied change request records is in the clone but is NOT
335
+ * that request's own merge commit — no second parent (not a merge), no first
336
+ * parent (a root commit), or a message that does not name the request. A
337
+ * validation failure like the one above, told apart
338
+ * because it never mends: a reader may remember it, where a commit the clone
339
+ * merely does not hold yet must be asked for again after the next fetch.
340
+ */
341
+ export class AppliedChangeMismatchError extends WorkflowValidationError {
342
+ constructor(message: string, payload?: Record<string, unknown>) {
343
+ super(message, payload);
344
+ this.name = 'AppliedChangeMismatchError';
345
+ }
346
+ }
347
+
333
348
  /**
334
349
  * The next step a missing path always offers, in one sentence.
335
350
  *
@@ -0,0 +1,45 @@
1
+ /**
2
+ * A connected tool Hexis keeps off every agent surface because its input
3
+ * schema is not valid JSON Schema as its server sent it — and the read port
4
+ * the surfaces that SHOW that to the server's owner use.
5
+ *
6
+ * Here rather than in either module because two of them meet over it: the MCP
7
+ * proxy finds these when a server's tools are loaded (`modules/mcp`), and the
8
+ * tool catalog reports them to the people who manage the server
9
+ * (`modules/tool-manuals`, on the tool page and in `list_tool_setup`). Neither
10
+ * has any business importing the other.
11
+ */
12
+
13
+ /** One connected tool hidden from agents, and why. */
14
+ export interface HiddenTool {
15
+ /** The manual (the `.tool` or `mcp.json` server) the tool came from, by catalog name. */
16
+ manual: string;
17
+ /** The name an agent would have called it by, had it been offered. */
18
+ name: string;
19
+ /** JSON Pointer to the place in the input schema that is not valid. */
20
+ path: string;
21
+ /** The violation in the validator's own words, e.g. `must be a string`. */
22
+ reason: string;
23
+ /** The one sentence every owner-facing surface shows, built once so they cannot drift. */
24
+ marker: string;
25
+ }
26
+
27
+ /**
28
+ * What the owner-facing surfaces read. A deployment that never builds an MCP
29
+ * surface has nothing to report, so every consumer treats the port as
30
+ * optional and an absent one as "no tool is hidden".
31
+ */
32
+ export interface HiddenToolSource {
33
+ /**
34
+ * The tools of `manual` currently hidden for an invalid schema, by the
35
+ * manual's CATALOG name — empty for a healthy server, and empty for a server
36
+ * whose tools this process has not loaded yet.
37
+ *
38
+ * A finding belongs to the SERVER, not to whoever's request loaded it: a
39
+ * schema is a property of the tool definition, and the people who can get it
40
+ * fixed are not necessarily the person whose connection saw it. So this is
41
+ * every finding this process holds for that manual, from whichever callers
42
+ * have loaded it, each distinct defect once.
43
+ */
44
+ hiddenFor(manual: string): HiddenTool[];
45
+ }