@bevel-software/platform-core-backend 0.33.0 → 0.35.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 (602) hide show
  1. package/agent-guide/access-control.md +146 -289
  2. package/agent-guide/conventions.md +23 -25
  3. package/agent-guide/directory-structure.md +72 -120
  4. package/agent-guide/drafts-and-change-requests.md +92 -0
  5. package/agent-guide/for-git-users.md +47 -0
  6. package/agent-guide/glossary.md +36 -0
  7. package/agent-guide/how-agents-find-things.md +49 -0
  8. package/agent-guide/html-views.md +239 -8
  9. package/agent-guide/new-knowledge-base.md +9 -8
  10. package/agent-guide/skills.md +67 -42
  11. package/agent-guide/start-here.md +65 -0
  12. package/agent-guide/tool-manuals.md +133 -155
  13. package/agent-guide/tools.md +67 -0
  14. package/agent-guide/where-a-new-file-goes.md +45 -28
  15. package/dist/core/catalog-revision.d.ts +12 -1
  16. package/dist/core/catalog-revision.d.ts.map +1 -1
  17. package/dist/core/catalog-revision.js +23 -3
  18. package/dist/core/catalog-revision.js.map +1 -1
  19. package/dist/core/core-ports.d.ts +41 -0
  20. package/dist/core/core-ports.d.ts.map +1 -1
  21. package/dist/core/core-ports.js.map +1 -1
  22. package/dist/core/create-core-server.d.ts.map +1 -1
  23. package/dist/core/create-core-server.js +15 -8
  24. package/dist/core/create-core-server.js.map +1 -1
  25. package/dist/core/create-core-services.d.ts +22 -5
  26. package/dist/core/create-core-services.d.ts.map +1 -1
  27. package/dist/core/create-core-services.js +44 -16
  28. package/dist/core/create-core-services.js.map +1 -1
  29. package/dist/core/lifecycle.d.ts +16 -0
  30. package/dist/core/lifecycle.d.ts.map +1 -1
  31. package/dist/core/lifecycle.js +12 -2
  32. package/dist/core/lifecycle.js.map +1 -1
  33. package/dist/index.d.ts +1 -1
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js.map +1 -1
  36. package/dist/modules/access/access-control.interface.d.ts +39 -9
  37. package/dist/modules/access/access-control.interface.d.ts.map +1 -1
  38. package/dist/modules/access/access-control.service.d.ts +15 -3
  39. package/dist/modules/access/access-control.service.d.ts.map +1 -1
  40. package/dist/modules/access/access-control.service.js +239 -75
  41. package/dist/modules/access/access-control.service.js.map +1 -1
  42. package/dist/modules/access/access-mutation.service.d.ts +22 -24
  43. package/dist/modules/access/access-mutation.service.d.ts.map +1 -1
  44. package/dist/modules/access/access-mutation.service.js +48 -51
  45. package/dist/modules/access/access-mutation.service.js.map +1 -1
  46. package/dist/modules/access/access-view.d.ts.map +1 -1
  47. package/dist/modules/access/access-view.js +12 -11
  48. package/dist/modules/access/access-view.js.map +1 -1
  49. package/dist/modules/access/access.routes.d.ts +3 -0
  50. package/dist/modules/access/access.routes.d.ts.map +1 -1
  51. package/dist/modules/access/access.routes.js +58 -33
  52. package/dist/modules/access/access.routes.js.map +1 -1
  53. package/dist/modules/access/groups-admin.routes.d.ts.map +1 -1
  54. package/dist/modules/access/groups-admin.routes.js +4 -2
  55. package/dist/modules/access/groups-admin.routes.js.map +1 -1
  56. package/dist/modules/access/groups-admin.service.d.ts +44 -13
  57. package/dist/modules/access/groups-admin.service.d.ts.map +1 -1
  58. package/dist/modules/access/groups-admin.service.js +80 -38
  59. package/dist/modules/access/groups-admin.service.js.map +1 -1
  60. package/dist/modules/access/groups-edit.d.ts +1 -1
  61. package/dist/modules/access/groups-edit.d.ts.map +1 -1
  62. package/dist/modules/access/groups-edit.js +1 -1
  63. package/dist/modules/access/groups-edit.js.map +1 -1
  64. package/dist/modules/access/index.d.ts +1 -1
  65. package/dist/modules/access/index.d.ts.map +1 -1
  66. package/dist/modules/access/reference-scan.d.ts +116 -24
  67. package/dist/modules/access/reference-scan.d.ts.map +1 -1
  68. package/dist/modules/access/reference-scan.js +305 -44
  69. package/dist/modules/access/reference-scan.js.map +1 -1
  70. package/dist/modules/access/roles-admin.service.d.ts +17 -8
  71. package/dist/modules/access/roles-admin.service.d.ts.map +1 -1
  72. package/dist/modules/access/roles-admin.service.js +32 -23
  73. package/dist/modules/access/roles-admin.service.js.map +1 -1
  74. package/dist/modules/access-model/access-grammar.d.ts +35 -10
  75. package/dist/modules/access-model/access-grammar.d.ts.map +1 -1
  76. package/dist/modules/access-model/access-grammar.js +60 -20
  77. package/dist/modules/access-model/access-grammar.js.map +1 -1
  78. package/dist/modules/access-model/access-splice.d.ts +22 -18
  79. package/dist/modules/access-model/access-splice.d.ts.map +1 -1
  80. package/dist/modules/access-model/access-splice.js +20 -14
  81. package/dist/modules/access-model/access-splice.js.map +1 -1
  82. package/dist/modules/access-model/group-files.d.ts +1 -0
  83. package/dist/modules/access-model/group-files.d.ts.map +1 -1
  84. package/dist/modules/access-model/group-files.js +5 -1
  85. package/dist/modules/access-model/group-files.js.map +1 -1
  86. package/dist/modules/agent-guide/agent-guide.d.ts +47 -7
  87. package/dist/modules/agent-guide/agent-guide.d.ts.map +1 -1
  88. package/dist/modules/agent-guide/agent-guide.js +103 -13
  89. package/dist/modules/agent-guide/agent-guide.js.map +1 -1
  90. package/dist/modules/agent-guide/agent-guide.tools.d.ts +6 -2
  91. package/dist/modules/agent-guide/agent-guide.tools.d.ts.map +1 -1
  92. package/dist/modules/agent-guide/agent-guide.tools.js +46 -22
  93. package/dist/modules/agent-guide/agent-guide.tools.js.map +1 -1
  94. package/dist/modules/agent-guide/guide-budget.d.ts +48 -0
  95. package/dist/modules/agent-guide/guide-budget.d.ts.map +1 -0
  96. package/dist/modules/agent-guide/guide-budget.js +67 -0
  97. package/dist/modules/agent-guide/guide-budget.js.map +1 -0
  98. package/dist/modules/agent-guide/index.d.ts +2 -1
  99. package/dist/modules/agent-guide/index.d.ts.map +1 -1
  100. package/dist/modules/agent-guide/index.js +2 -1
  101. package/dist/modules/agent-guide/index.js.map +1 -1
  102. package/dist/modules/agent-instructions/compose.d.ts +3 -1
  103. package/dist/modules/agent-instructions/compose.d.ts.map +1 -1
  104. package/dist/modules/agent-instructions/compose.js +7 -1
  105. package/dist/modules/agent-instructions/compose.js.map +1 -1
  106. package/dist/modules/agent-instructions/shared-file-rules.d.ts +5 -1
  107. package/dist/modules/agent-instructions/shared-file-rules.d.ts.map +1 -1
  108. package/dist/modules/agent-instructions/shared-file-rules.js +26 -9
  109. package/dist/modules/agent-instructions/shared-file-rules.js.map +1 -1
  110. package/dist/modules/agent-scope/agent-scope.contract.d.ts +67 -0
  111. package/dist/modules/agent-scope/agent-scope.contract.d.ts.map +1 -0
  112. package/dist/modules/agent-scope/agent-scope.contract.js +27 -0
  113. package/dist/modules/agent-scope/agent-scope.contract.js.map +1 -0
  114. package/dist/modules/agent-scope/connection-scope.d.ts +111 -0
  115. package/dist/modules/agent-scope/connection-scope.d.ts.map +1 -0
  116. package/dist/modules/agent-scope/connection-scope.js +165 -0
  117. package/dist/modules/agent-scope/connection-scope.js.map +1 -0
  118. package/dist/modules/agent-scope/index.d.ts +3 -0
  119. package/dist/modules/agent-scope/index.d.ts.map +1 -0
  120. package/dist/modules/agent-scope/index.js +3 -0
  121. package/dist/modules/agent-scope/index.js.map +1 -0
  122. package/dist/modules/audit/agent-audit.service.d.ts.map +1 -1
  123. package/dist/modules/audit/agent-audit.service.js +2 -0
  124. package/dist/modules/audit/agent-audit.service.js.map +1 -1
  125. package/dist/modules/audit/audit.contract.d.ts +6 -0
  126. package/dist/modules/audit/audit.contract.d.ts.map +1 -1
  127. package/dist/modules/audit/audit.contract.js.map +1 -1
  128. package/dist/modules/auth/account-admission.d.ts +12 -0
  129. package/dist/modules/auth/account-admission.d.ts.map +1 -1
  130. package/dist/modules/auth/account-admission.js +15 -0
  131. package/dist/modules/auth/account-admission.js.map +1 -1
  132. package/dist/modules/auth/auth.middleware.d.ts +27 -1
  133. package/dist/modules/auth/auth.middleware.d.ts.map +1 -1
  134. package/dist/modules/auth/auth.middleware.js +42 -4
  135. package/dist/modules/auth/auth.middleware.js.map +1 -1
  136. package/dist/modules/auth/auth.routes.d.ts +13 -9
  137. package/dist/modules/auth/auth.routes.d.ts.map +1 -1
  138. package/dist/modules/auth/auth.routes.js +35 -29
  139. package/dist/modules/auth/auth.routes.js.map +1 -1
  140. package/dist/modules/auth/auth.service.d.ts +55 -6
  141. package/dist/modules/auth/auth.service.d.ts.map +1 -1
  142. package/dist/modules/auth/auth.service.js +128 -34
  143. package/dist/modules/auth/auth.service.js.map +1 -1
  144. package/dist/modules/auth/index.d.ts +1 -1
  145. package/dist/modules/auth/index.d.ts.map +1 -1
  146. package/dist/modules/auth/index.js +1 -1
  147. package/dist/modules/auth/index.js.map +1 -1
  148. package/dist/modules/auth/oidc-auth-provider.d.ts.map +1 -1
  149. package/dist/modules/auth/oidc-auth-provider.js +2 -9
  150. package/dist/modules/auth/oidc-auth-provider.js.map +1 -1
  151. package/dist/modules/code-mode/code-mode.tool.js +2 -2
  152. package/dist/modules/code-mode/code-mode.tool.js.map +1 -1
  153. package/dist/modules/connection-probe/connection-probe.service.d.ts +0 -12
  154. package/dist/modules/connection-probe/connection-probe.service.d.ts.map +1 -1
  155. package/dist/modules/connection-probe/connection-probe.service.js +1 -38
  156. package/dist/modules/connection-probe/connection-probe.service.js.map +1 -1
  157. package/dist/modules/database/core-schema.d.ts +68 -0
  158. package/dist/modules/database/core-schema.d.ts.map +1 -1
  159. package/dist/modules/database/core-schema.js +33 -0
  160. package/dist/modules/database/core-schema.js.map +1 -1
  161. package/dist/modules/declared-variables/declared-variables.routes.d.ts +8 -1
  162. package/dist/modules/declared-variables/declared-variables.routes.d.ts.map +1 -1
  163. package/dist/modules/declared-variables/declared-variables.routes.js +13 -2
  164. package/dist/modules/declared-variables/declared-variables.routes.js.map +1 -1
  165. package/dist/modules/embed/embed.tools.d.ts.map +1 -1
  166. package/dist/modules/embed/embed.tools.js +5 -0
  167. package/dist/modules/embed/embed.tools.js.map +1 -1
  168. package/dist/modules/kb-fs/index.d.ts +2 -0
  169. package/dist/modules/kb-fs/index.d.ts.map +1 -1
  170. package/dist/modules/kb-fs/index.js +1 -0
  171. package/dist/modules/kb-fs/index.js.map +1 -1
  172. package/dist/modules/kb-fs/locking-filesystem.d.ts.map +1 -1
  173. package/dist/modules/kb-fs/locking-filesystem.js +7 -1
  174. package/dist/modules/kb-fs/locking-filesystem.js.map +1 -1
  175. package/dist/modules/kb-fs/repo-path.d.ts +26 -6
  176. package/dist/modules/kb-fs/repo-path.d.ts.map +1 -1
  177. package/dist/modules/kb-fs/repo-path.js +22 -7
  178. package/dist/modules/kb-fs/repo-path.js.map +1 -1
  179. package/dist/modules/kb-fs/tmp-ref.d.ts +36 -0
  180. package/dist/modules/kb-fs/tmp-ref.d.ts.map +1 -0
  181. package/dist/modules/kb-fs/tmp-ref.js +76 -0
  182. package/dist/modules/kb-fs/tmp-ref.js.map +1 -0
  183. package/dist/modules/mcp/mcp-auth.middleware.d.ts +7 -0
  184. package/dist/modules/mcp/mcp-auth.middleware.d.ts.map +1 -1
  185. package/dist/modules/mcp/mcp-auth.middleware.js +14 -1
  186. package/dist/modules/mcp/mcp-auth.middleware.js.map +1 -1
  187. package/dist/modules/mcp/mcp.routes.d.ts +7 -1
  188. package/dist/modules/mcp/mcp.routes.d.ts.map +1 -1
  189. package/dist/modules/mcp/mcp.routes.js +39 -4
  190. package/dist/modules/mcp/mcp.routes.js.map +1 -1
  191. package/dist/modules/mcp/mcp.service.d.ts +11 -0
  192. package/dist/modules/mcp/mcp.service.d.ts.map +1 -1
  193. package/dist/modules/mcp/mcp.service.js +74 -48
  194. package/dist/modules/mcp/mcp.service.js.map +1 -1
  195. package/dist/modules/mcp/oauth/bevel-oauth-provider.d.ts +24 -1
  196. package/dist/modules/mcp/oauth/bevel-oauth-provider.d.ts.map +1 -1
  197. package/dist/modules/mcp/oauth/bevel-oauth-provider.js +45 -10
  198. package/dist/modules/mcp/oauth/bevel-oauth-provider.js.map +1 -1
  199. package/dist/modules/mcp/oauth/oauth-consent.routes.d.ts +9 -0
  200. package/dist/modules/mcp/oauth/oauth-consent.routes.d.ts.map +1 -1
  201. package/dist/modules/mcp/oauth/oauth-consent.routes.js +34 -1
  202. package/dist/modules/mcp/oauth/oauth-consent.routes.js.map +1 -1
  203. package/dist/modules/plugins/index.d.ts +1 -1
  204. package/dist/modules/plugins/index.d.ts.map +1 -1
  205. package/dist/modules/plugins/index.js.map +1 -1
  206. package/dist/modules/plugins/plugin-links.d.ts +18 -2
  207. package/dist/modules/plugins/plugin-links.d.ts.map +1 -1
  208. package/dist/modules/plugins/plugin-links.js.map +1 -1
  209. package/dist/modules/plugins/plugins.routes.d.ts +5 -1
  210. package/dist/modules/plugins/plugins.routes.d.ts.map +1 -1
  211. package/dist/modules/plugins/plugins.routes.js +32 -5
  212. package/dist/modules/plugins/plugins.routes.js.map +1 -1
  213. package/dist/modules/secrets-vault/db-secrets-vault.service.d.ts +11 -1
  214. package/dist/modules/secrets-vault/db-secrets-vault.service.d.ts.map +1 -1
  215. package/dist/modules/secrets-vault/db-secrets-vault.service.js +159 -24
  216. package/dist/modules/secrets-vault/db-secrets-vault.service.js.map +1 -1
  217. package/dist/modules/secrets-vault/index.d.ts +1 -1
  218. package/dist/modules/secrets-vault/index.d.ts.map +1 -1
  219. package/dist/modules/secrets-vault/index.js +1 -1
  220. package/dist/modules/secrets-vault/index.js.map +1 -1
  221. package/dist/modules/secrets-vault/secrets-vault.contract.d.ts +53 -11
  222. package/dist/modules/secrets-vault/secrets-vault.contract.d.ts.map +1 -1
  223. package/dist/modules/secrets-vault/secrets-vault.contract.js +27 -0
  224. package/dist/modules/secrets-vault/secrets-vault.contract.js.map +1 -1
  225. package/dist/modules/secrets-vault/secrets-vault.routes.d.ts +9 -0
  226. package/dist/modules/secrets-vault/secrets-vault.routes.d.ts.map +1 -1
  227. package/dist/modules/secrets-vault/secrets-vault.routes.js +100 -42
  228. package/dist/modules/secrets-vault/secrets-vault.routes.js.map +1 -1
  229. package/dist/modules/settings/oidc-check.d.ts.map +1 -1
  230. package/dist/modules/settings/oidc-check.js +5 -17
  231. package/dist/modules/settings/oidc-check.js.map +1 -1
  232. package/dist/modules/skills/index.d.ts +3 -3
  233. package/dist/modules/skills/index.d.ts.map +1 -1
  234. package/dist/modules/skills/index.js +0 -2
  235. package/dist/modules/skills/index.js.map +1 -1
  236. package/dist/modules/skills/skills.contract.d.ts +16 -0
  237. package/dist/modules/skills/skills.contract.d.ts.map +1 -1
  238. package/dist/modules/skills/skills.service.d.ts.map +1 -1
  239. package/dist/modules/skills/skills.service.js +6 -1
  240. package/dist/modules/skills/skills.service.js.map +1 -1
  241. package/dist/modules/skills/skills.tools.d.ts.map +1 -1
  242. package/dist/modules/skills/skills.tools.js +25 -13
  243. package/dist/modules/skills/skills.tools.js.map +1 -1
  244. package/dist/modules/tool-auth/external-api-key.interface.d.ts +11 -0
  245. package/dist/modules/tool-auth/external-api-key.interface.d.ts.map +1 -1
  246. package/dist/modules/tool-auth/external-api-key.interface.js.map +1 -1
  247. package/dist/modules/tool-auth/external-api-key.service.d.ts +1 -0
  248. package/dist/modules/tool-auth/external-api-key.service.d.ts.map +1 -1
  249. package/dist/modules/tool-auth/external-api-key.service.js +4 -1
  250. package/dist/modules/tool-auth/external-api-key.service.js.map +1 -1
  251. package/dist/modules/tool-auth/index.d.ts +1 -1
  252. package/dist/modules/tool-auth/index.d.ts.map +1 -1
  253. package/dist/modules/tool-auth/internal-token.service.d.ts +9 -0
  254. package/dist/modules/tool-auth/internal-token.service.d.ts.map +1 -1
  255. package/dist/modules/tool-auth/internal-token.service.js +1 -0
  256. package/dist/modules/tool-auth/internal-token.service.js.map +1 -1
  257. package/dist/modules/tool-auth/tool-auth.middleware.d.ts +27 -3
  258. package/dist/modules/tool-auth/tool-auth.middleware.d.ts.map +1 -1
  259. package/dist/modules/tool-auth/tool-auth.middleware.js +32 -6
  260. package/dist/modules/tool-auth/tool-auth.middleware.js.map +1 -1
  261. package/dist/modules/tool-helpers/tool-context.d.ts +7 -0
  262. package/dist/modules/tool-helpers/tool-context.d.ts.map +1 -1
  263. package/dist/modules/tool-helpers/tool-context.js +3 -0
  264. package/dist/modules/tool-helpers/tool-context.js.map +1 -1
  265. package/dist/modules/tool-helpers/tool-def.js +1 -1
  266. package/dist/modules/tool-helpers/tool-def.js.map +1 -1
  267. package/dist/modules/tool-helpers/tool-handler.d.ts +15 -0
  268. package/dist/modules/tool-helpers/tool-handler.d.ts.map +1 -1
  269. package/dist/modules/tool-helpers/tool-handler.js +19 -0
  270. package/dist/modules/tool-helpers/tool-handler.js.map +1 -1
  271. package/dist/modules/tool-helpers/tool.contract.d.ts +10 -0
  272. package/dist/modules/tool-helpers/tool.contract.d.ts.map +1 -1
  273. package/dist/modules/tool-helpers/tool.contract.js.map +1 -1
  274. package/dist/modules/tool-helpers/validate-token.d.ts +3 -1
  275. package/dist/modules/tool-helpers/validate-token.d.ts.map +1 -1
  276. package/dist/modules/tool-helpers/validate-token.js +1 -1
  277. package/dist/modules/tool-helpers/validate-token.js.map +1 -1
  278. package/dist/modules/tool-manuals/index.d.ts +3 -3
  279. package/dist/modules/tool-manuals/index.d.ts.map +1 -1
  280. package/dist/modules/tool-manuals/index.js +0 -2
  281. package/dist/modules/tool-manuals/index.js.map +1 -1
  282. package/dist/modules/tool-manuals/mcp-json-discovery.d.ts +7 -1
  283. package/dist/modules/tool-manuals/mcp-json-discovery.d.ts.map +1 -1
  284. package/dist/modules/tool-manuals/mcp-json-discovery.js +69 -38
  285. package/dist/modules/tool-manuals/mcp-json-discovery.js.map +1 -1
  286. package/dist/modules/tool-manuals/tool-manuals.contract.d.ts +46 -6
  287. package/dist/modules/tool-manuals/tool-manuals.contract.d.ts.map +1 -1
  288. package/dist/modules/tool-manuals/tool-manuals.routes.d.ts +7 -1
  289. package/dist/modules/tool-manuals/tool-manuals.routes.d.ts.map +1 -1
  290. package/dist/modules/tool-manuals/tool-manuals.routes.js +41 -4
  291. package/dist/modules/tool-manuals/tool-manuals.routes.js.map +1 -1
  292. package/dist/modules/tool-manuals/tool-manuals.service.d.ts +24 -13
  293. package/dist/modules/tool-manuals/tool-manuals.service.d.ts.map +1 -1
  294. package/dist/modules/tool-manuals/tool-manuals.service.js +138 -59
  295. package/dist/modules/tool-manuals/tool-manuals.service.js.map +1 -1
  296. package/dist/modules/tool-manuals/tool-manuals.tools.d.ts +8 -2
  297. package/dist/modules/tool-manuals/tool-manuals.tools.d.ts.map +1 -1
  298. package/dist/modules/tool-manuals/tool-manuals.tools.js +56 -20
  299. package/dist/modules/tool-manuals/tool-manuals.tools.js.map +1 -1
  300. package/dist/modules/tool-registry/manual.routes.d.ts +8 -1
  301. package/dist/modules/tool-registry/manual.routes.d.ts.map +1 -1
  302. package/dist/modules/tool-registry/manual.routes.js +27 -7
  303. package/dist/modules/tool-registry/manual.routes.js.map +1 -1
  304. package/dist/modules/tool-registry/tool.contract.d.ts +7 -0
  305. package/dist/modules/tool-registry/tool.contract.d.ts.map +1 -1
  306. package/dist/modules/tool-registry/tool.contract.js.map +1 -1
  307. package/dist/modules/workflow/agent-tools/workflow.tools.d.ts.map +1 -1
  308. package/dist/modules/workflow/agent-tools/workflow.tools.js +36 -27
  309. package/dist/modules/workflow/agent-tools/workflow.tools.js.map +1 -1
  310. package/dist/modules/workflow/git/git.service.d.ts +2 -2
  311. package/dist/modules/workflow/git/git.service.d.ts.map +1 -1
  312. package/dist/modules/workflow/git/git.service.js +20 -35
  313. package/dist/modules/workflow/git/git.service.js.map +1 -1
  314. package/dist/modules/workflow/workflow.service.d.ts +2 -1
  315. package/dist/modules/workflow/workflow.service.d.ts.map +1 -1
  316. package/dist/modules/workflow/workflow.service.js +3 -0
  317. package/dist/modules/workflow/workflow.service.js.map +1 -1
  318. package/dist/modules/workspace/agent-access.gate.d.ts +1 -1
  319. package/dist/modules/workspace/agent-access.gate.d.ts.map +1 -1
  320. package/dist/modules/workspace/agent-access.gate.js +1 -1
  321. package/dist/modules/workspace/agent-access.gate.js.map +1 -1
  322. package/dist/modules/workspace/file-readers/doc-extract.service.d.ts +28 -4
  323. package/dist/modules/workspace/file-readers/doc-extract.service.d.ts.map +1 -1
  324. package/dist/modules/workspace/file-readers/doc-extract.service.js +61 -40
  325. package/dist/modules/workspace/file-readers/doc-extract.service.js.map +1 -1
  326. package/dist/modules/workspace/index.d.ts +8 -0
  327. package/dist/modules/workspace/index.d.ts.map +1 -1
  328. package/dist/modules/workspace/index.js +8 -0
  329. package/dist/modules/workspace/index.js.map +1 -1
  330. package/dist/modules/workspace/nul-scan.d.ts +19 -0
  331. package/dist/modules/workspace/nul-scan.d.ts.map +1 -0
  332. package/dist/modules/workspace/nul-scan.js +55 -0
  333. package/dist/modules/workspace/nul-scan.js.map +1 -0
  334. package/dist/modules/workspace/pattern-matcher.contract.d.ts +110 -27
  335. package/dist/modules/workspace/pattern-matcher.contract.d.ts.map +1 -1
  336. package/dist/modules/workspace/pattern-matcher.contract.js +30 -13
  337. package/dist/modules/workspace/pattern-matcher.contract.js.map +1 -1
  338. package/dist/modules/workspace/ripgrep-search.d.ts +54 -0
  339. package/dist/modules/workspace/ripgrep-search.d.ts.map +1 -0
  340. package/dist/modules/workspace/ripgrep-search.js +657 -0
  341. package/dist/modules/workspace/ripgrep-search.js.map +1 -0
  342. package/dist/modules/workspace/ripgrep-version.d.ts +15 -0
  343. package/dist/modules/workspace/ripgrep-version.d.ts.map +1 -0
  344. package/dist/modules/workspace/ripgrep-version.js +31 -0
  345. package/dist/modules/workspace/ripgrep-version.js.map +1 -0
  346. package/dist/modules/workspace/startup/beside-checkout.d.ts +8 -5
  347. package/dist/modules/workspace/startup/beside-checkout.d.ts.map +1 -1
  348. package/dist/modules/workspace/startup/beside-checkout.js +14 -5
  349. package/dist/modules/workspace/startup/beside-checkout.js.map +1 -1
  350. package/dist/modules/workspace/worker-pool.d.ts +3 -4
  351. package/dist/modules/workspace/worker-pool.d.ts.map +1 -1
  352. package/dist/modules/workspace/worker-pool.js.map +1 -1
  353. package/dist/modules/workspace/workspace-tmp.d.ts +146 -0
  354. package/dist/modules/workspace/workspace-tmp.d.ts.map +1 -0
  355. package/dist/modules/workspace/workspace-tmp.js +435 -0
  356. package/dist/modules/workspace/workspace-tmp.js.map +1 -0
  357. package/dist/modules/workspace/workspace.routes.d.ts.map +1 -1
  358. package/dist/modules/workspace/workspace.routes.js +5 -26
  359. package/dist/modules/workspace/workspace.routes.js.map +1 -1
  360. package/dist/modules/workspace/workspace.service.d.ts +47 -6
  361. package/dist/modules/workspace/workspace.service.d.ts.map +1 -1
  362. package/dist/modules/workspace/workspace.service.js +86 -6
  363. package/dist/modules/workspace/workspace.service.js.map +1 -1
  364. package/dist/modules/workspace/workspace.tools.d.ts +3 -2
  365. package/dist/modules/workspace/workspace.tools.d.ts.map +1 -1
  366. package/dist/modules/workspace/workspace.tools.js +531 -137
  367. package/dist/modules/workspace/workspace.tools.js.map +1 -1
  368. package/dist/shared/oauth-token-error.d.ts +23 -0
  369. package/dist/shared/oauth-token-error.d.ts.map +1 -0
  370. package/dist/shared/oauth-token-error.js +47 -0
  371. package/dist/shared/oauth-token-error.js.map +1 -0
  372. package/dist/shared/path-order.d.ts +28 -8
  373. package/dist/shared/path-order.d.ts.map +1 -1
  374. package/dist/shared/path-order.js +37 -8
  375. package/dist/shared/path-order.js.map +1 -1
  376. package/dist/shared/read-capped.d.ts +19 -0
  377. package/dist/shared/read-capped.d.ts.map +1 -0
  378. package/dist/shared/read-capped.js +45 -0
  379. package/dist/shared/read-capped.js.map +1 -0
  380. package/migrations/0022_session_version.sql +6 -0
  381. package/migrations/0023_agent_path.sql +3 -0
  382. package/migrations/meta/0022_snapshot.json +2360 -0
  383. package/migrations/meta/0023_snapshot.json +2378 -0
  384. package/migrations/meta/_journal.json +14 -0
  385. package/package.json +4 -3
  386. package/src/__tests__/hexis-catalog.ts +144 -0
  387. package/src/__tests__/pg-scratch.test.ts +69 -0
  388. package/src/__tests__/pg-scratch.ts +31 -0
  389. package/src/__tests__/real-disk.ts +21 -0
  390. package/src/__tests__/slow-ripgrep.ts +65 -0
  391. package/src/__tests__/tokens.ts +19 -0
  392. package/src/__tests__/tool-manuals.ts +32 -0
  393. package/src/__tests__/tool-wiring.ts +25 -0
  394. package/src/core/__tests__/boot-without-ripgrep.test.ts +34 -0
  395. package/src/core/__tests__/catalog-revision.test.ts +89 -0
  396. package/src/core/__tests__/damaged-document.tools.test.ts +2 -2
  397. package/src/core/__tests__/gated-boot.test.ts +8 -1
  398. package/src/core/__tests__/lifecycle.test.ts +46 -13
  399. package/src/core/__tests__/oauth-sign-in-real-file.test.ts +179 -0
  400. package/src/core/catalog-revision.ts +24 -2
  401. package/src/core/core-ports.ts +42 -0
  402. package/src/core/create-core-server.ts +14 -1
  403. package/src/core/create-core-services.ts +62 -15
  404. package/src/core/lifecycle.ts +26 -2
  405. package/src/index.ts +1 -1
  406. package/src/modules/access/__tests__/access-control.admin-root-floor.test.ts +12 -0
  407. package/src/modules/access/__tests__/access-control.colliding-name-cases.test.ts +113 -0
  408. package/src/modules/access/__tests__/access-control.frontmatter-head.test.ts +117 -0
  409. package/src/modules/access/__tests__/access-control.group-named-admin.test.ts +122 -0
  410. package/src/modules/access/__tests__/access-control.preview-relocation.test.ts +50 -0
  411. package/src/modules/access/__tests__/access-declarations.test.ts +19 -0
  412. package/src/modules/access/__tests__/access-groups.test.ts +55 -22
  413. package/src/modules/access/__tests__/access-mutation.service.test.ts +85 -59
  414. package/src/modules/access/__tests__/access-restrict-here.test.ts +7 -5
  415. package/src/modules/access/__tests__/access.routes.admins.test.ts +1 -0
  416. package/src/modules/access/__tests__/access.routes.batch.test.ts +2 -0
  417. package/src/modules/access/__tests__/access.routes.folder-governs.test.ts +2 -0
  418. package/src/modules/access/__tests__/access.routes.group-grant.test.ts +50 -14
  419. package/src/modules/access/__tests__/access.routes.overrides.test.ts +2 -1
  420. package/src/modules/access/__tests__/access.routes.proposal-branch.test.ts +1 -0
  421. package/src/modules/access/__tests__/access.routes.prospective.test.ts +52 -10
  422. package/src/modules/access/__tests__/access.routes.revoke.test.ts +18 -13
  423. package/src/modules/access/__tests__/access.routes.unknown-email.test.ts +2 -0
  424. package/src/modules/access/__tests__/grant-sources.test.ts +20 -15
  425. package/src/modules/access/__tests__/groups-admin.routes.test.ts +61 -0
  426. package/src/modules/access/__tests__/groups-admin.service.test.ts +211 -18
  427. package/src/modules/access/__tests__/holder-changes.test.ts +127 -0
  428. package/src/modules/access/__tests__/reference-scan.test.ts +366 -14
  429. package/src/modules/access/__tests__/roles-admin.security.test.ts +1 -1
  430. package/src/modules/access/__tests__/roles-admin.service.test.ts +43 -22
  431. package/src/modules/access/__tests__/roles-capabilities.test.ts +6 -2
  432. package/src/modules/access/__tests__/roles.routes.test.ts +1 -1
  433. package/src/modules/access/access-control.interface.ts +43 -10
  434. package/src/modules/access/access-control.service.ts +250 -72
  435. package/src/modules/access/access-mutation.service.ts +48 -53
  436. package/src/modules/access/access-view.ts +12 -10
  437. package/src/modules/access/access.routes.ts +66 -37
  438. package/src/modules/access/groups-admin.routes.ts +4 -2
  439. package/src/modules/access/groups-admin.service.ts +114 -44
  440. package/src/modules/access/groups-edit.ts +1 -1
  441. package/src/modules/access/index.ts +1 -1
  442. package/src/modules/access/reference-scan.ts +319 -46
  443. package/src/modules/access/roles-admin.service.ts +43 -26
  444. package/src/modules/access/synced-groups-writer.ts +1 -1
  445. package/src/modules/access-model/__tests__/access-grammar.test.ts +38 -1
  446. package/src/modules/access-model/__tests__/access-splice.test.ts +50 -8
  447. package/src/modules/access-model/access-grammar.ts +65 -20
  448. package/src/modules/access-model/access-splice.ts +31 -23
  449. package/src/modules/access-model/group-files.ts +5 -0
  450. package/src/modules/agent-guide/__tests__/agent-guide.test.ts +110 -25
  451. package/src/modules/agent-guide/__tests__/agent-guide.tools.test.ts +38 -11
  452. package/src/modules/agent-guide/__tests__/guide-budget.test.ts +95 -0
  453. package/src/modules/agent-guide/__tests__/guide-eval.test.ts +182 -0
  454. package/src/modules/agent-guide/__tests__/guide-matches-code.test.ts +263 -0
  455. package/src/modules/agent-guide/__tests__/tool-surface.json +514 -0
  456. package/src/modules/agent-guide/__tests__/tool-surface.test.ts +75 -0
  457. package/src/modules/agent-guide/agent-guide.tools.ts +51 -22
  458. package/src/modules/agent-guide/agent-guide.ts +120 -13
  459. package/src/modules/agent-guide/guide-budget.ts +73 -0
  460. package/src/modules/agent-guide/index.ts +12 -0
  461. package/src/modules/agent-instructions/__tests__/shared-file-rules.test.ts +5 -4
  462. package/src/modules/agent-instructions/compose.ts +7 -1
  463. package/src/modules/agent-instructions/shared-file-rules.ts +27 -9
  464. package/src/modules/agent-scope/__tests__/agent-surfaces.connect-as.test.ts +421 -0
  465. package/src/modules/agent-scope/__tests__/connection-scope.test.ts +46 -0
  466. package/src/modules/agent-scope/__tests__/in-memory-kb.ts +148 -0
  467. package/src/modules/agent-scope/agent-scope.contract.ts +83 -0
  468. package/src/modules/agent-scope/connection-scope.ts +199 -0
  469. package/src/modules/agent-scope/index.ts +15 -0
  470. package/src/modules/audit/agent-audit.service.ts +2 -0
  471. package/src/modules/audit/audit.contract.ts +6 -0
  472. package/src/modules/auth/__tests__/auth.middleware.test.ts +30 -2
  473. package/src/modules/auth/__tests__/auth.routes.test.ts +71 -5
  474. package/src/modules/auth/__tests__/auth.service.test.ts +90 -9
  475. package/src/modules/auth/__tests__/password-change-sessions.pg.test.ts +269 -0
  476. package/src/modules/auth/account-admission.ts +19 -0
  477. package/src/modules/auth/auth.middleware.ts +45 -4
  478. package/src/modules/auth/auth.routes.ts +46 -29
  479. package/src/modules/auth/auth.service.ts +135 -34
  480. package/src/modules/auth/index.ts +2 -0
  481. package/src/modules/auth/oidc-auth-provider.ts +2 -9
  482. package/src/modules/code-mode/code-mode.tool.ts +2 -2
  483. package/src/modules/connection-probe/__tests__/connection-probe.service.test.ts +5 -24
  484. package/src/modules/connection-probe/connection-probe.service.ts +1 -34
  485. package/src/modules/database/__tests__/session-version-migration.pg.test.ts +133 -0
  486. package/src/modules/database/core-schema.ts +33 -0
  487. package/src/modules/declared-variables/__tests__/declared-variables.route.test.ts +44 -1
  488. package/src/modules/declared-variables/declared-variables.routes.ts +12 -1
  489. package/src/modules/embed/__tests__/embed.tools.test.ts +12 -0
  490. package/src/modules/embed/embed.tools.ts +5 -0
  491. package/src/modules/kb-fs/__tests__/locking-filesystem.test.ts +25 -0
  492. package/src/modules/kb-fs/__tests__/repo-path.test.ts +53 -8
  493. package/src/modules/kb-fs/index.ts +2 -0
  494. package/src/modules/kb-fs/locking-filesystem.ts +7 -1
  495. package/src/modules/kb-fs/repo-path.ts +33 -7
  496. package/src/modules/kb-fs/tmp-ref.ts +81 -0
  497. package/src/modules/mcp/__tests__/connect-as.pg.test.ts +137 -0
  498. package/src/modules/mcp/__tests__/mcp-auth.middleware.test.ts +29 -5
  499. package/src/modules/mcp/__tests__/mcp-routes-harness.ts +8 -0
  500. package/src/modules/mcp/__tests__/mcp.connect-as.test.ts +195 -0
  501. package/src/modules/mcp/__tests__/mcp.routes.connect-as.test.ts +192 -0
  502. package/src/modules/mcp/__tests__/oauth-consent.connect-as.test.ts +146 -0
  503. package/src/modules/mcp/mcp-auth.middleware.ts +19 -2
  504. package/src/modules/mcp/mcp.routes.ts +39 -3
  505. package/src/modules/mcp/mcp.service.ts +112 -52
  506. package/src/modules/mcp/oauth/bevel-oauth-provider.ts +46 -8
  507. package/src/modules/mcp/oauth/oauth-consent.routes.ts +41 -1
  508. package/src/modules/plugins/__tests__/plugins.tools.test.ts +38 -2
  509. package/src/modules/plugins/index.ts +1 -1
  510. package/src/modules/plugins/plugin-links.ts +19 -2
  511. package/src/modules/plugins/plugins.routes.ts +35 -4
  512. package/src/modules/secrets-vault/__tests__/connect-pending.route.test.ts +113 -0
  513. package/src/modules/secrets-vault/__tests__/db-secrets-vault.oauth.test.ts +430 -3
  514. package/src/modules/secrets-vault/__tests__/oauth-live-declaration.route.test.ts +218 -0
  515. package/src/modules/secrets-vault/__tests__/oauth-return-to.route.test.ts +39 -6
  516. package/src/modules/secrets-vault/db-secrets-vault.service.ts +170 -27
  517. package/src/modules/secrets-vault/index.ts +1 -1
  518. package/src/modules/secrets-vault/secrets-vault.contract.ts +61 -11
  519. package/src/modules/secrets-vault/secrets-vault.routes.ts +114 -42
  520. package/src/modules/settings/oidc-check.ts +5 -18
  521. package/src/modules/skills/__tests__/skills.service.test.ts +34 -0
  522. package/src/modules/skills/index.ts +7 -2
  523. package/src/modules/skills/skills.contract.ts +17 -0
  524. package/src/modules/skills/skills.service.ts +5 -1
  525. package/src/modules/skills/skills.tools.ts +45 -14
  526. package/src/modules/tool-auth/__tests__/external-api-key.service.test.ts +4 -0
  527. package/src/modules/tool-auth/external-api-key.interface.ts +11 -1
  528. package/src/modules/tool-auth/external-api-key.service.ts +5 -2
  529. package/src/modules/tool-auth/index.ts +1 -1
  530. package/src/modules/tool-auth/internal-token.service.ts +10 -0
  531. package/src/modules/tool-auth/tool-auth.middleware.ts +45 -3
  532. package/src/modules/tool-helpers/__tests__/agent-roles-write.test.ts +19 -11
  533. package/src/modules/tool-helpers/__tests__/can-write.tools.test.ts +3 -1
  534. package/src/modules/tool-helpers/__tests__/route-tool-schemas.catalog.test.ts +38 -0
  535. package/src/modules/tool-helpers/tool-context.ts +10 -0
  536. package/src/modules/tool-helpers/tool-def.ts +1 -1
  537. package/src/modules/tool-helpers/tool-handler.ts +34 -0
  538. package/src/modules/tool-helpers/tool.contract.ts +10 -0
  539. package/src/modules/tool-helpers/validate-token.ts +4 -2
  540. package/src/modules/tool-manuals/__tests__/tool-manuals.archive.route.test.ts +80 -4
  541. package/src/modules/tool-manuals/__tests__/tool-manuals.mcp-oauth.test.ts +197 -11
  542. package/src/modules/tool-manuals/__tests__/tool-manuals.service.test.ts +42 -0
  543. package/src/modules/tool-manuals/__tests__/tool-manuals.tools.test.ts +99 -8
  544. package/src/modules/tool-manuals/index.ts +6 -2
  545. package/src/modules/tool-manuals/mcp-json-discovery.ts +73 -34
  546. package/src/modules/tool-manuals/tool-manuals.contract.ts +40 -6
  547. package/src/modules/tool-manuals/tool-manuals.routes.ts +40 -3
  548. package/src/modules/tool-manuals/tool-manuals.service.ts +169 -61
  549. package/src/modules/tool-manuals/tool-manuals.tools.ts +81 -25
  550. package/src/modules/tool-registry/__tests__/tool-description-length.test.ts +6 -113
  551. package/src/modules/tool-registry/manual.routes.ts +26 -5
  552. package/src/modules/tool-registry/tool.contract.ts +7 -0
  553. package/src/modules/workflow/__tests__/workflow.service.facade.test.ts +1 -1
  554. package/src/modules/workflow/__tests__/workflow.service.sync.test.ts +1 -1
  555. package/src/modules/workflow/agent-tools/__tests__/workflow.tools.test.ts +54 -3
  556. package/src/modules/workflow/agent-tools/workflow.tools.ts +36 -27
  557. package/src/modules/workflow/git/__tests__/git.service.list-branches.test.ts +130 -3
  558. package/src/modules/workflow/git/git.service.ts +29 -48
  559. package/src/modules/workflow/workflow.service.ts +5 -0
  560. package/src/modules/workspace/__tests__/git-internals.security.test.ts +2 -2
  561. package/src/modules/workspace/__tests__/holds-nul.test.ts +66 -0
  562. package/src/modules/workspace/__tests__/rest-tool-arguments.test.ts +3 -3
  563. package/src/modules/workspace/__tests__/ripgrep-search.test.ts +570 -0
  564. package/src/modules/workspace/__tests__/ripgrep-version.test.ts +29 -0
  565. package/src/modules/workspace/__tests__/workspace-tmp.tools.test.ts +928 -0
  566. package/src/modules/workspace/__tests__/workspace.routes.read-gate.test.ts +17 -37
  567. package/src/modules/workspace/__tests__/workspace.service.read-filter.test.ts +20 -8
  568. package/src/modules/workspace/__tests__/workspace.tools.agents-file.test.ts +10 -12
  569. package/src/modules/workspace/__tests__/workspace.tools.branch-errors.test.ts +22 -10
  570. package/src/modules/workspace/__tests__/workspace.tools.test.ts +549 -162
  571. package/src/modules/workspace/agent-access.gate.ts +1 -1
  572. package/src/modules/workspace/file-readers/__tests__/doc-extract.service.test.ts +10 -0
  573. package/src/modules/workspace/file-readers/doc-extract.service.ts +71 -38
  574. package/src/modules/workspace/index.ts +20 -0
  575. package/src/modules/workspace/nul-scan.ts +50 -0
  576. package/src/modules/workspace/pattern-matcher.contract.ts +123 -32
  577. package/src/modules/workspace/ripgrep-search.ts +715 -0
  578. package/src/modules/workspace/ripgrep-version.ts +35 -0
  579. package/src/modules/workspace/startup/__tests__/beside-checkout.test.ts +11 -0
  580. package/src/modules/workspace/startup/beside-checkout.ts +13 -5
  581. package/src/modules/workspace/worker-pool.ts +3 -4
  582. package/src/modules/workspace/workspace-tmp.ts +457 -0
  583. package/src/modules/workspace/workspace.routes.ts +6 -27
  584. package/src/modules/workspace/workspace.service.ts +92 -15
  585. package/src/modules/workspace/workspace.tools.ts +574 -176
  586. package/src/shared/__tests__/read-capped.test.ts +68 -0
  587. package/src/shared/oauth-token-error.ts +46 -0
  588. package/src/shared/path-order.ts +37 -8
  589. package/src/shared/read-capped.ts +40 -0
  590. package/starter-packs/engineering/Plugins/engineering-starter/access.md +1 -1
  591. package/starter-packs/general/Plugins/general-starter/access.md +1 -1
  592. package/starter-packs/operations/Plugins/operations-starter/access.md +1 -1
  593. package/starter-packs/sales/Plugins/sales-starter/access.md +1 -1
  594. package/starter-packs/support/Plugins/support-starter/access.md +1 -1
  595. package/agent-guide/finding-things.md +0 -7
  596. package/agent-guide/introduction.md +0 -27
  597. package/dist/modules/workspace/bounded-matcher.d.ts +0 -73
  598. package/dist/modules/workspace/bounded-matcher.d.ts.map +0 -1
  599. package/dist/modules/workspace/bounded-matcher.js +0 -200
  600. package/dist/modules/workspace/bounded-matcher.js.map +0 -1
  601. package/src/modules/workspace/__tests__/bounded-matcher.test.ts +0 -213
  602. package/src/modules/workspace/bounded-matcher.ts +0 -271
@@ -72,7 +72,7 @@ export interface SyncedGroupsSource {
72
72
 
73
73
  // Name safety is the SHARED predicate in `group-files.ts` (`unsafeNameReason`)
74
74
  // — the same rules the manual group editor asserts, including the reserved
75
- // `role/` prefix. Here the name arrives from the IdP, so an unsafe group is
75
+ // `role/` and `group/` prefixes. Here the name arrives from the IdP, so an unsafe group is
76
76
  // SKIPPED with a warning instead of erroring (fail-closed: an unrepresentable
77
77
  // group grants nothing).
78
78
 
@@ -6,8 +6,44 @@ import {
6
6
  accessFrontmatterExtensionList,
7
7
  parseOwnAccessEntries,
8
8
  parseRolesYaml,
9
+ parseAccessEntry,
10
+ mergeGroupsIntoRoles,
9
11
  } from '../access-grammar.js';
10
- import { parseGroupsFile } from '../group-files.js';
12
+ import { parseGroupsFile, unsafeNameReason } from '../group-files.js';
13
+
14
+ describe('principal spellings — a bare name is the role, group/<Name> the group', () => {
15
+ it('parses group/<Name> into the canonical group token, spacing normalised', () => {
16
+ const r = parseAccessEntry('group/ Product Team');
17
+ expect(r).toEqual({ ok: true, entry: { kind: 'role', role: 'group/product team', displayRole: 'group/ Product Team', deny: false } });
18
+ const d = parseAccessEntry('deny group/Admin');
19
+ expect(d.ok && d.entry.kind === 'role' && d.entry.role).toBe('group/admin');
20
+ expect(parseAccessEntry('group/').ok).toBe(false);
21
+ });
22
+
23
+ it('reserves the group/ prefix for roles and groups alike', () => {
24
+ const roles = parseRolesYaml('roles:\n Admin:\n - a@x.io\n group/Ops:\n - b@x.io\n');
25
+ expect(roles.ok).toBe(false);
26
+ if (!roles.ok) expect(roles.errors.join(' ')).toMatch(/reserved 'group\/' prefix/);
27
+ expect(unsafeNameReason('group/Ops')).toMatch(/group\//);
28
+ });
29
+
30
+ it('merging keeps the bare key for the role and registers group/ for every group', () => {
31
+ const parsed = parseRolesYaml('roles:\n Admin:\n - admin@x.io\n');
32
+ if (!parsed.ok) throw new Error('roles');
33
+ const groups = parseGroupsFile('groups:\n Admin:\n - mallory@x.io\n Sales:\n - sam@x.io\n', 'groups.yaml');
34
+ if (!groups.ok) throw new Error('groups');
35
+ const warnings = mergeGroupsIntoRoles(parsed.index, groups.groups, 'groups.yaml');
36
+ const idx = parsed.index.byCanonical;
37
+ expect(idx.get('admin')?.kind).toBe('role');
38
+ expect(idx.get('role/admin')).toBe(idx.get('admin'));
39
+ expect(idx.get('group/admin')?.kind).toBe('group');
40
+ expect(idx.get('sales')?.kind).toBe('group');
41
+ expect(idx.get('group/sales')).toBe(idx.get('sales'));
42
+ // The group's member holds only the group's keys, never the role's.
43
+ expect([...(parsed.index.byEmail.get('mallory@x.io') ?? [])]).toEqual(['group/admin']);
44
+ expect(warnings.join(' ')).toMatch(/resolves to the ROLE; use 'group\/admin'/);
45
+ });
46
+ });
11
47
 
12
48
  describe('parseYamlSubset — inline empty collections', () => {
13
49
  it('reads `key: []` as an empty list, not the scalar "[]"', () => {
@@ -103,6 +139,7 @@ describe('the access-frontmatter extension set', () => {
103
139
  expect(accessFrontmatterExtensionList()).toHaveLength(after);
104
140
  expect(accessFrontmatterExtensionList()).toContain('.md');
105
141
  expect(accessFrontmatterExtensionList()).toContain('.tool');
142
+ expect(accessFrontmatterExtensionList()).not.toContain('.agent');
106
143
  });
107
144
 
108
145
  it('throws on a malformed extension rather than skipping it', () => {
@@ -302,12 +302,13 @@ describe('round-trip — every granted entry parses back', () => {
302
302
  });
303
303
  });
304
304
 
305
- describe('token-kind family — exact-token grants, shadow-aware revokes', () => {
306
- // One name, two spellings, two possible principals: the bare token is the
307
- // GROUP's whenever a group shadows the name (group-first precedence), the
308
- // `role/` token is always the ROLE's. Grants must be exact-token idempotent;
309
- // revokes are alias-tolerant ONLY when unshadowed (the caller passes
310
- // tokenMatch: 'exact' when a group owns the bare name).
305
+ describe('token-kind family — exact-token grants, kind-aware revokes', () => {
306
+ // One name, three spellings, two possible principals: `role/` is always the
307
+ // ROLE's, `group/` always the GROUP's, and the bare token is the role's when
308
+ // one exists, else the group's. Grants must be exact-token idempotent;
309
+ // revokes are alias-tolerant (explicit + bare, never the other kind's
310
+ // explicit spelling) only when the bare name is the principal's — the
311
+ // caller passes tokenMatch: 'exact' otherwise.
311
312
  const SHARED = `---
312
313
  write:
313
314
  - role/Sales
@@ -360,10 +361,51 @@ write:
360
361
  expect(lines).not.toContain(' - Sales');
361
362
  });
362
363
 
363
- it("REVOKE unshadowed bare principal (group since vanished): 'name' matching strips both spellings", () => {
364
+ it("REVOKE bare principal: 'name' matching strips both role spellings", () => {
364
365
  const r = spliceRevoke(SHARED, 'write', { kind: 'role', role: 'Sales' });
365
366
  expect(r.changed).toBe(true);
366
- expect(r.text).not.toContain('Sales'); // bare token falls back to the role — same principal
367
+ expect(r.text).not.toContain('Sales'); // the bare token is the role — same principal
368
+ });
369
+
370
+ const ALL = `---
371
+ write:
372
+ - role/Sales
373
+ - Sales
374
+ - group/Sales
375
+ - Admin
376
+ ---
377
+ `;
378
+
379
+ it('GRANT is exact-token: a group/<Name> grant goes through beside the bare and role/ spellings', () => {
380
+ const md = '---\nwrite:\n - role/Sales\n - Sales\n---\n';
381
+ const r = spliceGrant(md, 'write', { kind: 'role', role: 'group/Sales' });
382
+ expect(r.changed).toBe(true);
383
+ expect(r.text.split('\n')).toContain(' - group/Sales');
384
+ expect(spliceGrant(ALL, 'write', { kind: 'role', role: 'group/Sales' }).changed).toBe(false);
385
+ });
386
+
387
+ it("REVOKE 'name' never crosses kinds: the role's revoke keeps group/, the group's keeps role/", () => {
388
+ const asRole = spliceRevoke(ALL, 'write', { kind: 'role', role: 'role/Sales' });
389
+ expect(asRole.text.split('\n')).toEqual(['---', 'write:', ' - group/Sales', ' - Admin', '---', '']);
390
+
391
+ // The service asks 'name' for a GROUP only while no role shares the name
392
+ // (with one, as in ALL, it picks 'exact' — the last test below), so the
393
+ // group's case is a file where the bare token can only be the group.
394
+ const GROUP_ONLY = '---\nwrite:\n - Sales\n - group/Sales\n - Admin\n---\n';
395
+ const asGroup = spliceRevoke(GROUP_ONLY, 'write', { kind: 'role', role: 'group/Sales' });
396
+ expect(asGroup.text.split('\n')).toEqual(['---', 'write:', ' - Admin', '---', '']);
397
+ });
398
+
399
+ it('REVOKE matches a spaced `group/ <Name>` request against the parsed `group/<name>` entry', () => {
400
+ const r = spliceRevoke(ALL, 'write', { kind: 'role', role: 'group/ Sales' }, { tokenMatch: 'exact' });
401
+ expect(r.text.split('\n')).toEqual(['---', 'write:', ' - role/Sales', ' - Sales', ' - Admin', '---', '']);
402
+ const md = '---\nwrite:\n - group/ Sales\n---\n';
403
+ expect(spliceRevoke(md, 'write', { kind: 'role', role: 'group/Sales' }).changed).toBe(true);
404
+ });
405
+
406
+ it("REVOKE 'exact' of group/<Name> strips only that spelling", () => {
407
+ const r = spliceRevoke(ALL, 'write', { kind: 'role', role: 'group/Sales' }, { tokenMatch: 'exact' });
408
+ expect(r.text.split('\n')).toEqual(['---', 'write:', ' - role/Sales', ' - Sales', ' - Admin', '---', '']);
367
409
  });
368
410
  });
369
411
 
@@ -92,14 +92,47 @@ export const DENY_PREFIX = 'deny ';
92
92
  export const GROUP_REF_PREFIX = 'group:';
93
93
  /**
94
94
  * Explicit ROLE token prefix in access.md entries: `role/<Name>` resolves to
95
- * the roles.yaml role only, never a group. A BARE name resolves GROUP-FIRST
96
- * and falls back to the role — so `role/` is the escape hatch when a group
97
- * shares the role's name. The prefix is reserved in the group name-safety
98
- * rules (a group may never be named `role/...`), and every role is also
99
- * registered in the principal index under its `role/<canonical>` alias.
95
+ * the roles.yaml role only, never a group. A BARE name resolves ROLE-FIRST
96
+ * and falls back to the group, so a group that shares a role's name is
97
+ * reached only as `group/<Name>` (see `GROUP_TOKEN_PREFIX`) — a group named
98
+ * `Admin` never takes over the grants written for the Admin role. The prefix
99
+ * is reserved in the group name-safety rules (a group may never be named
100
+ * `role/...`), and every role is also registered in the principal index under
101
+ * its `role/<canonical>` alias.
100
102
  */
101
103
  export const ROLE_TOKEN_PREFIX = 'role/';
102
104
 
105
+ /**
106
+ * Explicit GROUP token prefix in access.md entries: `group/<Name>` resolves
107
+ * to the active source's group only, never a role. Every group is registered
108
+ * in the principal index under its `group/<canonical>` alias; the bare name
109
+ * reaches the group only while no role of that name exists. The share dialog
110
+ * and the access API always write groups this way. Reserved in the group
111
+ * name-safety rules and in roles.yaml like `role/`.
112
+ */
113
+ export const GROUP_TOKEN_PREFIX = 'group/';
114
+
115
+ /**
116
+ * A canonical role-shaped token split into its explicit kind prefix and its
117
+ * bare name: `role/admin` → `{ prefix: 'role', name: 'admin' }`, `group/admin`
118
+ * → `{ prefix: 'group', name: 'admin' }`, anything else (a bare name, a
119
+ * plugin token, `everyone`) → `{ prefix: null, name: <token> }`. A prefixed
120
+ * name is canonicalised again, so `group/ admin` splits like `group/admin`
121
+ * (as `parseAccessEntry` reads it).
122
+ */
123
+ export function splitPrincipalToken(canonical: string): {
124
+ prefix: 'role' | 'group' | null;
125
+ name: string;
126
+ } {
127
+ if (canonical.startsWith(ROLE_TOKEN_PREFIX)) {
128
+ return { prefix: 'role', name: canonicalRoleName(canonical.slice(ROLE_TOKEN_PREFIX.length)) };
129
+ }
130
+ if (canonical.startsWith(GROUP_TOKEN_PREFIX)) {
131
+ return { prefix: 'group', name: canonicalRoleName(canonical.slice(GROUP_TOKEN_PREFIX.length)) };
132
+ }
133
+ return { prefix: null, name: canonical };
134
+ }
135
+
103
136
  /**
104
137
  * PLUGIN-principal token prefix in access.md entries: `plugin/<Name>/<verb>`
105
138
  * names everyone who holds `<verb>` on the plugin folder `Plugins/<Name>` —
@@ -585,6 +618,11 @@ export function parseAccessEntry(
585
618
  const suffix = canonicalRoleName(role.slice(ROLE_TOKEN_PREFIX.length));
586
619
  if (!suffix) return { ok: false, error: `entry '${body}' names no role after '${ROLE_TOKEN_PREFIX}'` };
587
620
  role = `${ROLE_TOKEN_PREFIX}${suffix}`;
621
+ } else if (role.startsWith(GROUP_TOKEN_PREFIX)) {
622
+ // Explicit group token, normalised the same way to `group/<canonicalName>`.
623
+ const suffix = canonicalRoleName(role.slice(GROUP_TOKEN_PREFIX.length));
624
+ if (!suffix) return { ok: false, error: `entry '${body}' names no group after '${GROUP_TOKEN_PREFIX}'` };
625
+ role = `${GROUP_TOKEN_PREFIX}${suffix}`;
588
626
  } else if (role.startsWith(PLUGIN_TOKEN_PREFIX)) {
589
627
  // `plugin/<Name>/<verb>`: the name is canonicalised to the plugin's
590
628
  // manifest slug, the verb must be one of the three. Anything else is a
@@ -649,6 +687,12 @@ export function parseRolesYaml(
649
687
  );
650
688
  continue;
651
689
  }
690
+ if (canonical.startsWith(GROUP_TOKEN_PREFIX)) {
691
+ errors.push(
692
+ `roles.yaml: role '${displayName}' starts with the reserved '${GROUP_TOKEN_PREFIX}' prefix — that spelling is the explicit group token in access entries`,
693
+ );
694
+ continue;
695
+ }
652
696
  // A role spelled `plugin/…` would be overwritten by the synthesised plugin
653
697
  // principal of the same key while its members kept the token — plugin
654
698
  // access for people who are not plugin members. Refused at parse time,
@@ -730,15 +774,17 @@ export function parseRolesYaml(
730
774
  *
731
775
  * Rules (the grant-grammar precedence):
732
776
  * - Every role is ALSO registered under its explicit `role/<canonical>`
733
- * alias — the token that always resolves to the role.
734
- * - A BARE name resolves GROUP-FIRST: when a group's canonical name
735
- * collides with a role's, the bare key resolves to the GROUP (warned);
736
- * the role stays reachable via `role/<canonical>`.
777
+ * alias, and every group under its explicit `group/<canonical>` alias —
778
+ * the tokens that always resolve to that kind.
779
+ * - A BARE name resolves ROLE-FIRST: when a group's canonical name
780
+ * collides with a role's, the bare key stays the ROLE's (warned); the
781
+ * group is reachable only via `group/<canonical>`. So a group named
782
+ * `Admin` — whoever proposed it — never gains what `Admin` is granted.
737
783
  * - Role `group:<Name>` refs — Admin's included — expand against the
738
784
  * merged groups; an unknown ref contributes nothing (warned).
739
785
  * - `byEmail` is rebuilt so each member holds exactly the tokens that
740
786
  * resolve to a principal they belong to (bare + `role/` alias for roles,
741
- * bare for groups).
787
+ * `group/` alias plus the bare name when the group owns it for groups).
742
788
  */
743
789
  export function mergeGroupsIntoRoles(
744
790
  index: RolesIndex,
@@ -765,19 +811,18 @@ export function mergeGroupsIntoRoles(
765
811
  }
766
812
  }
767
813
 
768
- // 2. Bare-name precedence: groups win the bare token; the role keeps its
769
- // `role/<canonical>` alias registered below.
814
+ // 2. Bare-name precedence: roles keep the bare token; every group gets its
815
+ // `group/<canonical>` alias, and the bare key only when no role owns it.
770
816
  for (const [canonical, def] of groups) {
817
+ const group = { displayName: def.displayName, emails: new Set(def.emails), kind: 'group' as const };
818
+ index.byCanonical.set(`${GROUP_TOKEN_PREFIX}${canonical}`, group);
771
819
  if (roleRecords.has(canonical)) {
772
820
  warnings.push(
773
- `${sourceFile}: group '${def.displayName}' shares its name with a role — the bare name now resolves to the GROUP; use '${ROLE_TOKEN_PREFIX}${canonical}' to reference the role`,
821
+ `${sourceFile}: group '${def.displayName}' shares its name with a role — the bare name resolves to the ROLE; use '${GROUP_TOKEN_PREFIX}${canonical}' to reference the group`,
774
822
  );
823
+ continue;
775
824
  }
776
- index.byCanonical.set(canonical, {
777
- displayName: def.displayName,
778
- emails: new Set(def.emails),
779
- kind: 'group',
780
- });
825
+ index.byCanonical.set(canonical, group);
781
826
  }
782
827
 
783
828
  // 3. Explicit `role/<canonical>` alias for every role (same record — the
@@ -788,8 +833,8 @@ export function mergeGroupsIntoRoles(
788
833
  }
789
834
 
790
835
  // 4. Rebuild the email → tokens map from the final index so membership
791
- // reflects the post-precedence keys (a collided role's members no longer
792
- // hold the bare token unless the group also contains them).
836
+ // reflects the post-precedence keys (a collided group's members hold
837
+ // only its `group/` alias, never the role's bare token).
793
838
  index.byEmail.clear();
794
839
  for (const [key, principal] of index.byCanonical) {
795
840
  for (const email of principal.emails) {
@@ -33,6 +33,7 @@ import {
33
33
  canonicalEmail,
34
34
  canonicalRoleName,
35
35
  accessMdDeclaresBodyRules,
36
+ splitPrincipalToken,
36
37
  } from './access-grammar.js';
37
38
  import { freshAccessMd } from './access-template.js';
38
39
  import { scanFrontmatter } from './frontmatter-lines.js';
@@ -149,15 +150,18 @@ export function validatePrincipal(p: Principal): Principal {
149
150
  /**
150
151
  * How a role-shaped principal's TOKEN is compared against a file entry:
151
152
  *
152
- * - `'exact'`: the entry must carry the SAME spelling — a bare token and a
153
- * `role/<name>` token are DIFFERENT principals (bare = whoever owns the
154
- * bare key under group-first precedence, `role/` = the role). Grants
155
- * always use this: granting the group `X` while `role/X` is present (or
156
- * vice versa) must NOT be treated as already-granted, or the other
157
- * principal silently never receives the grant.
158
- * - `'name'`: both spellings of the name match. Revokes use this ONLY when
159
- * no group shadows the bare name — then both spellings resolve to the
160
- * role, and revoking the role must also remove legacy bare spellings.
153
+ * - `'exact'`: the entry must carry the SAME spelling — a bare token, a
154
+ * `role/<name>` token and a `group/<name>` token can be DIFFERENT
155
+ * principals (bare = the role when one exists, else the group). Grants
156
+ * always use this: granting `group/X` while `role/X` or a bare `X` is
157
+ * present must NOT be treated as already-granted, or the group silently
158
+ * never receives the grant.
159
+ * - `'name'`: the principal's explicit spelling AND the bare name match —
160
+ * `role/X` (or a bare `X`) matches `X` and `role/X`; `group/X` matches
161
+ * `X` and `group/X`. Never across kinds: a role's revoke never touches
162
+ * `group/X`, a group's never `role/X`. Revokes use this only when the
163
+ * bare name is this principal's (AccessMutationService decides), so
164
+ * revoking also removes the legacy bare spelling.
161
165
  */
162
166
  export type TokenMatch = 'exact' | 'name';
163
167
 
@@ -169,11 +173,14 @@ export type TokenMatch = 'exact' | 'name';
169
173
  function entryMatches(entry: ParsedEntry, p: Principal, tokenMatch: TokenMatch): boolean {
170
174
  if (p.kind === 'user') return entry.kind === 'user' && entry.email === canonicalEmail(p.email);
171
175
  if (entry.kind !== 'role') return false;
172
- const canonicalPrincipal = canonicalRoleName(p.role);
173
- if (tokenMatch === 'exact') return entry.role === canonicalPrincipal;
174
- const strip = (token: string): string =>
175
- token.startsWith('role/') ? canonicalRoleName(token.slice('role/'.length)) : token;
176
- return strip(entry.role) === strip(canonicalPrincipal);
176
+ // Compare split, canonical halves: `group/ Product` in the request names the
177
+ // same principal as the `group/product` the parser reads from the file.
178
+ const own = splitPrincipalToken(canonicalRoleName(p.role));
179
+ const theirs = splitPrincipalToken(entry.role);
180
+ if (theirs.name !== own.name) return false;
181
+ if (tokenMatch === 'exact') return theirs.prefix === own.prefix;
182
+ // Same family only: a bare principal stands for the role, as `role/` does.
183
+ return theirs.prefix === null || theirs.prefix === (own.prefix ?? 'role');
177
184
  }
178
185
 
179
186
  // ---------------------------------------------------------------------------
@@ -344,10 +351,10 @@ function blockEntries(fm: string[], block: VerbBlock): { entry: ParsedEntry; lin
344
351
  * Add `principal` under `verb`. Idempotent: if an entry with the EXACT same
345
352
  * token spelling already exists with the SAME deny-ness (a grant when
346
353
  * granting, a deny when denying), returns `changed: false`. Idempotency is
347
- * deliberately exact-token, never name-level: a bare token and a `role/`
348
- * token are different principals (group-first precedence gives the bare key
349
- * to a same-named group), so a bare-`X` GROUP grant must go through even when
350
- * `role/X` is present, and vice versa. Creates the verb key (and the
354
+ * deliberately exact-token, never name-level: a bare token, a `role/` token
355
+ * and a `group/` token can be different principals (the bare key is the
356
+ * role's when one exists), so a `group/X` grant must go through even when a
357
+ * bare `X` or `role/X` is present, and vice versa. Creates the verb key (and the
351
358
  * frontmatter fence, for a fresh file) as needed. Preserves everything else
352
359
  * byte-for-byte.
353
360
  *
@@ -445,11 +452,12 @@ export function spliceGrant(
445
452
  * Preserves everything else.
446
453
  *
447
454
  * Token matching for role-shaped principals is `tokenMatch` (default `'name'`:
448
- * BOTH spellings — revoking the role removes legacy bare spellings too).
449
- * Callers pass `'exact'` when a GROUP shadows the principal's bare name: the
450
- * bare token then belongs to the group and the `role/` token to the role, so a
451
- * revoke of one must never strip the other (see AccessMutationService, which
452
- * derives the flag from the merged principal index).
455
+ * the explicit spelling and the bare one — revoking removes legacy bare
456
+ * spellings too). Callers pass `'exact'` when the bare name belongs to the
457
+ * OTHER kind (a group revoke while a same-named role owns the bare key, or a
458
+ * role revoke while only a group of that name exists), so a revoke of one
459
+ * never strips the other (see AccessMutationService, which derives the flag
460
+ * from the merged principal index).
453
461
  */
454
462
  export function spliceRevoke(
455
463
  text: string,
@@ -4,6 +4,7 @@ import {
4
4
  RESERVED_ROLE_NAMES,
5
5
  PLUGIN_TOKEN_PREFIX,
6
6
  ROLE_TOKEN_PREFIX,
7
+ GROUP_TOKEN_PREFIX,
7
8
  canonicalEmail,
8
9
  canonicalRoleName,
9
10
  parseYamlSubset,
@@ -58,6 +59,7 @@ export const SYNCED_GROUPS_YAML = 'synced-groups.yaml';
58
59
  * \x00-\x1f control chars / newlines break line structure outright
59
60
  * `role/…` reserved: that spelling is the EXPLICIT role token in access
60
61
  * entries — a group carrying it could never be referenced.
62
+ * `group/…` reserved likewise: the EXPLICIT group token.
61
63
  */
62
64
  export function unsafeNameReason(displayName: string): string | null {
63
65
  const trimmed = displayName.trim();
@@ -69,6 +71,9 @@ export function unsafeNameReason(displayName: string): string | null {
69
71
  if (canonicalRoleName(trimmed).startsWith(ROLE_TOKEN_PREFIX)) {
70
72
  return `starts with the reserved '${ROLE_TOKEN_PREFIX}' prefix (the explicit role token in access entries)`;
71
73
  }
74
+ if (canonicalRoleName(trimmed).startsWith(GROUP_TOKEN_PREFIX)) {
75
+ return `starts with the reserved '${GROUP_TOKEN_PREFIX}' prefix (the explicit group token in access entries)`;
76
+ }
72
77
  if (canonicalRoleName(trimmed).startsWith(PLUGIN_TOKEN_PREFIX)) {
73
78
  return `starts with the reserved '${PLUGIN_TOKEN_PREFIX}' prefix (the plugin-principal token in access entries)`;
74
79
  }
@@ -1,7 +1,7 @@
1
1
  import { DEFAULT_KB_LAYOUT } from '@bevel-software/platform-shared';
2
2
  import { describe, expect, it } from 'vitest';
3
- import { mergeGroupsIntoRoles, parseRolesYaml } from '../../access-model/access-grammar.js';
4
- import { sharedFileRulesSection } from '../../agent-instructions/shared-file-rules.js';
3
+ import { mergeGroupsIntoRoles, parseRolesYaml } from '../../access-model/index.js';
4
+ import { sharedFileRulesSection } from '../../agent-instructions/index.js';
5
5
  import {
6
6
  CORE_SECTION_IDS,
7
7
  PLATFORM_GUIDE_SEPARATOR,
@@ -9,6 +9,7 @@ import {
9
9
  agentGuideSections,
10
10
  composeAgentGuide,
11
11
  coreAgentGuideSections,
12
+ guideStartText,
12
13
  isAgentGuidePath,
13
14
  isManagedGuide,
14
15
  joinGuideSections,
@@ -44,11 +45,8 @@ describe('the guide is composed from the platform\'s sections', () => {
44
45
  const rules = sections.find((s) => s.id === WORKING_WITH_FILES_SECTION_ID)!;
45
46
  expect(rules.body).toBe(sharedFileRulesSection(DEFAULT_KB_LAYOUT));
46
47
  expect(rules.literal).toBe(true);
47
- // Every file section begins with its heading, so the guide reads as one document.
48
- for (const s of sections) {
49
- if (s.id === 'introduction') expect(s.body.startsWith('# Knowledge base')).toBe(true);
50
- else expect(s.body.startsWith('## '), s.id).toBe(true);
51
- }
48
+ // Every section begins with its heading, so the guide reads as one document.
49
+ for (const s of sections) expect(s.body.startsWith('## '), s.id).toBe(true);
52
50
  // The order of the composed guide is the order of the sections — pinned
53
51
  // exactly above by the id list, and by the join test below, which equates
54
52
  // the composed guide with the sections joined.
@@ -72,12 +70,13 @@ describe('the guide is composed from the platform\'s sections', () => {
72
70
  });
73
71
 
74
72
  it('says it is served, not written, and names the one file it is read as', async () => {
75
- const intro = section(await composeAgentGuide(DEFAULT_KB_LAYOUT), '# Knowledge base').replace(/\n> ?/g, ' ');
76
- expect(intro).toContain('**This guide is served by the platform.**');
77
- expect(intro).toContain('`get_agent_guide` returns it, whole or one section at a time');
78
- expect(intro).toContain('`read_file` on `AGENTS.md` at the repository root');
79
- expect(intro).toContain('`grep` searches it there too');
80
- expect(intro).toContain('never write this text into it');
73
+ const guide0 = await composeAgentGuide(DEFAULT_KB_LAYOUT);
74
+ const start = section(guide0, '## Start here').replace(/\s+/g, ' ');
75
+ expect(start).toContain('`get_agent_guide({ body: { section: "all" } })` returns the whole guide');
76
+ // By id, not by heading: the section shows an AGENTS.md snippet whose own `##` heading sits in a code fence.
77
+ const finding = (await agentGuideSections(DEFAULT_KB_LAYOUT)).find((s) => s.id === 'how-agents-find-things')!.body.replace(/\s+/g, ' ');
78
+ expect(finding).toContain('`read_file` on `AGENTS.md` returns that file first, then the start of this guide');
79
+ expect(finding).toContain('never copy this guide into either: the platform serves it');
81
80
  // The old header, which proved a file on disk was the platform's, is gone
82
81
  // from the served text — or every read would look like a stale copy.
83
82
  expect(isManagedGuide(await composeAgentGuide(DEFAULT_KB_LAYOUT))).toBe(false);
@@ -95,7 +94,7 @@ describe('the guide is composed from the platform\'s sections', () => {
95
94
  it('serves the guide as sections with titles, the whole being the sections joined', async () => {
96
95
  const sections = await agentGuideSections(DEFAULT_KB_LAYOUT);
97
96
  expect(sections.map((s) => s.id)).toEqual(CORE_SECTION_IDS);
98
- expect(sections.find((s) => s.id === 'introduction')!.title).toBe('Knowledge base');
97
+ expect(sections.find((s) => s.id === 'start-here')!.title).toBe('Start here');
99
98
  expect(sections.find((s) => s.id === 'where-a-new-file-goes')!.title).toBe('Where a new file goes');
100
99
  expect(sections.find((s) => s.id === 'skills')!.title).toContain('Skills (`Skills/');
101
100
  expect(joinGuideSections(sections)).toBe(await composeAgentGuide(DEFAULT_KB_LAYOUT));
@@ -120,13 +119,13 @@ describe('the guide is composed from the platform\'s sections', () => {
120
119
  const hook = (sections: readonly AgentGuideSection[]) => [
121
120
  ...sections
122
121
  .filter((s) => s.id !== 'conventions')
123
- .map((s) => (s.id === 'finding-things' ? { ...s, body: '## Finding things\n\nAsk the graph.\n' } : s)),
122
+ .map((s) => (s.id === 'glossary' ? { ...s, body: '## Words used here\n\nAsk the graph.\n' } : s)),
124
123
  { id: 'knowledge-graph', body: '## Knowledge graph\n\nNodes live under `{{knowledgeBaseDir}}/<Ontology>/Knowledge/`.\n' },
125
124
  ];
126
125
  const guide = await composeAgentGuide({ ...DEFAULT_KB_LAYOUT, knowledgeBaseDir: 'Docs' }, hook);
127
126
  expect(guide).not.toContain('## Conventions');
128
- expect(section(guide, '## Finding things')).toContain('Ask the graph.');
129
- expect(section(guide, '## Finding things')).not.toContain('grep');
127
+ expect(section(guide, '## Words used here')).toContain('Ask the graph.');
128
+ expect(section(guide, '## Words used here')).not.toContain('Plugin');
130
129
  expect(guide.trimEnd().endsWith('Nodes live under `Docs/<Ontology>/Knowledge/`.')).toBe(true);
131
130
  expect(guide).toContain(sharedFileRulesSection({ ...DEFAULT_KB_LAYOUT, knowledgeBaseDir: 'Docs' }));
132
131
  // An async hook is awaited, and the layout it is handed is the resolved one.
@@ -138,6 +137,61 @@ describe('the guide is composed from the platform\'s sections', () => {
138
137
  expect(seen).toEqual(['Docs', 'AGENTS.md']);
139
138
  });
140
139
 
140
+ it('routes the start section to a section a distribution adds, when that section says what it answers', async () => {
141
+ const hook = (sections: readonly AgentGuideSection[]) => [
142
+ ...sections,
143
+ { id: 'graph-rules', body: '## Graph rules\n\nNodes carry an id.\n', asks: 'How is a graph node written? Which | fields?' },
144
+ { id: 'quiet', body: '## Quiet\n\nNot routed.\n' },
145
+ // Already in the table: no second row.
146
+ { id: 'tools', body: '## Tools again\n\nA second copy.\n', asks: 'When can I use a tool?' },
147
+ ];
148
+ const start = (await agentGuideSections(DEFAULT_KB_LAYOUT, hook)).find((s) => s.id === 'start-here')!.body;
149
+ const rows = start.split('\n').filter((line) => line.startsWith('|'));
150
+ // After the table's last row, with a `|` in the question escaped.
151
+ expect(rows.at(-1)).toBe('| How is a graph node written? Which \\| fields? | `graph-rules` |');
152
+ expect(start).not.toContain('`quiet`');
153
+ expect(rows.filter((line) => line.endsWith('| `tools` |'))).toHaveLength(1);
154
+ // Core's own guide is left as it was.
155
+ const core = (await agentGuideSections(DEFAULT_KB_LAYOUT)).find((s) => s.id === 'start-here')!.body;
156
+ expect(core).not.toContain('graph-rules');
157
+ });
158
+
159
+ it('serves a distribution\'s own version of a core section in core\'s place, when the hook adds it under the same id', async () => {
160
+ const hook = (sections: readonly AgentGuideSection[]) => [
161
+ ...sections,
162
+ // The enterprise way: its own sections appended, two of them under core ids.
163
+ { id: 'html-views', body: '## Pages, our way\n\nOurs.\n' },
164
+ { id: 'start-here', body: '## Start here\n\nOurs first.\n\n| Question | Section |\n|---|---|\n| Pages? | `html-views` |\n' },
165
+ { id: 'graph', body: '## Graph\n\nNodes.\n', asks: 'How is the graph written?' },
166
+ ];
167
+ const sections = await agentGuideSections(DEFAULT_KB_LAYOUT, hook);
168
+ const core = await agentGuideSections(DEFAULT_KB_LAYOUT);
169
+ // One section per id, each where core's stood, the hook's text in it.
170
+ expect(sections.map((s) => s.id)).toEqual([...core.map((s) => s.id), 'graph']);
171
+ expect(sections.find((s) => s.id === 'html-views')!.body).toBe('## Pages, our way\n\nOurs.');
172
+ const start = sections.find((s) => s.id === 'start-here')!.body;
173
+ expect(start.startsWith('## Start here\n\nOurs first.')).toBe(true);
174
+ // The overriding start section is the one routed to: a row for the added section lands in ITS table.
175
+ expect(start).toContain('| How is the graph written? | `graph` |');
176
+ expect(start).not.toContain('`for-git-users`');
177
+ // The whole guide carries the override once, and never core's text under that id.
178
+ const guide = joinGuideSections(sections);
179
+ expect(guide.match(/^## Pages, our way$/gm)).toHaveLength(1);
180
+ expect(guide).not.toContain('## HTML views');
181
+ });
182
+
183
+ it('answers a read of AGENTS.md with the start section and every section by id, not the whole guide', async () => {
184
+ const sections = await agentGuideSections(DEFAULT_KB_LAYOUT, undefined, { defaultBranch: 'main' });
185
+ const text = guideStartText(sections);
186
+ const start = sections.find((s) => s.id === 'start-here')!;
187
+ expect(text.startsWith(start.body)).toBe(true);
188
+ for (const s of sections) expect(text).toContain(`- \`${s.id}\`: ${s.title}`);
189
+ expect(text).not.toContain(sections.find((s) => s.id === 'access-control')!.body);
190
+ // A guide a distribution left without a start section is answered whole.
191
+ const without = sections.filter((s) => s.id !== 'start-here');
192
+ expect(guideStartText(without)).toBe(joinGuideSections(without));
193
+ });
194
+
141
195
  it('refuses a hook that drops or empties the shared file rules, which every file tool points at', async () => {
142
196
  await expect(
143
197
  composeAgentGuide(DEFAULT_KB_LAYOUT, (sections) => sections.filter((s) => s.id !== WORKING_WITH_FILES_SECTION_ID)),
@@ -250,8 +304,8 @@ describe('what the guide tells an agent', () => {
250
304
  expect(placement).toContain('Extensions/personal-<id>/');
251
305
  // A skill's own bundled files are part of the skill and stay welcome, so the
252
306
  // rule does not deter an agent from writing a COMPLETE skill.
253
- expect(placement).toContain("each skill's own bundled files");
254
- expect(placement).toContain("inside that skill's folder included");
307
+ expect(placement).toContain("each skill's own files");
308
+ expect(placement).toContain("in that skill's folder");
255
309
  expect(placement).toContain('never a note or any other document');
256
310
  // A private request gets a question and the restrictable folder, in the
257
311
  // deployment's own root name.
@@ -259,8 +313,8 @@ describe('what the guide tells an agent', () => {
259
313
  expect(placement).toContain('restricted so only they can read it');
260
314
  // And when the user insists, the agent declines, says why, and offers again.
261
315
  expect(placement).toContain('If they insist on the personal plugin, decline');
262
- expect(placement).toContain('sits outside the knowledge graph, where it is never found as knowledge again');
263
- expect(placement).toContain('offer a place under `Docs/` once more');
316
+ expect(placement).toContain('only they would ever find it, and never where the organisation looks for knowledge');
317
+ expect(placement).toContain('offer a place under `Docs/` again');
264
318
  // Nothing tells the agent to move or flag documents already filed there.
265
319
  expect(placement).not.toMatch(/\bmove (them|it|any)\b/);
266
320
  });
@@ -281,8 +335,8 @@ describe('what the guide tells an agent', () => {
281
335
  // The four places that introduce it: the `my_plugin` bullet, the sentence
282
336
  // on moving a skill, the placement rule, and the `everyone` note.
283
337
  const prose = guide.replace(/\s+/g, ' ');
284
- expect(prose).toContain("`my_plugin` — your user's personal plugin, holding their own skills and tools");
285
- expect(prose).toContain('A skill moves from a personal plugin into a shared plugin by moving its folder.');
338
+ expect(prose).toContain(`\`my_plugin\`: the user's personal plugin, \`${plugins}/personal-<id>/\`, created on first use.`);
339
+ expect(prose).toContain('A skill moves from a personal plugin to a shared one by moving its folder.');
286
340
  expect(prose).toContain("A person's private skill goes in their personal plugin");
287
341
  expect(prose).toContain(
288
342
  `A person's personal plugin (\`${plugins}/personal-<id>/\`) grants its owner access and denies \`everyone\` outright`,
@@ -299,8 +353,8 @@ describe('what the guide tells an agent', () => {
299
353
  expect(prose).toContain('add people to existing roles, and use a GROUP for a task- or team-scoped set of people');
300
354
  });
301
355
 
302
- it('describes delete_branch where it says what an agent does with branches', async () => {
303
- const text = section(await composeAgentGuide(DEFAULT_KB_LAYOUT), '### An agent proposes and syncs; a person merges');
356
+ it('describes delete_branch where it says what happens to a draft', async () => {
357
+ const text = section(await composeAgentGuide(DEFAULT_KB_LAYOUT), '### When a draft goes away');
304
358
  const prose = text.replace(/\s+/g, ' ');
305
359
  expect(prose).toContain('`delete_branch`');
306
360
  expect(prose).toContain('Only its author');
@@ -367,6 +421,7 @@ describe('the HTML views section', () => {
367
421
  expect(text).toContain('**No host navigation.**');
368
422
  expect(text).toContain('`window.bevel.openNode(href)`');
369
423
  expect(text).toContain('`window.bevel.navigate(href)`');
424
+ expect(text).toContain('`window.bevel.branch` — the branch the page is viewed on');
370
425
  expect(text).toContain('A distribution may add data members of its own to `window.bevel`');
371
426
  expect(text).toContain('**A bare fragment scrolls the page.**');
372
427
  // Rendered for the deployment's layout like every other section.
@@ -374,6 +429,36 @@ describe('the HTML views section', () => {
374
429
  expect(docs).toContain('/workspace/main/Docs/Alice.md');
375
430
  });
376
431
 
432
+ // A dashboard that painted one "Loading…" line and then swapped in the whole
433
+ // board read as broken on a cold load (2026-10-20). These rules are what
434
+ // agents write pages from; the reference page under them is run by
435
+ // core-frontend's `liveDataReferencePage.test.ts`.
436
+ it('tells page authors how a page loads live data without changing shape', async () => {
437
+ const text = section(await composeAgentGuide(DEFAULT_KB_LAYOUT), '## HTML views').replace(/\s+/g, ' ');
438
+ expect(text).toContain('### Live data: a page that stays still while it loads');
439
+ expect(text).toContain('**Render the final page shell in the markup, before any data.**');
440
+ expect(text).toContain('The loading treatment — placeholder rows, `aria-busy="true"` — sits **inside** the data regions.');
441
+ expect(text).toContain('**Publish one consistent dataset at a time.**');
442
+ expect(text).toContain('Until then show no counts, no cards and no empty columns');
443
+ expect(text).toContain('**Refresh keeps the current board.**');
444
+ expect(text).toContain('**A failed read fails the whole load, visibly.**');
445
+ expect(text).toContain('offer a Retry button that runs the same load again');
446
+ expect(text).toContain('**Read in bounded parallel once the paths are known.**');
447
+ expect(text).toContain('**Load with `callTool`, not `callToolChain`.**');
448
+ expect(text).toContain("**The reads are the viewer's own.**");
449
+ expect(text).toContain('**Link to what the tools named by the path they answered.**');
450
+ expect(text).toContain('Never put the checkout folder in front of a path a tool answered: it is already there.');
451
+ expect(text).toContain('plain HTML and one inline script, no build step and no framework');
452
+ // The rule names the deployment's own checkout folder; the page links on its branch by the tool's path.
453
+ const docs = section(await composeAgentGuide(DEFAULT_KB_LAYOUT, undefined, { kbDirName: 'kb' }), '## HTML views');
454
+ expect(docs.replace(/\s+/g, ' ')).toContain('starts with the checkout folder, `kb/Data/Tickets/A.md`');
455
+ // Each segment of the tool's path encoded on its own, so a name carrying `#` stays a name.
456
+ expect(docs).toContain(
457
+ "link.href = '/workspace/' + encodeURIComponent(BRANCH) + '/' + ticket.path.split('/').map(encodeURIComponent).join('/');",
458
+ );
459
+ expect(docs).not.toContain('{{');
460
+ });
461
+
377
462
  it('documents window.bevel.tools: its four functions, the null check, the consent rule and requireTools with its card', async () => {
378
463
  const text = section(await composeAgentGuide(DEFAULT_KB_LAYOUT), '## HTML views').replace(/\s+/g, ' ');
379
464
  for (const member of [