@bevel-software/platform-core-backend 0.14.0 → 0.19.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 (1307) hide show
  1. package/THIRD-PARTY-NOTICES.md +8 -8
  2. package/dist/core/catalog-cache-invalidation.d.ts +45 -0
  3. package/dist/core/catalog-cache-invalidation.d.ts.map +1 -0
  4. package/dist/core/catalog-cache-invalidation.js +76 -0
  5. package/dist/core/catalog-cache-invalidation.js.map +1 -0
  6. package/dist/core/catalog-revision.d.ts +39 -0
  7. package/dist/core/catalog-revision.d.ts.map +1 -0
  8. package/dist/core/catalog-revision.js +94 -0
  9. package/dist/core/catalog-revision.js.map +1 -0
  10. package/dist/core/create-core-server.d.ts +7 -0
  11. package/dist/core/create-core-server.d.ts.map +1 -1
  12. package/dist/core/create-core-server.js +279 -64
  13. package/dist/core/create-core-server.js.map +1 -1
  14. package/dist/core/create-core-services.d.ts +66 -2
  15. package/dist/core/create-core-services.d.ts.map +1 -1
  16. package/dist/core/create-core-services.js +290 -111
  17. package/dist/core/create-core-services.js.map +1 -1
  18. package/dist/core/lifecycle.d.ts +138 -0
  19. package/dist/core/lifecycle.d.ts.map +1 -0
  20. package/dist/core/lifecycle.js +218 -0
  21. package/dist/core/lifecycle.js.map +1 -0
  22. package/dist/core/public-config.d.ts +82 -0
  23. package/dist/core/public-config.d.ts.map +1 -0
  24. package/dist/core/public-config.js +78 -0
  25. package/dist/core/public-config.js.map +1 -0
  26. package/dist/core/readiness.d.ts +86 -0
  27. package/dist/core/readiness.d.ts.map +1 -0
  28. package/dist/core/readiness.js +56 -0
  29. package/dist/core/readiness.js.map +1 -0
  30. package/dist/core-config.d.ts +30 -0
  31. package/dist/core-config.d.ts.map +1 -1
  32. package/dist/core-config.js +64 -2
  33. package/dist/core-config.js.map +1 -1
  34. package/dist/index.d.ts +4 -1
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +6 -0
  37. package/dist/index.js.map +1 -1
  38. package/dist/modules/access/access-control.interface.d.ts +231 -15
  39. package/dist/modules/access/access-control.interface.d.ts.map +1 -1
  40. package/dist/modules/access/access-control.service.d.ts +83 -12
  41. package/dist/modules/access/access-control.service.d.ts.map +1 -1
  42. package/dist/modules/access/access-control.service.js +815 -212
  43. package/dist/modules/access/access-control.service.js.map +1 -1
  44. package/dist/modules/access/access-mutation.service.d.ts +71 -5
  45. package/dist/modules/access/access-mutation.service.d.ts.map +1 -1
  46. package/dist/modules/access/access-mutation.service.js +161 -13
  47. package/dist/modules/access/access-mutation.service.js.map +1 -1
  48. package/dist/modules/access/access-view.d.ts +147 -0
  49. package/dist/modules/access/access-view.d.ts.map +1 -0
  50. package/dist/modules/access/access-view.js +211 -0
  51. package/dist/modules/access/access-view.js.map +1 -0
  52. package/dist/modules/access/access.routes.d.ts.map +1 -1
  53. package/dist/modules/access/access.routes.js +312 -84
  54. package/dist/modules/access/access.routes.js.map +1 -1
  55. package/dist/modules/access/admin-locked-commit.d.ts +2 -3
  56. package/dist/modules/access/admin-locked-commit.d.ts.map +1 -1
  57. package/dist/modules/access/admin-locked-commit.js +9 -8
  58. package/dist/modules/access/admin-locked-commit.js.map +1 -1
  59. package/dist/modules/access/admin-route-helpers.d.ts.map +1 -1
  60. package/dist/modules/access/admin-route-helpers.js +4 -4
  61. package/dist/modules/access/admin-route-helpers.js.map +1 -1
  62. package/dist/modules/access/change-read-gate.d.ts +53 -0
  63. package/dist/modules/access/change-read-gate.d.ts.map +1 -0
  64. package/dist/modules/access/change-read-gate.js +134 -0
  65. package/dist/modules/access/change-read-gate.js.map +1 -0
  66. package/dist/modules/access/creator-access.d.ts +33 -39
  67. package/dist/modules/access/creator-access.d.ts.map +1 -1
  68. package/dist/modules/access/creator-access.js +92 -119
  69. package/dist/modules/access/creator-access.js.map +1 -1
  70. package/dist/modules/access/directory-sync-bot.d.ts.map +1 -1
  71. package/dist/modules/access/directory-sync-bot.js +3 -1
  72. package/dist/modules/access/directory-sync-bot.js.map +1 -1
  73. package/dist/modules/access/groups-admin.service.d.ts +3 -3
  74. package/dist/modules/access/groups-admin.service.js +2 -2
  75. package/dist/modules/access/reference-scan.d.ts +1 -1
  76. package/dist/modules/access/reference-scan.d.ts.map +1 -1
  77. package/dist/modules/access/reference-scan.js +5 -4
  78. package/dist/modules/access/reference-scan.js.map +1 -1
  79. package/dist/modules/access/roles-admin.service.d.ts +72 -13
  80. package/dist/modules/access/roles-admin.service.d.ts.map +1 -1
  81. package/dist/modules/access/roles-admin.service.js +71 -13
  82. package/dist/modules/access/roles-admin.service.js.map +1 -1
  83. package/dist/modules/access/synced-groups-committer.d.ts.map +1 -1
  84. package/dist/modules/access/synced-groups-committer.js +8 -6
  85. package/dist/modules/access/synced-groups-committer.js.map +1 -1
  86. package/dist/modules/access/synced-groups-writer.d.ts.map +1 -1
  87. package/dist/modules/access/synced-groups-writer.js +3 -8
  88. package/dist/modules/access/synced-groups-writer.js.map +1 -1
  89. package/dist/modules/access/user-access-removal.service.d.ts +209 -0
  90. package/dist/modules/access/user-access-removal.service.d.ts.map +1 -0
  91. package/dist/modules/access/user-access-removal.service.js +662 -0
  92. package/dist/modules/access/user-access-removal.service.js.map +1 -0
  93. package/dist/modules/access-model/access-errors.d.ts +32 -1
  94. package/dist/modules/access-model/access-errors.d.ts.map +1 -1
  95. package/dist/modules/access-model/access-errors.js +38 -11
  96. package/dist/modules/access-model/access-errors.js.map +1 -1
  97. package/dist/modules/access-model/access-grammar.d.ts +100 -25
  98. package/dist/modules/access-model/access-grammar.d.ts.map +1 -1
  99. package/dist/modules/access-model/access-grammar.js +162 -48
  100. package/dist/modules/access-model/access-grammar.js.map +1 -1
  101. package/dist/modules/access-model/access-splice.d.ts.map +1 -1
  102. package/dist/modules/access-model/access-splice.js +18 -18
  103. package/dist/modules/access-model/access-splice.js.map +1 -1
  104. package/dist/modules/access-model/access-template.d.ts +22 -0
  105. package/dist/modules/access-model/access-template.d.ts.map +1 -0
  106. package/dist/modules/access-model/access-template.js +42 -0
  107. package/dist/modules/access-model/access-template.js.map +1 -0
  108. package/dist/modules/access-model/change-gate.d.ts +65 -0
  109. package/dist/modules/access-model/change-gate.d.ts.map +1 -0
  110. package/dist/modules/access-model/change-gate.js +25 -0
  111. package/dist/modules/access-model/change-gate.js.map +1 -0
  112. package/dist/modules/access-model/creator.d.ts +13 -16
  113. package/dist/modules/access-model/creator.d.ts.map +1 -1
  114. package/dist/modules/access-model/creator.js.map +1 -1
  115. package/dist/modules/access-model/frontmatter-lines.d.ts +27 -0
  116. package/dist/modules/access-model/frontmatter-lines.d.ts.map +1 -0
  117. package/dist/modules/access-model/frontmatter-lines.js +74 -0
  118. package/dist/modules/access-model/frontmatter-lines.js.map +1 -0
  119. package/dist/modules/access-model/group-files.d.ts.map +1 -1
  120. package/dist/modules/access-model/group-files.js +4 -1
  121. package/dist/modules/access-model/group-files.js.map +1 -1
  122. package/dist/modules/access-model/plugin-principals.d.ts +51 -0
  123. package/dist/modules/access-model/plugin-principals.d.ts.map +1 -0
  124. package/dist/modules/access-model/plugin-principals.js +119 -0
  125. package/dist/modules/access-model/plugin-principals.js.map +1 -0
  126. package/dist/modules/access-model/roles-yaml-guard.d.ts +73 -2
  127. package/dist/modules/access-model/roles-yaml-guard.d.ts.map +1 -1
  128. package/dist/modules/access-model/roles-yaml-guard.js +153 -3
  129. package/dist/modules/access-model/roles-yaml-guard.js.map +1 -1
  130. package/dist/modules/admin/admin-access.service.d.ts.map +1 -1
  131. package/dist/modules/admin/admin-access.service.js +5 -2
  132. package/dist/modules/admin/admin-access.service.js.map +1 -1
  133. package/dist/modules/agent-instructions/agent-instructions.routes.d.ts +21 -0
  134. package/dist/modules/agent-instructions/agent-instructions.routes.d.ts.map +1 -0
  135. package/dist/modules/agent-instructions/agent-instructions.routes.js +38 -0
  136. package/dist/modules/agent-instructions/agent-instructions.routes.js.map +1 -0
  137. package/dist/modules/agent-instructions/compose.d.ts +71 -0
  138. package/dist/modules/agent-instructions/compose.d.ts.map +1 -0
  139. package/dist/modules/agent-instructions/compose.js +234 -0
  140. package/dist/modules/agent-instructions/compose.js.map +1 -0
  141. package/dist/modules/agent-instructions/index.d.ts +4 -0
  142. package/dist/modules/agent-instructions/index.d.ts.map +1 -0
  143. package/dist/modules/agent-instructions/index.js +4 -0
  144. package/dist/modules/agent-instructions/index.js.map +1 -0
  145. package/dist/modules/agent-instructions/read-preamble.d.ts +37 -0
  146. package/dist/modules/agent-instructions/read-preamble.d.ts.map +1 -0
  147. package/dist/modules/agent-instructions/read-preamble.js +81 -0
  148. package/dist/modules/agent-instructions/read-preamble.js.map +1 -0
  149. package/dist/modules/auth/account-erasure.service.d.ts +30 -4
  150. package/dist/modules/auth/account-erasure.service.d.ts.map +1 -1
  151. package/dist/modules/auth/account-erasure.service.js +96 -10
  152. package/dist/modules/auth/account-erasure.service.js.map +1 -1
  153. package/dist/modules/auth/account.routes.d.ts +6 -3
  154. package/dist/modules/auth/account.routes.d.ts.map +1 -1
  155. package/dist/modules/auth/account.routes.js +109 -10
  156. package/dist/modules/auth/account.routes.js.map +1 -1
  157. package/dist/modules/auth/auth.routes.d.ts +10 -3
  158. package/dist/modules/auth/auth.routes.d.ts.map +1 -1
  159. package/dist/modules/auth/auth.routes.js +9 -4
  160. package/dist/modules/auth/auth.routes.js.map +1 -1
  161. package/dist/modules/auth/auth.service.d.ts +89 -5
  162. package/dist/modules/auth/auth.service.d.ts.map +1 -1
  163. package/dist/modules/auth/auth.service.js +160 -25
  164. package/dist/modules/auth/auth.service.js.map +1 -1
  165. package/dist/modules/auth/oidc-auth-provider.d.ts +46 -3
  166. package/dist/modules/auth/oidc-auth-provider.d.ts.map +1 -1
  167. package/dist/modules/auth/oidc-auth-provider.js +99 -21
  168. package/dist/modules/auth/oidc-auth-provider.js.map +1 -1
  169. package/dist/modules/code-mode/code-mode.tool.d.ts.map +1 -1
  170. package/dist/modules/code-mode/code-mode.tool.js +14 -2
  171. package/dist/modules/code-mode/code-mode.tool.js.map +1 -1
  172. package/dist/modules/connection-probe/connection-probe.service.d.ts.map +1 -1
  173. package/dist/modules/connection-probe/connection-probe.service.js +56 -7
  174. package/dist/modules/connection-probe/connection-probe.service.js.map +1 -1
  175. package/dist/modules/database/advisory-lock.d.ts +103 -0
  176. package/dist/modules/database/advisory-lock.d.ts.map +1 -0
  177. package/dist/modules/database/advisory-lock.js +320 -0
  178. package/dist/modules/database/advisory-lock.js.map +1 -0
  179. package/dist/modules/database/connection.d.ts.map +1 -1
  180. package/dist/modules/database/connection.js +9 -1
  181. package/dist/modules/database/connection.js.map +1 -1
  182. package/dist/modules/database/core-schema.d.ts +672 -0
  183. package/dist/modules/database/core-schema.d.ts.map +1 -1
  184. package/dist/modules/database/core-schema.js +171 -0
  185. package/dist/modules/database/core-schema.js.map +1 -1
  186. package/dist/modules/database/migrate.d.ts.map +1 -1
  187. package/dist/modules/database/migrate.js +26 -6
  188. package/dist/modules/database/migrate.js.map +1 -1
  189. package/dist/modules/declared-variables/declared-variables.routes.d.ts.map +1 -1
  190. package/dist/modules/declared-variables/declared-variables.routes.js +4 -2
  191. package/dist/modules/declared-variables/declared-variables.routes.js.map +1 -1
  192. package/dist/modules/diff/diff.routes.d.ts.map +1 -1
  193. package/dist/modules/diff/diff.routes.js +2 -4
  194. package/dist/modules/diff/diff.routes.js.map +1 -1
  195. package/dist/modules/diff/diff.service.d.ts +4 -1
  196. package/dist/modules/diff/diff.service.d.ts.map +1 -1
  197. package/dist/modules/diff/diff.service.js +79 -81
  198. package/dist/modules/diff/diff.service.js.map +1 -1
  199. package/dist/modules/kb-fs/bevel-ignore.d.ts +35 -0
  200. package/dist/modules/kb-fs/bevel-ignore.d.ts.map +1 -0
  201. package/dist/modules/kb-fs/bevel-ignore.js +71 -0
  202. package/dist/modules/kb-fs/bevel-ignore.js.map +1 -0
  203. package/dist/modules/kb-fs/file-change-notifier.d.ts.map +1 -1
  204. package/dist/modules/kb-fs/file-change-notifier.js +3 -1
  205. package/dist/modules/kb-fs/file-change-notifier.js.map +1 -1
  206. package/dist/modules/kb-fs/git-guarded-filesystem.d.ts +37 -0
  207. package/dist/modules/kb-fs/git-guarded-filesystem.d.ts.map +1 -0
  208. package/dist/modules/kb-fs/git-guarded-filesystem.js +104 -0
  209. package/dist/modules/kb-fs/git-guarded-filesystem.js.map +1 -0
  210. package/dist/modules/kb-fs/locking-filesystem.d.ts +113 -18
  211. package/dist/modules/kb-fs/locking-filesystem.d.ts.map +1 -1
  212. package/dist/modules/kb-fs/locking-filesystem.js +353 -76
  213. package/dist/modules/kb-fs/locking-filesystem.js.map +1 -1
  214. package/dist/modules/kb-fs/node-fs.d.ts +23 -0
  215. package/dist/modules/kb-fs/node-fs.d.ts.map +1 -0
  216. package/dist/modules/kb-fs/node-fs.js +158 -0
  217. package/dist/modules/kb-fs/node-fs.js.map +1 -0
  218. package/dist/modules/kb-fs/read-only-filesystem.d.ts +10 -9
  219. package/dist/modules/kb-fs/read-only-filesystem.d.ts.map +1 -1
  220. package/dist/modules/kb-fs/read-only-filesystem.js +18 -9
  221. package/dist/modules/kb-fs/read-only-filesystem.js.map +1 -1
  222. package/dist/modules/kb-fs/repo-path.d.ts +100 -3
  223. package/dist/modules/kb-fs/repo-path.d.ts.map +1 -1
  224. package/dist/modules/kb-fs/repo-path.js +195 -3
  225. package/dist/modules/kb-fs/repo-path.js.map +1 -1
  226. package/dist/modules/kb-sync/index.d.ts +6 -0
  227. package/dist/modules/kb-sync/index.d.ts.map +1 -0
  228. package/dist/modules/kb-sync/index.js +5 -0
  229. package/dist/modules/kb-sync/index.js.map +1 -0
  230. package/dist/modules/kb-sync/kb-sync.interface.d.ts +77 -0
  231. package/dist/modules/kb-sync/kb-sync.interface.d.ts.map +1 -0
  232. package/dist/modules/kb-sync/kb-sync.interface.js +2 -0
  233. package/dist/modules/kb-sync/kb-sync.interface.js.map +1 -0
  234. package/dist/modules/kb-sync/kb-sync.routes.d.ts +70 -0
  235. package/dist/modules/kb-sync/kb-sync.routes.d.ts.map +1 -0
  236. package/dist/modules/kb-sync/kb-sync.routes.js +137 -0
  237. package/dist/modules/kb-sync/kb-sync.routes.js.map +1 -0
  238. package/dist/modules/kb-sync/kb-sync.service.d.ts +45 -0
  239. package/dist/modules/kb-sync/kb-sync.service.d.ts.map +1 -0
  240. package/dist/modules/kb-sync/kb-sync.service.js +185 -0
  241. package/dist/modules/kb-sync/kb-sync.service.js.map +1 -0
  242. package/dist/modules/kb-sync/sync-auth.d.ts +59 -0
  243. package/dist/modules/kb-sync/sync-auth.d.ts.map +1 -0
  244. package/dist/modules/kb-sync/sync-auth.js +62 -0
  245. package/dist/modules/kb-sync/sync-auth.js.map +1 -0
  246. package/dist/modules/kb-sync/sync-payload.d.ts +32 -0
  247. package/dist/modules/kb-sync/sync-payload.d.ts.map +1 -0
  248. package/dist/modules/kb-sync/sync-payload.js +105 -0
  249. package/dist/modules/kb-sync/sync-payload.js.map +1 -0
  250. package/dist/modules/marketplace/git-http.routes.d.ts +42 -0
  251. package/dist/modules/marketplace/git-http.routes.d.ts.map +1 -0
  252. package/dist/modules/marketplace/git-http.routes.js +192 -0
  253. package/dist/modules/marketplace/git-http.routes.js.map +1 -0
  254. package/dist/modules/marketplace/github-facade/github-facade-admin.routes.d.ts +33 -0
  255. package/dist/modules/marketplace/github-facade/github-facade-admin.routes.d.ts.map +1 -0
  256. package/dist/modules/marketplace/github-facade/github-facade-admin.routes.js +88 -0
  257. package/dist/modules/marketplace/github-facade/github-facade-admin.routes.js.map +1 -0
  258. package/dist/modules/marketplace/github-facade/github-facade-codes.store.d.ts +59 -0
  259. package/dist/modules/marketplace/github-facade/github-facade-codes.store.d.ts.map +1 -0
  260. package/dist/modules/marketplace/github-facade/github-facade-codes.store.js +96 -0
  261. package/dist/modules/marketplace/github-facade/github-facade-codes.store.js.map +1 -0
  262. package/dist/modules/marketplace/github-facade/github-facade-credentials.service.d.ts +126 -0
  263. package/dist/modules/marketplace/github-facade/github-facade-credentials.service.d.ts.map +1 -0
  264. package/dist/modules/marketplace/github-facade/github-facade-credentials.service.js +192 -0
  265. package/dist/modules/marketplace/github-facade/github-facade-credentials.service.js.map +1 -0
  266. package/dist/modules/marketplace/github-facade/github-facade.routes.d.ts +47 -0
  267. package/dist/modules/marketplace/github-facade/github-facade.routes.d.ts.map +1 -0
  268. package/dist/modules/marketplace/github-facade/github-facade.routes.js +252 -0
  269. package/dist/modules/marketplace/github-facade/github-facade.routes.js.map +1 -0
  270. package/dist/modules/marketplace/github-facade/github-facade.service.d.ts +121 -0
  271. package/dist/modules/marketplace/github-facade/github-facade.service.d.ts.map +1 -0
  272. package/dist/modules/marketplace/github-facade/github-facade.service.js +213 -0
  273. package/dist/modules/marketplace/github-facade/github-facade.service.js.map +1 -0
  274. package/dist/modules/marketplace/github-facade/index.d.ts +6 -0
  275. package/dist/modules/marketplace/github-facade/index.d.ts.map +1 -0
  276. package/dist/modules/marketplace/github-facade/index.js +6 -0
  277. package/dist/modules/marketplace/github-facade/index.js.map +1 -0
  278. package/dist/modules/marketplace/index.d.ts +4 -0
  279. package/dist/modules/marketplace/index.d.ts.map +1 -0
  280. package/dist/modules/marketplace/index.js +4 -0
  281. package/dist/modules/marketplace/index.js.map +1 -0
  282. package/dist/modules/marketplace/marketplace-repo.service.d.ts +135 -0
  283. package/dist/modules/marketplace/marketplace-repo.service.d.ts.map +1 -0
  284. package/dist/modules/marketplace/marketplace-repo.service.js +318 -0
  285. package/dist/modules/marketplace/marketplace-repo.service.js.map +1 -0
  286. package/dist/modules/mcp/downstream-pool.d.ts +107 -0
  287. package/dist/modules/mcp/downstream-pool.d.ts.map +1 -0
  288. package/dist/modules/mcp/downstream-pool.js +221 -0
  289. package/dist/modules/mcp/downstream-pool.js.map +1 -0
  290. package/dist/modules/mcp/downstream-token-refresh.d.ts +77 -0
  291. package/dist/modules/mcp/downstream-token-refresh.d.ts.map +1 -0
  292. package/dist/modules/mcp/downstream-token-refresh.js +166 -0
  293. package/dist/modules/mcp/downstream-token-refresh.js.map +1 -0
  294. package/dist/modules/mcp/first-call-probe.cli-args.d.ts +78 -0
  295. package/dist/modules/mcp/first-call-probe.cli-args.d.ts.map +1 -0
  296. package/dist/modules/mcp/first-call-probe.cli-args.js +151 -0
  297. package/dist/modules/mcp/first-call-probe.cli-args.js.map +1 -0
  298. package/dist/modules/mcp/first-call-probe.cli.d.ts +2 -0
  299. package/dist/modules/mcp/first-call-probe.cli.d.ts.map +1 -0
  300. package/dist/modules/mcp/first-call-probe.cli.js +62 -0
  301. package/dist/modules/mcp/first-call-probe.cli.js.map +1 -0
  302. package/dist/modules/mcp/first-call-probe.d.ts +165 -0
  303. package/dist/modules/mcp/first-call-probe.d.ts.map +1 -0
  304. package/dist/modules/mcp/first-call-probe.js +298 -0
  305. package/dist/modules/mcp/first-call-probe.js.map +1 -0
  306. package/dist/modules/mcp/manual-failure-memo.d.ts +42 -17
  307. package/dist/modules/mcp/manual-failure-memo.d.ts.map +1 -1
  308. package/dist/modules/mcp/manual-failure-memo.js +79 -23
  309. package/dist/modules/mcp/manual-failure-memo.js.map +1 -1
  310. package/dist/modules/mcp/mcp-auth.middleware.d.ts +9 -5
  311. package/dist/modules/mcp/mcp-auth.middleware.d.ts.map +1 -1
  312. package/dist/modules/mcp/mcp-auth.middleware.js +24 -16
  313. package/dist/modules/mcp/mcp-auth.middleware.js.map +1 -1
  314. package/dist/modules/mcp/mcp.routes.d.ts +4 -4
  315. package/dist/modules/mcp/mcp.routes.d.ts.map +1 -1
  316. package/dist/modules/mcp/mcp.routes.js +83 -179
  317. package/dist/modules/mcp/mcp.routes.js.map +1 -1
  318. package/dist/modules/mcp/mcp.service.d.ts +218 -61
  319. package/dist/modules/mcp/mcp.service.d.ts.map +1 -1
  320. package/dist/modules/mcp/mcp.service.js +713 -159
  321. package/dist/modules/mcp/mcp.service.js.map +1 -1
  322. package/dist/modules/mcp/oauth/bevel-oauth-provider.d.ts.map +1 -1
  323. package/dist/modules/mcp/oauth/bevel-oauth-provider.js +8 -2
  324. package/dist/modules/mcp/oauth/bevel-oauth-provider.js.map +1 -1
  325. package/dist/modules/mcp/oauth/oauth-consent.routes.d.ts +14 -8
  326. package/dist/modules/mcp/oauth/oauth-consent.routes.d.ts.map +1 -1
  327. package/dist/modules/mcp/oauth/oauth-consent.routes.js +16 -2
  328. package/dist/modules/mcp/oauth/oauth-consent.routes.js.map +1 -1
  329. package/dist/modules/mcp/oauth/oauth-state.d.ts +13 -2
  330. package/dist/modules/mcp/oauth/oauth-state.d.ts.map +1 -1
  331. package/dist/modules/mcp/oauth/oauth-state.js +3 -1
  332. package/dist/modules/mcp/oauth/oauth-state.js.map +1 -1
  333. package/dist/modules/mcp/surface-log-throttle.d.ts +59 -0
  334. package/dist/modules/mcp/surface-log-throttle.d.ts.map +1 -0
  335. package/dist/modules/mcp/surface-log-throttle.js +94 -0
  336. package/dist/modules/mcp/surface-log-throttle.js.map +1 -0
  337. package/dist/modules/plugins/compile/compile-marketplace.d.ts +104 -0
  338. package/dist/modules/plugins/compile/compile-marketplace.d.ts.map +1 -0
  339. package/dist/modules/plugins/compile/compile-marketplace.js +323 -0
  340. package/dist/modules/plugins/compile/compile-marketplace.js.map +1 -0
  341. package/dist/modules/plugins/compile/marketplace-compiler.service.d.ts +73 -0
  342. package/dist/modules/plugins/compile/marketplace-compiler.service.d.ts.map +1 -0
  343. package/dist/modules/plugins/compile/marketplace-compiler.service.js +135 -0
  344. package/dist/modules/plugins/compile/marketplace-compiler.service.js.map +1 -0
  345. package/dist/modules/plugins/discovery/bundle-dialect/bundle.source.d.ts +37 -0
  346. package/dist/modules/plugins/discovery/bundle-dialect/bundle.source.d.ts.map +1 -0
  347. package/dist/modules/plugins/discovery/bundle-dialect/bundle.source.js +158 -0
  348. package/dist/modules/plugins/discovery/bundle-dialect/bundle.source.js.map +1 -0
  349. package/dist/modules/plugins/discovery/bundle-dialect/registry.d.ts +65 -0
  350. package/dist/modules/plugins/discovery/bundle-dialect/registry.d.ts.map +1 -0
  351. package/dist/modules/plugins/discovery/bundle-dialect/registry.js +204 -0
  352. package/dist/modules/plugins/discovery/bundle-dialect/registry.js.map +1 -0
  353. package/dist/modules/plugins/discovery/kb-plugin-source.d.ts +39 -0
  354. package/dist/modules/plugins/discovery/kb-plugin-source.d.ts.map +1 -0
  355. package/dist/modules/plugins/discovery/kb-plugin-source.js +110 -0
  356. package/dist/modules/plugins/discovery/kb-plugin-source.js.map +1 -0
  357. package/dist/modules/plugins/discovery/native.source.d.ts +16 -0
  358. package/dist/modules/plugins/discovery/native.source.d.ts.map +1 -0
  359. package/dist/modules/plugins/discovery/native.source.js +120 -0
  360. package/dist/modules/plugins/discovery/native.source.js.map +1 -0
  361. package/dist/modules/plugins/discovery/plugin-source.d.ts +110 -0
  362. package/dist/modules/plugins/discovery/plugin-source.d.ts.map +1 -0
  363. package/dist/modules/plugins/discovery/plugin-source.js +2 -0
  364. package/dist/modules/plugins/discovery/plugin-source.js.map +1 -0
  365. package/dist/modules/plugins/index.d.ts +15 -3
  366. package/dist/modules/plugins/index.d.ts.map +1 -1
  367. package/dist/modules/plugins/index.js +13 -2
  368. package/dist/modules/plugins/index.js.map +1 -1
  369. package/dist/modules/plugins/join-request-jobs.service.d.ts +250 -0
  370. package/dist/modules/plugins/join-request-jobs.service.d.ts.map +1 -0
  371. package/dist/modules/plugins/join-request-jobs.service.js +602 -0
  372. package/dist/modules/plugins/join-request-jobs.service.js.map +1 -0
  373. package/dist/modules/plugins/join-request-records.store.d.ts +161 -0
  374. package/dist/modules/plugins/join-request-records.store.d.ts.map +1 -0
  375. package/dist/modules/plugins/join-request-records.store.js +179 -0
  376. package/dist/modules/plugins/join-request-records.store.js.map +1 -0
  377. package/dist/modules/plugins/join-requests.service.d.ts.map +1 -1
  378. package/dist/modules/plugins/join-requests.service.js +4 -2
  379. package/dist/modules/plugins/join-requests.service.js.map +1 -1
  380. package/dist/modules/plugins/plugin-links.d.ts +69 -0
  381. package/dist/modules/plugins/plugin-links.d.ts.map +1 -0
  382. package/dist/modules/plugins/plugin-links.js +163 -0
  383. package/dist/modules/plugins/plugin-links.js.map +1 -0
  384. package/dist/modules/plugins/plugin-links.service.d.ts +111 -0
  385. package/dist/modules/plugins/plugin-links.service.d.ts.map +1 -0
  386. package/dist/modules/plugins/plugin-links.service.js +295 -0
  387. package/dist/modules/plugins/plugin-links.service.js.map +1 -0
  388. package/dist/modules/plugins/plugin-provision.service.d.ts +129 -21
  389. package/dist/modules/plugins/plugin-provision.service.d.ts.map +1 -1
  390. package/dist/modules/plugins/plugin-provision.service.js +320 -75
  391. package/dist/modules/plugins/plugin-provision.service.js.map +1 -1
  392. package/dist/modules/plugins/plugin-rename.service.d.ts +71 -0
  393. package/dist/modules/plugins/plugin-rename.service.d.ts.map +1 -0
  394. package/dist/modules/plugins/plugin-rename.service.js +254 -0
  395. package/dist/modules/plugins/plugin-rename.service.js.map +1 -0
  396. package/dist/modules/plugins/plugins.contract.d.ts +78 -4
  397. package/dist/modules/plugins/plugins.contract.d.ts.map +1 -1
  398. package/dist/modules/plugins/plugins.routes.d.ts +44 -5
  399. package/dist/modules/plugins/plugins.routes.d.ts.map +1 -1
  400. package/dist/modules/plugins/plugins.routes.js +297 -115
  401. package/dist/modules/plugins/plugins.routes.js.map +1 -1
  402. package/dist/modules/plugins/plugins.service.d.ts +82 -3
  403. package/dist/modules/plugins/plugins.service.d.ts.map +1 -1
  404. package/dist/modules/plugins/plugins.service.js +178 -48
  405. package/dist/modules/plugins/plugins.service.js.map +1 -1
  406. package/dist/modules/plugins/plugins.tools.d.ts +25 -0
  407. package/dist/modules/plugins/plugins.tools.d.ts.map +1 -0
  408. package/dist/modules/plugins/plugins.tools.js +82 -0
  409. package/dist/modules/plugins/plugins.tools.js.map +1 -0
  410. package/dist/modules/plugins/teams.routes.d.ts +42 -0
  411. package/dist/modules/plugins/teams.routes.d.ts.map +1 -0
  412. package/dist/modules/plugins/teams.routes.js +79 -0
  413. package/dist/modules/plugins/teams.routes.js.map +1 -0
  414. package/dist/modules/secrets-vault/db-secrets-vault.service.d.ts +40 -1
  415. package/dist/modules/secrets-vault/db-secrets-vault.service.d.ts.map +1 -1
  416. package/dist/modules/secrets-vault/db-secrets-vault.service.js +202 -37
  417. package/dist/modules/secrets-vault/db-secrets-vault.service.js.map +1 -1
  418. package/dist/modules/secrets-vault/index.d.ts +1 -1
  419. package/dist/modules/secrets-vault/index.d.ts.map +1 -1
  420. package/dist/modules/secrets-vault/index.js.map +1 -1
  421. package/dist/modules/secrets-vault/mcp-oauth-discovery.service.d.ts.map +1 -1
  422. package/dist/modules/secrets-vault/mcp-oauth-discovery.service.js +5 -3
  423. package/dist/modules/secrets-vault/mcp-oauth-discovery.service.js.map +1 -1
  424. package/dist/modules/secrets-vault/secrets-variable-loader.d.ts.map +1 -1
  425. package/dist/modules/secrets-vault/secrets-variable-loader.js +3 -1
  426. package/dist/modules/secrets-vault/secrets-variable-loader.js.map +1 -1
  427. package/dist/modules/secrets-vault/secrets-vault.contract.d.ts +78 -0
  428. package/dist/modules/secrets-vault/secrets-vault.contract.d.ts.map +1 -1
  429. package/dist/modules/secrets-vault/secrets-vault.contract.js +40 -0
  430. package/dist/modules/secrets-vault/secrets-vault.contract.js.map +1 -1
  431. package/dist/modules/secrets-vault/secrets-vault.routes.d.ts.map +1 -1
  432. package/dist/modules/secrets-vault/secrets-vault.routes.js +106 -52
  433. package/dist/modules/secrets-vault/secrets-vault.routes.js.map +1 -1
  434. package/dist/modules/settings/connection-check.d.ts +118 -0
  435. package/dist/modules/settings/connection-check.d.ts.map +1 -0
  436. package/dist/modules/settings/connection-check.js +287 -0
  437. package/dist/modules/settings/connection-check.js.map +1 -0
  438. package/dist/modules/settings/deployment-settings.service.d.ts +157 -2
  439. package/dist/modules/settings/deployment-settings.service.d.ts.map +1 -1
  440. package/dist/modules/settings/deployment-settings.service.js +412 -35
  441. package/dist/modules/settings/deployment-settings.service.js.map +1 -1
  442. package/dist/modules/settings/git-root-folders.d.ts +54 -0
  443. package/dist/modules/settings/git-root-folders.d.ts.map +1 -0
  444. package/dist/modules/settings/git-root-folders.js +130 -0
  445. package/dist/modules/settings/git-root-folders.js.map +1 -0
  446. package/dist/modules/settings/oidc-check.d.ts +85 -0
  447. package/dist/modules/settings/oidc-check.d.ts.map +1 -0
  448. package/dist/modules/settings/oidc-check.js +172 -0
  449. package/dist/modules/settings/oidc-check.js.map +1 -0
  450. package/dist/modules/settings/setup.routes.d.ts +57 -1
  451. package/dist/modules/settings/setup.routes.d.ts.map +1 -1
  452. package/dist/modules/settings/setup.routes.js +408 -108
  453. package/dist/modules/settings/setup.routes.js.map +1 -1
  454. package/dist/modules/skills/allowed-tools-check.d.ts +132 -0
  455. package/dist/modules/skills/allowed-tools-check.d.ts.map +1 -0
  456. package/dist/modules/skills/allowed-tools-check.js +260 -0
  457. package/dist/modules/skills/allowed-tools-check.js.map +1 -0
  458. package/dist/modules/skills/index.d.ts +4 -1
  459. package/dist/modules/skills/index.d.ts.map +1 -1
  460. package/dist/modules/skills/index.js +2 -0
  461. package/dist/modules/skills/index.js.map +1 -1
  462. package/dist/modules/skills/pending-skills.service.d.ts +6 -11
  463. package/dist/modules/skills/pending-skills.service.d.ts.map +1 -1
  464. package/dist/modules/skills/pending-skills.service.js +24 -71
  465. package/dist/modules/skills/pending-skills.service.js.map +1 -1
  466. package/dist/modules/skills/skill-access-requests.routes.d.ts +38 -0
  467. package/dist/modules/skills/skill-access-requests.routes.d.ts.map +1 -0
  468. package/dist/modules/skills/skill-access-requests.routes.js +171 -0
  469. package/dist/modules/skills/skill-access-requests.routes.js.map +1 -0
  470. package/dist/modules/skills/skills.contract.d.ts +67 -2
  471. package/dist/modules/skills/skills.contract.d.ts.map +1 -1
  472. package/dist/modules/skills/skills.routes.d.ts +35 -3
  473. package/dist/modules/skills/skills.routes.d.ts.map +1 -1
  474. package/dist/modules/skills/skills.routes.js +57 -4
  475. package/dist/modules/skills/skills.routes.js.map +1 -1
  476. package/dist/modules/skills/skills.service.d.ts +31 -3
  477. package/dist/modules/skills/skills.service.d.ts.map +1 -1
  478. package/dist/modules/skills/skills.service.js +236 -65
  479. package/dist/modules/skills/skills.service.js.map +1 -1
  480. package/dist/modules/skills/skills.tools.d.ts +4 -1
  481. package/dist/modules/skills/skills.tools.d.ts.map +1 -1
  482. package/dist/modules/skills/skills.tools.js +41 -6
  483. package/dist/modules/skills/skills.tools.js.map +1 -1
  484. package/dist/modules/tool-auth/connection-key-rejection.d.ts +18 -0
  485. package/dist/modules/tool-auth/connection-key-rejection.d.ts.map +1 -0
  486. package/dist/modules/tool-auth/connection-key-rejection.js +20 -0
  487. package/dist/modules/tool-auth/connection-key-rejection.js.map +1 -0
  488. package/dist/modules/tool-auth/connection-keys-admin.routes.d.ts +14 -0
  489. package/dist/modules/tool-auth/connection-keys-admin.routes.d.ts.map +1 -0
  490. package/dist/modules/tool-auth/connection-keys-admin.routes.js +67 -0
  491. package/dist/modules/tool-auth/connection-keys-admin.routes.js.map +1 -0
  492. package/dist/modules/tool-auth/external-api-key.interface.d.ts +66 -1
  493. package/dist/modules/tool-auth/external-api-key.interface.d.ts.map +1 -1
  494. package/dist/modules/tool-auth/external-api-key.interface.js +2 -1
  495. package/dist/modules/tool-auth/external-api-key.interface.js.map +1 -1
  496. package/dist/modules/tool-auth/external-api-key.service.d.ts +23 -4
  497. package/dist/modules/tool-auth/external-api-key.service.d.ts.map +1 -1
  498. package/dist/modules/tool-auth/external-api-key.service.js +79 -19
  499. package/dist/modules/tool-auth/external-api-key.service.js.map +1 -1
  500. package/dist/modules/tool-auth/internal-token.service.d.ts +5 -2
  501. package/dist/modules/tool-auth/internal-token.service.d.ts.map +1 -1
  502. package/dist/modules/tool-auth/internal-token.service.js +5 -2
  503. package/dist/modules/tool-auth/internal-token.service.js.map +1 -1
  504. package/dist/modules/tool-auth/key-or-session.middleware.d.ts +23 -0
  505. package/dist/modules/tool-auth/key-or-session.middleware.d.ts.map +1 -0
  506. package/dist/modules/tool-auth/key-or-session.middleware.js +39 -0
  507. package/dist/modules/tool-auth/key-or-session.middleware.js.map +1 -0
  508. package/dist/modules/tool-auth/tool-auth.middleware.d.ts +7 -1
  509. package/dist/modules/tool-auth/tool-auth.middleware.d.ts.map +1 -1
  510. package/dist/modules/tool-auth/tool-auth.middleware.js +8 -5
  511. package/dist/modules/tool-auth/tool-auth.middleware.js.map +1 -1
  512. package/dist/modules/tool-helpers/tool-context.d.ts +14 -0
  513. package/dist/modules/tool-helpers/tool-context.d.ts.map +1 -1
  514. package/dist/modules/tool-helpers/tool-context.js +28 -6
  515. package/dist/modules/tool-helpers/tool-context.js.map +1 -1
  516. package/dist/modules/tool-helpers/tool-handler.d.ts.map +1 -1
  517. package/dist/modules/tool-helpers/tool-handler.js +22 -5
  518. package/dist/modules/tool-helpers/tool-handler.js.map +1 -1
  519. package/dist/modules/tool-helpers/tool.contract.d.ts +6 -1
  520. package/dist/modules/tool-helpers/tool.contract.d.ts.map +1 -1
  521. package/dist/modules/tool-helpers/tool.contract.js +7 -1
  522. package/dist/modules/tool-helpers/tool.contract.js.map +1 -1
  523. package/dist/modules/tool-manuals/index.d.ts +2 -1
  524. package/dist/modules/tool-manuals/index.d.ts.map +1 -1
  525. package/dist/modules/tool-manuals/index.js +1 -0
  526. package/dist/modules/tool-manuals/index.js.map +1 -1
  527. package/dist/modules/tool-manuals/mcp-json-discovery.d.ts +36 -0
  528. package/dist/modules/tool-manuals/mcp-json-discovery.d.ts.map +1 -1
  529. package/dist/modules/tool-manuals/mcp-json-discovery.js +136 -96
  530. package/dist/modules/tool-manuals/mcp-json-discovery.js.map +1 -1
  531. package/dist/modules/tool-manuals/mcp-server-edit.service.d.ts +10 -2
  532. package/dist/modules/tool-manuals/mcp-server-edit.service.d.ts.map +1 -1
  533. package/dist/modules/tool-manuals/mcp-server-edit.service.js +12 -15
  534. package/dist/modules/tool-manuals/mcp-server-edit.service.js.map +1 -1
  535. package/dist/modules/tool-manuals/pending-tools.service.d.ts +50 -0
  536. package/dist/modules/tool-manuals/pending-tools.service.d.ts.map +1 -0
  537. package/dist/modules/tool-manuals/pending-tools.service.js +149 -0
  538. package/dist/modules/tool-manuals/pending-tools.service.js.map +1 -0
  539. package/dist/modules/tool-manuals/tool-delete.service.d.ts +135 -0
  540. package/dist/modules/tool-manuals/tool-delete.service.d.ts.map +1 -0
  541. package/dist/modules/tool-manuals/tool-delete.service.js +303 -0
  542. package/dist/modules/tool-manuals/tool-delete.service.js.map +1 -0
  543. package/dist/modules/tool-manuals/tool-manuals.contract.d.ts +161 -0
  544. package/dist/modules/tool-manuals/tool-manuals.contract.d.ts.map +1 -1
  545. package/dist/modules/tool-manuals/tool-manuals.contract.js +9 -0
  546. package/dist/modules/tool-manuals/tool-manuals.contract.js.map +1 -1
  547. package/dist/modules/tool-manuals/tool-manuals.routes.d.ts +19 -2
  548. package/dist/modules/tool-manuals/tool-manuals.routes.d.ts.map +1 -1
  549. package/dist/modules/tool-manuals/tool-manuals.routes.js +127 -57
  550. package/dist/modules/tool-manuals/tool-manuals.routes.js.map +1 -1
  551. package/dist/modules/tool-manuals/tool-manuals.service.d.ts +121 -2
  552. package/dist/modules/tool-manuals/tool-manuals.service.d.ts.map +1 -1
  553. package/dist/modules/tool-manuals/tool-manuals.service.js +415 -92
  554. package/dist/modules/tool-manuals/tool-manuals.service.js.map +1 -1
  555. package/dist/modules/tool-manuals/tool-manuals.tools.d.ts.map +1 -1
  556. package/dist/modules/tool-manuals/tool-manuals.tools.js +78 -6
  557. package/dist/modules/tool-manuals/tool-manuals.tools.js.map +1 -1
  558. package/dist/modules/workflow/agent-tools/workflow.tools.d.ts.map +1 -1
  559. package/dist/modules/workflow/agent-tools/workflow.tools.js +88 -54
  560. package/dist/modules/workflow/agent-tools/workflow.tools.js.map +1 -1
  561. package/dist/modules/workflow/event-bus.d.ts +10 -4
  562. package/dist/modules/workflow/event-bus.d.ts.map +1 -1
  563. package/dist/modules/workflow/event-bus.js +14 -11
  564. package/dist/modules/workflow/event-bus.js.map +1 -1
  565. package/dist/modules/workflow/events.routes.d.ts +17 -2
  566. package/dist/modules/workflow/events.routes.d.ts.map +1 -1
  567. package/dist/modules/workflow/events.routes.js +31 -12
  568. package/dist/modules/workflow/events.routes.js.map +1 -1
  569. package/dist/modules/workflow/file-lock.service.d.ts +22 -4
  570. package/dist/modules/workflow/file-lock.service.d.ts.map +1 -1
  571. package/dist/modules/workflow/file-lock.service.js +27 -4
  572. package/dist/modules/workflow/file-lock.service.js.map +1 -1
  573. package/dist/modules/workflow/git/change-request-link.d.ts +20 -0
  574. package/dist/modules/workflow/git/change-request-link.d.ts.map +1 -0
  575. package/dist/modules/workflow/git/change-request-link.js +34 -0
  576. package/dist/modules/workflow/git/change-request-link.js.map +1 -0
  577. package/dist/modules/workflow/git/git-version.d.ts.map +1 -1
  578. package/dist/modules/workflow/git/git-version.js +3 -1
  579. package/dist/modules/workflow/git/git-version.js.map +1 -1
  580. package/dist/modules/workflow/git/git.service.d.ts +303 -6
  581. package/dist/modules/workflow/git/git.service.d.ts.map +1 -1
  582. package/dist/modules/workflow/git/git.service.js +753 -170
  583. package/dist/modules/workflow/git/git.service.js.map +1 -1
  584. package/dist/modules/workflow/git/node-git-runner.d.ts +36 -0
  585. package/dist/modules/workflow/git/node-git-runner.d.ts.map +1 -0
  586. package/dist/modules/workflow/git/node-git-runner.js +273 -0
  587. package/dist/modules/workflow/git/node-git-runner.js.map +1 -0
  588. package/dist/modules/workflow/git/pull-request.service.d.ts +57 -2
  589. package/dist/modules/workflow/git/pull-request.service.d.ts.map +1 -1
  590. package/dist/modules/workflow/git/pull-request.service.js +165 -25
  591. package/dist/modules/workflow/git/pull-request.service.js.map +1 -1
  592. package/dist/modules/workflow/pending-commits.service.d.ts +11 -0
  593. package/dist/modules/workflow/pending-commits.service.d.ts.map +1 -1
  594. package/dist/modules/workflow/pending-commits.service.js +24 -4
  595. package/dist/modules/workflow/pending-commits.service.js.map +1 -1
  596. package/dist/modules/workflow/pending-commits.worker.d.ts +15 -8
  597. package/dist/modules/workflow/pending-commits.worker.d.ts.map +1 -1
  598. package/dist/modules/workflow/pending-commits.worker.js +107 -58
  599. package/dist/modules/workflow/pending-commits.worker.js.map +1 -1
  600. package/dist/modules/workflow/recovery-bot.d.ts.map +1 -1
  601. package/dist/modules/workflow/recovery-bot.js +3 -1
  602. package/dist/modules/workflow/recovery-bot.js.map +1 -1
  603. package/dist/modules/workflow/review-workflow/approval-lock.d.ts +60 -0
  604. package/dist/modules/workflow/review-workflow/approval-lock.d.ts.map +1 -0
  605. package/dist/modules/workflow/review-workflow/approval-lock.js +116 -0
  606. package/dist/modules/workflow/review-workflow/approval-lock.js.map +1 -0
  607. package/dist/modules/workflow/review-workflow/review-workflow.interface.d.ts +54 -7
  608. package/dist/modules/workflow/review-workflow/review-workflow.interface.d.ts.map +1 -1
  609. package/dist/modules/workflow/review-workflow/review-workflow.service.d.ts +64 -0
  610. package/dist/modules/workflow/review-workflow/review-workflow.service.d.ts.map +1 -1
  611. package/dist/modules/workflow/review-workflow/review-workflow.service.js +269 -90
  612. package/dist/modules/workflow/review-workflow/review-workflow.service.js.map +1 -1
  613. package/dist/modules/workflow/sanitize-error.d.ts +5 -2
  614. package/dist/modules/workflow/sanitize-error.d.ts.map +1 -1
  615. package/dist/modules/workflow/sanitize-error.js +8 -4
  616. package/dist/modules/workflow/sanitize-error.js.map +1 -1
  617. package/dist/modules/workflow/workflow.routes.d.ts +2 -0
  618. package/dist/modules/workflow/workflow.routes.d.ts.map +1 -1
  619. package/dist/modules/workflow/workflow.routes.js +259 -29
  620. package/dist/modules/workflow/workflow.routes.js.map +1 -1
  621. package/dist/modules/workflow/workflow.service.d.ts +217 -14
  622. package/dist/modules/workflow/workflow.service.d.ts.map +1 -1
  623. package/dist/modules/workflow/workflow.service.js +1142 -109
  624. package/dist/modules/workflow/workflow.service.js.map +1 -1
  625. package/dist/modules/workspace/empty-dirs.d.ts +15 -0
  626. package/dist/modules/workspace/empty-dirs.d.ts.map +1 -0
  627. package/dist/modules/workspace/empty-dirs.js +90 -0
  628. package/dist/modules/workspace/empty-dirs.js.map +1 -0
  629. package/dist/modules/workspace/file-readers/content-mode.d.ts +50 -0
  630. package/dist/modules/workspace/file-readers/content-mode.d.ts.map +1 -0
  631. package/dist/modules/workspace/file-readers/content-mode.js +55 -0
  632. package/dist/modules/workspace/file-readers/content-mode.js.map +1 -0
  633. package/dist/modules/workspace/file-readers/doc-extract.service.d.ts +4 -1
  634. package/dist/modules/workspace/file-readers/doc-extract.service.d.ts.map +1 -1
  635. package/dist/modules/workspace/file-readers/doc-extract.service.js +4 -1
  636. package/dist/modules/workspace/file-readers/doc-extract.service.js.map +1 -1
  637. package/dist/modules/workspace/file-readers/doc-extract.types.d.ts +5 -2
  638. package/dist/modules/workspace/file-readers/doc-extract.types.d.ts.map +1 -1
  639. package/dist/modules/workspace/file-readers/doc-extract.types.js +7 -4
  640. package/dist/modules/workspace/file-readers/doc-extract.types.js.map +1 -1
  641. package/dist/modules/workspace/file-readers/document-reader.d.ts +2 -0
  642. package/dist/modules/workspace/file-readers/document-reader.d.ts.map +1 -1
  643. package/dist/modules/workspace/file-readers/document-reader.js +5 -0
  644. package/dist/modules/workspace/file-readers/document-reader.js.map +1 -1
  645. package/dist/modules/workspace/file-readers/extract-docx.d.ts +6 -0
  646. package/dist/modules/workspace/file-readers/extract-docx.d.ts.map +1 -1
  647. package/dist/modules/workspace/file-readers/extract-docx.js +6 -0
  648. package/dist/modules/workspace/file-readers/extract-docx.js.map +1 -1
  649. package/dist/modules/workspace/file-readers/file-reader.d.ts +24 -5
  650. package/dist/modules/workspace/file-readers/file-reader.d.ts.map +1 -1
  651. package/dist/modules/workspace/file-readers/file-reader.js +11 -6
  652. package/dist/modules/workspace/file-readers/file-reader.js.map +1 -1
  653. package/dist/modules/workspace/file-readers/file-reader.registry.d.ts.map +1 -1
  654. package/dist/modules/workspace/file-readers/file-reader.registry.js +6 -1
  655. package/dist/modules/workspace/file-readers/file-reader.registry.js.map +1 -1
  656. package/dist/modules/workspace/file-readers/image-reader.d.ts +7 -5
  657. package/dist/modules/workspace/file-readers/image-reader.d.ts.map +1 -1
  658. package/dist/modules/workspace/file-readers/image-reader.js +9 -5
  659. package/dist/modules/workspace/file-readers/image-reader.js.map +1 -1
  660. package/dist/modules/workspace/file-readers/ooxml-text.d.ts +3 -0
  661. package/dist/modules/workspace/file-readers/ooxml-text.d.ts.map +1 -1
  662. package/dist/modules/workspace/file-readers/ooxml-text.js +39 -2
  663. package/dist/modules/workspace/file-readers/ooxml-text.js.map +1 -1
  664. package/dist/modules/workspace/file-readers/text-reader.d.ts +29 -1
  665. package/dist/modules/workspace/file-readers/text-reader.d.ts.map +1 -1
  666. package/dist/modules/workspace/file-readers/text-reader.js +57 -3
  667. package/dist/modules/workspace/file-readers/text-reader.js.map +1 -1
  668. package/dist/modules/workspace/git-internals.middleware.d.ts +28 -0
  669. package/dist/modules/workspace/git-internals.middleware.d.ts.map +1 -0
  670. package/dist/modules/workspace/git-internals.middleware.js +93 -0
  671. package/dist/modules/workspace/git-internals.middleware.js.map +1 -0
  672. package/dist/modules/workspace/not-found.d.ts +80 -0
  673. package/dist/modules/workspace/not-found.d.ts.map +1 -0
  674. package/dist/modules/workspace/not-found.js +112 -0
  675. package/dist/modules/workspace/not-found.js.map +1 -0
  676. package/dist/modules/workspace/session-ontology.gate.d.ts +1 -1
  677. package/dist/modules/workspace/session-ontology.gate.d.ts.map +1 -1
  678. package/dist/modules/workspace/session-ontology.gate.js +1 -1
  679. package/dist/modules/workspace/session-ontology.gate.js.map +1 -1
  680. package/dist/modules/workspace/startup/beside-checkout.d.ts +42 -0
  681. package/dist/modules/workspace/startup/beside-checkout.d.ts.map +1 -0
  682. package/dist/modules/workspace/startup/beside-checkout.js +110 -0
  683. package/dist/modules/workspace/startup/beside-checkout.js.map +1 -0
  684. package/dist/modules/workspace/startup/kb-git.d.ts +7 -12
  685. package/dist/modules/workspace/startup/kb-git.d.ts.map +1 -1
  686. package/dist/modules/workspace/startup/kb-git.js +34 -35
  687. package/dist/modules/workspace/startup/kb-git.js.map +1 -1
  688. package/dist/modules/workspace/startup/kb-startup-runner.d.ts +95 -0
  689. package/dist/modules/workspace/startup/kb-startup-runner.d.ts.map +1 -1
  690. package/dist/modules/workspace/startup/kb-startup-runner.js +257 -55
  691. package/dist/modules/workspace/startup/kb-startup-runner.js.map +1 -1
  692. package/dist/modules/workspace/startup/steps/groups-to-plugins.step.d.ts +45 -0
  693. package/dist/modules/workspace/startup/steps/groups-to-plugins.step.d.ts.map +1 -1
  694. package/dist/modules/workspace/startup/steps/groups-to-plugins.step.js +417 -385
  695. package/dist/modules/workspace/startup/steps/groups-to-plugins.step.js.map +1 -1
  696. package/dist/modules/workspace/startup/steps/personal-spaces.step.d.ts +48 -0
  697. package/dist/modules/workspace/startup/steps/personal-spaces.step.d.ts.map +1 -0
  698. package/dist/modules/workspace/startup/steps/personal-spaces.step.js +183 -0
  699. package/dist/modules/workspace/startup/steps/personal-spaces.step.js.map +1 -0
  700. package/dist/modules/workspace/startup/steps/plugin-display-names.step.d.ts +51 -0
  701. package/dist/modules/workspace/startup/steps/plugin-display-names.step.d.ts.map +1 -0
  702. package/dist/modules/workspace/startup/steps/plugin-display-names.step.js +158 -0
  703. package/dist/modules/workspace/startup/steps/plugin-display-names.step.js.map +1 -0
  704. package/dist/modules/workspace/startup/steps/plugin-layout.d.ts +64 -0
  705. package/dist/modules/workspace/startup/steps/plugin-layout.d.ts.map +1 -0
  706. package/dist/modules/workspace/startup/steps/plugin-layout.js +97 -0
  707. package/dist/modules/workspace/startup/steps/plugin-layout.js.map +1 -0
  708. package/dist/modules/workspace/startup/steps/plugin-manifests.step.d.ts +41 -0
  709. package/dist/modules/workspace/startup/steps/plugin-manifests.step.d.ts.map +1 -0
  710. package/dist/modules/workspace/startup/steps/plugin-manifests.step.js +85 -0
  711. package/dist/modules/workspace/startup/steps/plugin-manifests.step.js.map +1 -0
  712. package/dist/modules/workspace/startup/steps/roles-yaml.step.d.ts +3 -1
  713. package/dist/modules/workspace/startup/steps/roles-yaml.step.d.ts.map +1 -1
  714. package/dist/modules/workspace/startup/steps/roles-yaml.step.js +4 -12
  715. package/dist/modules/workspace/startup/steps/roles-yaml.step.js.map +1 -1
  716. package/dist/modules/workspace/startup/steps/seed-tree.d.ts +6 -1
  717. package/dist/modules/workspace/startup/steps/seed-tree.d.ts.map +1 -1
  718. package/dist/modules/workspace/startup/steps/seed-tree.js +174 -54
  719. package/dist/modules/workspace/startup/steps/seed-tree.js.map +1 -1
  720. package/dist/modules/workspace/startup/steps/template-files.step.d.ts +96 -24
  721. package/dist/modules/workspace/startup/steps/template-files.step.d.ts.map +1 -1
  722. package/dist/modules/workspace/startup/steps/template-files.step.js +521 -126
  723. package/dist/modules/workspace/startup/steps/template-files.step.js.map +1 -1
  724. package/dist/modules/workspace/startup/steps/template-source.d.ts +94 -0
  725. package/dist/modules/workspace/startup/steps/template-source.d.ts.map +1 -0
  726. package/dist/modules/workspace/startup/steps/template-source.js +160 -0
  727. package/dist/modules/workspace/startup/steps/template-source.js.map +1 -0
  728. package/dist/modules/workspace/workspace.routes.d.ts +14 -2
  729. package/dist/modules/workspace/workspace.routes.d.ts.map +1 -1
  730. package/dist/modules/workspace/workspace.routes.js +657 -246
  731. package/dist/modules/workspace/workspace.routes.js.map +1 -1
  732. package/dist/modules/workspace/workspace.service.d.ts +323 -12
  733. package/dist/modules/workspace/workspace.service.d.ts.map +1 -1
  734. package/dist/modules/workspace/workspace.service.js +1006 -257
  735. package/dist/modules/workspace/workspace.service.js.map +1 -1
  736. package/dist/modules/workspace/workspace.tools.d.ts +42 -1
  737. package/dist/modules/workspace/workspace.tools.d.ts.map +1 -1
  738. package/dist/modules/workspace/workspace.tools.js +1590 -146
  739. package/dist/modules/workspace/workspace.tools.js.map +1 -1
  740. package/dist/modules/workspace/write-denial.d.ts +76 -0
  741. package/dist/modules/workspace/write-denial.d.ts.map +1 -0
  742. package/dist/modules/workspace/write-denial.js +138 -0
  743. package/dist/modules/workspace/write-denial.js.map +1 -0
  744. package/dist/shared/canonical-file-identity.d.ts +64 -0
  745. package/dist/shared/canonical-file-identity.d.ts.map +1 -0
  746. package/dist/shared/canonical-file-identity.js +92 -0
  747. package/dist/shared/canonical-file-identity.js.map +1 -0
  748. package/dist/shared/domain-errors.d.ts +128 -0
  749. package/dist/shared/domain-errors.d.ts.map +1 -1
  750. package/dist/shared/domain-errors.js +166 -0
  751. package/dist/shared/domain-errors.js.map +1 -1
  752. package/dist/shared/email-identity.d.ts +32 -0
  753. package/dist/shared/email-identity.d.ts.map +1 -0
  754. package/dist/shared/email-identity.js +37 -0
  755. package/dist/shared/email-identity.js.map +1 -0
  756. package/dist/shared/fs-errors.d.ts +8 -0
  757. package/dist/shared/fs-errors.d.ts.map +1 -0
  758. package/dist/shared/fs-errors.js +11 -0
  759. package/dist/shared/fs-errors.js.map +1 -0
  760. package/dist/shared/fs-walk.d.ts +20 -6
  761. package/dist/shared/fs-walk.d.ts.map +1 -1
  762. package/dist/shared/fs-walk.js +35 -27
  763. package/dist/shared/fs-walk.js.map +1 -1
  764. package/dist/shared/fs.contract.d.ts +206 -0
  765. package/dist/shared/fs.contract.d.ts.map +1 -0
  766. package/dist/shared/fs.contract.js +79 -0
  767. package/dist/shared/fs.contract.js.map +1 -0
  768. package/dist/shared/git-failure.d.ts +80 -0
  769. package/dist/shared/git-failure.d.ts.map +1 -0
  770. package/dist/shared/git-failure.js +139 -0
  771. package/dist/shared/git-failure.js.map +1 -0
  772. package/dist/shared/git-internals.d.ts +17 -0
  773. package/dist/shared/git-internals.d.ts.map +1 -0
  774. package/dist/shared/git-internals.js +123 -0
  775. package/dist/shared/git-internals.js.map +1 -0
  776. package/dist/shared/git.contract.d.ts +157 -0
  777. package/dist/shared/git.contract.d.ts.map +1 -0
  778. package/dist/shared/git.contract.js +103 -0
  779. package/dist/shared/git.contract.js.map +1 -0
  780. package/dist/shared/http-errors.d.ts +33 -0
  781. package/dist/shared/http-errors.d.ts.map +1 -0
  782. package/dist/shared/http-errors.js +35 -0
  783. package/dist/shared/http-errors.js.map +1 -0
  784. package/dist/shared/kb-walk.d.ts +52 -0
  785. package/dist/shared/kb-walk.d.ts.map +1 -0
  786. package/dist/shared/kb-walk.js +76 -0
  787. package/dist/shared/kb-walk.js.map +1 -0
  788. package/dist/shared/logger.contract.d.ts +63 -0
  789. package/dist/shared/logger.contract.d.ts.map +1 -0
  790. package/dist/shared/logger.contract.js +126 -0
  791. package/dist/shared/logger.contract.js.map +1 -0
  792. package/dist/shared/logging.d.ts +31 -0
  793. package/dist/shared/logging.d.ts.map +1 -0
  794. package/dist/shared/logging.js +92 -0
  795. package/dist/shared/logging.js.map +1 -0
  796. package/dist/shared/path-containment.d.ts +21 -0
  797. package/dist/shared/path-containment.d.ts.map +1 -0
  798. package/dist/shared/path-containment.js +34 -0
  799. package/dist/shared/path-containment.js.map +1 -0
  800. package/dist/shared/path-order.d.ts +18 -0
  801. package/dist/shared/path-order.d.ts.map +1 -0
  802. package/dist/shared/path-order.js +35 -0
  803. package/dist/shared/path-order.js.map +1 -0
  804. package/dist/shared/pending-proposals.d.ts +71 -0
  805. package/dist/shared/pending-proposals.d.ts.map +1 -0
  806. package/dist/shared/pending-proposals.js +98 -0
  807. package/dist/shared/pending-proposals.js.map +1 -0
  808. package/dist/shared/printable.d.ts +22 -0
  809. package/dist/shared/printable.d.ts.map +1 -0
  810. package/dist/shared/printable.js +65 -0
  811. package/dist/shared/printable.js.map +1 -0
  812. package/dist/shared/redact-secret.d.ts +30 -0
  813. package/dist/shared/redact-secret.d.ts.map +1 -0
  814. package/dist/shared/redact-secret.js +114 -0
  815. package/dist/shared/redact-secret.js.map +1 -0
  816. package/dist/shared/rename-no-replace.d.ts +119 -0
  817. package/dist/shared/rename-no-replace.d.ts.map +1 -0
  818. package/dist/shared/rename-no-replace.js +323 -0
  819. package/dist/shared/rename-no-replace.js.map +1 -0
  820. package/dist/shared/ssrf.d.ts.map +1 -1
  821. package/dist/shared/ssrf.js +49 -18
  822. package/dist/shared/ssrf.js.map +1 -1
  823. package/dist/shared/ttl-cache.d.ts +28 -1
  824. package/dist/shared/ttl-cache.d.ts.map +1 -1
  825. package/dist/shared/ttl-cache.js +33 -1
  826. package/dist/shared/ttl-cache.js.map +1 -1
  827. package/dist/version.d.ts +10 -6
  828. package/dist/version.d.ts.map +1 -1
  829. package/dist/version.js +10 -6
  830. package/dist/version.js.map +1 -1
  831. package/kb-template/.bevelignore +4 -3
  832. package/kb-template/AGENTS.md +364 -54
  833. package/kb-template/KnowledgeBase/How to get started.md +1 -1
  834. package/kb-template/Skills/.gitkeep +0 -0
  835. package/kb-template/access.md +5 -3
  836. package/kb-template/mcp-description.md +27 -0
  837. package/migrations/0005_github_facade.sql +25 -0
  838. package/migrations/0006_api_tokens_revoked_by.sql +1 -0
  839. package/migrations/0007_github_facade_registered.sql +1 -0
  840. package/migrations/0008_change_request_apply_failure.sql +4 -0
  841. package/migrations/0009_change_request_apply_failure_kind.sql +13 -0
  842. package/migrations/0010_plugin_join_requests.sql +15 -0
  843. package/migrations/0011_plugin_join_request_claim.sql +11 -0
  844. package/migrations/meta/0005_snapshot.json +1717 -0
  845. package/migrations/meta/0006_snapshot.json +1723 -0
  846. package/migrations/meta/0007_snapshot.json +1729 -0
  847. package/migrations/meta/0008_snapshot.json +1753 -0
  848. package/migrations/meta/0009_snapshot.json +1759 -0
  849. package/migrations/meta/0010_snapshot.json +1872 -0
  850. package/migrations/meta/0011_snapshot.json +1884 -0
  851. package/migrations/meta/_journal.json +49 -0
  852. package/package.json +8 -7
  853. package/src/__tests__/core-config.domain.test.ts +67 -2
  854. package/src/__tests__/kb-layout-config.test.ts +295 -0
  855. package/src/__tests__/open-change-gate.ts +15 -0
  856. package/src/core/__tests__/catalog-cache-invalidation.test.ts +226 -0
  857. package/src/core/__tests__/catalog-revision.test.ts +190 -0
  858. package/src/core/__tests__/lifecycle.test.ts +369 -0
  859. package/src/core/__tests__/public-config.test.ts +30 -0
  860. package/src/core/__tests__/readiness.test.ts +109 -0
  861. package/src/core/catalog-cache-invalidation.ts +111 -0
  862. package/src/core/catalog-revision.ts +110 -0
  863. package/src/core/create-core-server.ts +358 -63
  864. package/src/core/create-core-services.ts +449 -107
  865. package/src/core/lifecycle.ts +312 -0
  866. package/src/core/public-config.ts +86 -0
  867. package/src/core/readiness.ts +123 -0
  868. package/src/core-config.ts +70 -4
  869. package/src/index.ts +15 -0
  870. package/src/modules/access/__tests__/access-control.admin-root-floor.test.ts +362 -0
  871. package/src/modules/access/__tests__/access-control.atref-batch.test.ts +2 -1
  872. package/src/modules/access/__tests__/access-control.atref-cache.test.ts +46 -22
  873. package/src/modules/access/__tests__/access-control.deny-dependents.test.ts +215 -0
  874. package/src/modules/access/__tests__/access-control.download-read.test.ts +268 -0
  875. package/src/modules/access/__tests__/access-control.everyone-read.test.ts +145 -0
  876. package/src/modules/access/__tests__/access-control.group-read.test.ts +163 -0
  877. package/src/modules/access/__tests__/access-control.invalidate-race.test.ts +88 -0
  878. package/src/modules/access/__tests__/access-control.platform-restore.test.ts +152 -0
  879. package/src/modules/access/__tests__/access-control.prospective.test.ts +189 -0
  880. package/src/modules/access/__tests__/access-control.read-batch.test.ts +2 -1
  881. package/src/modules/access/__tests__/access-control.service.test.ts +142 -81
  882. package/src/modules/access/__tests__/access-groups.test.ts +47 -14
  883. package/src/modules/access/__tests__/access-md-format.test.ts +2 -1
  884. package/src/modules/access/__tests__/access-mutation.service.test.ts +52 -4
  885. package/src/modules/access/__tests__/access-own-read-grant.test.ts +2 -0
  886. package/src/modules/access/__tests__/access-personal-plugin.test.ts +153 -0
  887. package/src/modules/access/__tests__/access-plugin-principals.test.ts +385 -0
  888. package/src/modules/access/__tests__/access-restrict-here.test.ts +450 -0
  889. package/src/modules/access/__tests__/access.routes.batch.test.ts +139 -0
  890. package/src/modules/access/__tests__/access.routes.folder-governs.test.ts +394 -0
  891. package/src/modules/access/__tests__/access.routes.group-grant.test.ts +6 -2
  892. package/src/modules/access/__tests__/access.routes.overrides.test.ts +3 -2
  893. package/src/modules/access/__tests__/access.routes.proposal-branch.test.ts +234 -0
  894. package/src/modules/access/__tests__/access.routes.prospective.test.ts +216 -0
  895. package/src/modules/access/__tests__/access.routes.revoke.test.ts +7 -2
  896. package/src/modules/access/__tests__/access.routes.unknown-email.test.ts +360 -0
  897. package/src/modules/access/__tests__/change-read-gate.test.ts +272 -0
  898. package/src/modules/access/__tests__/creator-access.test.ts +81 -83
  899. package/src/modules/access/__tests__/grant-sources.test.ts +3 -2
  900. package/src/modules/access/__tests__/groups-admin.service.test.ts +14 -5
  901. package/src/modules/access/__tests__/roles-admin.security.test.ts +196 -0
  902. package/src/modules/access/__tests__/roles-admin.service.test.ts +127 -2
  903. package/src/modules/access/__tests__/roles-capabilities.test.ts +5 -4
  904. package/src/modules/access/__tests__/roles.routes.test.ts +5 -2
  905. package/src/modules/access/__tests__/user-access-removal.service.test.ts +402 -0
  906. package/src/modules/access/__tests__/users-db-double.ts +63 -0
  907. package/src/modules/access/access-control.interface.ts +269 -18
  908. package/src/modules/access/access-control.service.ts +943 -226
  909. package/src/modules/access/access-mutation.service.ts +182 -13
  910. package/src/modules/access/access-view.ts +316 -0
  911. package/src/modules/access/access.routes.ts +385 -105
  912. package/src/modules/access/admin-locked-commit.ts +12 -14
  913. package/src/modules/access/admin-route-helpers.ts +4 -4
  914. package/src/modules/access/change-read-gate.ts +148 -0
  915. package/src/modules/access/creator-access.ts +89 -124
  916. package/src/modules/access/directory-sync-bot.ts +4 -1
  917. package/src/modules/access/groups-admin.service.ts +3 -3
  918. package/src/modules/access/reference-scan.ts +5 -5
  919. package/src/modules/access/roles-admin.service.ts +94 -15
  920. package/src/modules/access/synced-groups-committer.ts +10 -15
  921. package/src/modules/access/synced-groups-writer.ts +3 -12
  922. package/src/modules/access/user-access-removal.service.ts +711 -0
  923. package/src/modules/access-model/__tests__/access-grammar.test.ts +18 -0
  924. package/src/modules/access-model/__tests__/access-splice.test.ts +63 -1
  925. package/src/modules/access-model/__tests__/frontmatter-fences.test.ts +71 -0
  926. package/src/modules/access-model/__tests__/plugin-token-canonical.test.ts +40 -0
  927. package/src/modules/access-model/__tests__/roles-yaml-guard.test.ts +131 -0
  928. package/src/modules/access-model/access-errors.ts +56 -15
  929. package/src/modules/access-model/access-grammar.ts +198 -49
  930. package/src/modules/access-model/access-splice.ts +18 -18
  931. package/src/modules/access-model/access-template.ts +44 -0
  932. package/src/modules/access-model/change-gate.ts +80 -0
  933. package/src/modules/access-model/creator.ts +18 -21
  934. package/src/modules/access-model/frontmatter-lines.ts +98 -0
  935. package/src/modules/access-model/group-files.ts +4 -0
  936. package/src/modules/access-model/plugin-principals.ts +130 -0
  937. package/src/modules/access-model/roles-yaml-guard.ts +182 -4
  938. package/src/modules/admin/admin-access.service.ts +6 -2
  939. package/src/modules/agent-instructions/__tests__/agent-instructions.route.test.ts +100 -0
  940. package/src/modules/agent-instructions/__tests__/compose.test.ts +256 -0
  941. package/src/modules/agent-instructions/__tests__/read-preamble.test.ts +135 -0
  942. package/src/modules/agent-instructions/agent-instructions.routes.ts +41 -0
  943. package/src/modules/agent-instructions/compose.ts +271 -0
  944. package/src/modules/agent-instructions/index.ts +14 -0
  945. package/src/modules/agent-instructions/read-preamble.ts +88 -0
  946. package/src/modules/auth/__tests__/account-erasure.approval-gate.test.ts +209 -0
  947. package/src/modules/auth/__tests__/account.routes.test.ts +178 -2
  948. package/src/modules/auth/__tests__/auth.middleware.test.ts +78 -0
  949. package/src/modules/auth/__tests__/auth.service.test.ts +338 -5
  950. package/src/modules/auth/__tests__/oidc-auth-provider.test.ts +352 -12
  951. package/src/modules/auth/__tests__/onboarding-done.route.test.ts +3 -1
  952. package/src/modules/auth/account-erasure.service.ts +117 -10
  953. package/src/modules/auth/account.routes.ts +115 -14
  954. package/src/modules/auth/auth.routes.ts +20 -7
  955. package/src/modules/auth/auth.service.ts +187 -28
  956. package/src/modules/auth/oidc-auth-provider.ts +120 -22
  957. package/src/modules/code-mode/__tests__/code-mode.tool.test.ts +109 -1
  958. package/src/modules/code-mode/code-mode.tool.ts +13 -2
  959. package/src/modules/connection-probe/__tests__/connection-probe.service.test.ts +78 -0
  960. package/src/modules/connection-probe/connection-probe.service.ts +59 -7
  961. package/src/modules/database/__tests__/advisory-lease.test.ts +152 -0
  962. package/src/modules/database/__tests__/advisory-lock.test.ts +147 -0
  963. package/src/modules/database/advisory-lock.ts +346 -0
  964. package/src/modules/database/connection.ts +9 -1
  965. package/src/modules/database/core-schema.ts +177 -0
  966. package/src/modules/database/migrate.ts +27 -6
  967. package/src/modules/declared-variables/declared-variables.routes.ts +6 -3
  968. package/src/modules/diff/__tests__/diff.service.seed-atomicity.test.ts +3 -0
  969. package/src/modules/diff/__tests__/diff.service.test.ts +3 -0
  970. package/src/modules/diff/diff.routes.ts +2 -4
  971. package/src/modules/diff/diff.service.ts +75 -79
  972. package/src/modules/kb-fs/__tests__/locking-filesystem.test.ts +472 -9
  973. package/src/modules/kb-fs/__tests__/node-fs.test.ts +409 -0
  974. package/src/modules/kb-fs/__tests__/repo-path.test.ts +210 -1
  975. package/src/modules/{workspace → kb-fs}/bevel-ignore.ts +22 -15
  976. package/src/modules/kb-fs/file-change-notifier.ts +4 -1
  977. package/src/modules/kb-fs/git-guarded-filesystem.ts +123 -0
  978. package/src/modules/kb-fs/locking-filesystem.ts +399 -109
  979. package/src/modules/kb-fs/node-fs.ts +165 -0
  980. package/src/modules/kb-fs/read-only-filesystem.ts +18 -9
  981. package/src/modules/kb-fs/repo-path.ts +213 -3
  982. package/src/modules/kb-sync/__tests__/kb-sync.routes.test.ts +297 -0
  983. package/src/modules/kb-sync/__tests__/kb-sync.service.test.ts +295 -0
  984. package/src/modules/kb-sync/__tests__/sync-auth.test.ts +118 -0
  985. package/src/modules/kb-sync/__tests__/sync-payload.test.ts +148 -0
  986. package/src/modules/kb-sync/index.ts +19 -0
  987. package/src/modules/kb-sync/kb-sync.interface.ts +79 -0
  988. package/src/modules/kb-sync/kb-sync.routes.ts +200 -0
  989. package/src/modules/kb-sync/kb-sync.service.ts +204 -0
  990. package/src/modules/kb-sync/sync-auth.ts +118 -0
  991. package/src/modules/kb-sync/sync-payload.ts +143 -0
  992. package/src/modules/marketplace/__tests__/github-facade.test.ts +575 -0
  993. package/src/modules/marketplace/__tests__/marketplace-git.test.ts +180 -0
  994. package/src/modules/marketplace/__tests__/marketplace-repo.head.test.ts +47 -0
  995. package/src/modules/marketplace/git-http.routes.ts +196 -0
  996. package/src/modules/marketplace/github-facade/github-facade-admin.routes.ts +110 -0
  997. package/src/modules/marketplace/github-facade/github-facade-codes.store.ts +145 -0
  998. package/src/modules/marketplace/github-facade/github-facade-credentials.service.ts +263 -0
  999. package/src/modules/marketplace/github-facade/github-facade.routes.ts +294 -0
  1000. package/src/modules/marketplace/github-facade/github-facade.service.ts +282 -0
  1001. package/src/modules/marketplace/github-facade/index.ts +26 -0
  1002. package/src/modules/marketplace/index.ts +3 -0
  1003. package/src/modules/marketplace/marketplace-repo.service.ts +352 -0
  1004. package/src/modules/mcp/__tests__/downstream-pool.test.ts +278 -0
  1005. package/src/modules/mcp/__tests__/downstream-token-refresh.test.ts +178 -0
  1006. package/src/modules/mcp/__tests__/fake-downstream-mcp-server.ts +156 -0
  1007. package/src/modules/mcp/__tests__/first-call-probe.cli-args.test.ts +178 -0
  1008. package/src/modules/mcp/__tests__/first-call-probe.test.ts +414 -0
  1009. package/src/modules/mcp/__tests__/manual-failure-memo.test.ts +98 -6
  1010. package/src/modules/mcp/__tests__/mcp-auth.middleware.test.ts +8 -5
  1011. package/src/modules/mcp/__tests__/mcp-routes-harness.ts +1 -3
  1012. package/src/modules/mcp/__tests__/mcp.e2e.test.ts +1055 -59
  1013. package/src/modules/mcp/__tests__/mcp.routes.stateless.test.ts +182 -0
  1014. package/src/modules/mcp/__tests__/mcp.service.test.ts +274 -6
  1015. package/src/modules/mcp/__tests__/surface-log-throttle.test.ts +169 -0
  1016. package/src/modules/mcp/downstream-pool.ts +256 -0
  1017. package/src/modules/mcp/downstream-token-refresh.ts +170 -0
  1018. package/src/modules/mcp/first-call-probe.cli-args.ts +159 -0
  1019. package/src/modules/mcp/first-call-probe.cli.ts +60 -0
  1020. package/src/modules/mcp/first-call-probe.ts +418 -0
  1021. package/src/modules/mcp/manual-failure-memo.ts +81 -23
  1022. package/src/modules/mcp/mcp-auth.middleware.ts +25 -16
  1023. package/src/modules/mcp/mcp.routes.ts +376 -482
  1024. package/src/modules/mcp/mcp.service.ts +846 -176
  1025. package/src/modules/mcp/oauth/bevel-oauth-provider.ts +8 -2
  1026. package/src/modules/mcp/oauth/oauth-consent.routes.ts +28 -6
  1027. package/src/modules/mcp/oauth/oauth-state.ts +15 -3
  1028. package/src/modules/mcp/surface-log-throttle.ts +120 -0
  1029. package/src/modules/plugins/__tests__/bundle-dialect.e2e.test.ts +166 -0
  1030. package/src/modules/plugins/__tests__/bundle-dialect.test.ts +165 -0
  1031. package/src/modules/plugins/__tests__/compile-marketplace.test.ts +423 -0
  1032. package/src/modules/plugins/__tests__/fake-join-request-store.ts +189 -0
  1033. package/src/modules/plugins/__tests__/join-request-jobs.service.test.ts +608 -0
  1034. package/src/modules/plugins/__tests__/plugin-creation-display-name.test.ts +241 -0
  1035. package/src/modules/plugins/__tests__/plugin-index.service.test.ts +201 -16
  1036. package/src/modules/plugins/__tests__/plugin-links.service.test.ts +284 -0
  1037. package/src/modules/plugins/__tests__/plugin-names.test.ts +77 -0
  1038. package/src/modules/plugins/__tests__/plugin-provision.service.test.ts +330 -20
  1039. package/src/modules/plugins/__tests__/plugin-rename.service.test.ts +456 -0
  1040. package/src/modules/plugins/__tests__/plugins.routes.test.ts +441 -43
  1041. package/src/modules/plugins/__tests__/plugins.tools.test.ts +131 -0
  1042. package/src/modules/plugins/__tests__/teams.routes.test.ts +248 -0
  1043. package/src/modules/plugins/compile/compile-marketplace.ts +500 -0
  1044. package/src/modules/plugins/compile/marketplace-compiler.service.ts +151 -0
  1045. package/src/modules/plugins/discovery/__tests__/kb-plugin-source.test.ts +365 -0
  1046. package/src/modules/plugins/discovery/bundle-dialect/bundle.source.ts +161 -0
  1047. package/src/modules/plugins/discovery/bundle-dialect/registry.ts +250 -0
  1048. package/src/modules/plugins/discovery/kb-plugin-source.ts +117 -0
  1049. package/src/modules/plugins/discovery/native.source.ts +140 -0
  1050. package/src/modules/plugins/discovery/plugin-source.ts +113 -0
  1051. package/src/modules/plugins/index.ts +25 -2
  1052. package/src/modules/plugins/join-request-jobs.service.ts +675 -0
  1053. package/src/modules/plugins/join-request-records.store.ts +353 -0
  1054. package/src/modules/plugins/join-requests.service.ts +7 -4
  1055. package/src/modules/plugins/plugin-links.service.ts +350 -0
  1056. package/src/modules/plugins/plugin-links.ts +187 -0
  1057. package/src/modules/plugins/plugin-provision.service.ts +356 -78
  1058. package/src/modules/plugins/plugin-rename.service.ts +310 -0
  1059. package/src/modules/plugins/plugins.contract.ts +79 -4
  1060. package/src/modules/plugins/plugins.routes.ts +320 -123
  1061. package/src/modules/plugins/plugins.service.ts +184 -57
  1062. package/src/modules/plugins/plugins.tools.ts +88 -0
  1063. package/src/modules/plugins/teams.routes.ts +129 -0
  1064. package/src/modules/secrets-vault/__tests__/connect-pending.route.test.ts +359 -15
  1065. package/src/modules/secrets-vault/__tests__/db-secrets-vault.oauth.test.ts +248 -0
  1066. package/src/modules/secrets-vault/__tests__/namespace-secrets.test.ts +202 -0
  1067. package/src/modules/secrets-vault/db-secrets-vault.service.ts +217 -35
  1068. package/src/modules/secrets-vault/index.ts +1 -0
  1069. package/src/modules/secrets-vault/mcp-oauth-discovery.service.ts +6 -3
  1070. package/src/modules/secrets-vault/secrets-variable-loader.ts +4 -4
  1071. package/src/modules/secrets-vault/secrets-vault.contract.ts +93 -0
  1072. package/src/modules/secrets-vault/secrets-vault.routes.ts +99 -38
  1073. package/src/modules/settings/__tests__/connection-check.test.ts +233 -0
  1074. package/src/modules/settings/__tests__/deployment-settings.service.test.ts +240 -1
  1075. package/src/modules/settings/__tests__/git-root-folders.test.ts +247 -0
  1076. package/src/modules/settings/__tests__/oidc-check.test.ts +185 -0
  1077. package/src/modules/settings/__tests__/setup.routes.boot-failure.test.ts +119 -0
  1078. package/src/modules/settings/__tests__/setup.routes.layout-phase.test.ts +170 -0
  1079. package/src/modules/settings/__tests__/setup.routes.oidc.test.ts +555 -0
  1080. package/src/modules/settings/__tests__/setup.routes.sync-status.test.ts +77 -0
  1081. package/src/modules/settings/__tests__/setup.routes.test.ts +667 -16
  1082. package/src/modules/settings/connection-check.ts +351 -0
  1083. package/src/modules/settings/deployment-settings.service.ts +478 -39
  1084. package/src/modules/settings/git-root-folders.ts +171 -0
  1085. package/src/modules/settings/oidc-check.ts +229 -0
  1086. package/src/modules/settings/setup.routes.ts +480 -110
  1087. package/src/modules/skills/__tests__/allowed-tools-check.test.ts +224 -0
  1088. package/src/modules/skills/__tests__/allowed-tools-warn.tools.test.ts +210 -0
  1089. package/src/modules/skills/__tests__/pending-skills.service.test.ts +3 -3
  1090. package/src/modules/skills/__tests__/skill-access-requests.routes.test.ts +128 -0
  1091. package/src/modules/skills/__tests__/skills.routes.warnings.test.ts +91 -0
  1092. package/src/modules/skills/__tests__/skills.service.test.ts +308 -2
  1093. package/src/modules/skills/allowed-tools-check.ts +352 -0
  1094. package/src/modules/skills/index.ts +5 -0
  1095. package/src/modules/skills/pending-skills.service.ts +26 -85
  1096. package/src/modules/skills/skill-access-requests.routes.ts +203 -0
  1097. package/src/modules/skills/skills.contract.ts +67 -3
  1098. package/src/modules/skills/skills.routes.ts +75 -4
  1099. package/src/modules/skills/skills.service.ts +258 -66
  1100. package/src/modules/skills/skills.tools.ts +43 -5
  1101. package/src/modules/tool-auth/__tests__/connection-key-rejection.test.ts +192 -0
  1102. package/src/modules/tool-auth/__tests__/connection-keys-admin.routes.test.ts +116 -0
  1103. package/src/modules/tool-auth/__tests__/external-api-key.service.test.ts +124 -0
  1104. package/src/modules/tool-auth/connection-key-rejection.ts +25 -0
  1105. package/src/modules/tool-auth/connection-keys-admin.routes.ts +76 -0
  1106. package/src/modules/tool-auth/external-api-key.interface.ts +73 -1
  1107. package/src/modules/tool-auth/external-api-key.service.ts +95 -29
  1108. package/src/modules/tool-auth/internal-token.service.ts +5 -2
  1109. package/src/modules/tool-auth/key-or-session.middleware.ts +45 -0
  1110. package/src/modules/tool-auth/tool-auth.middleware.ts +16 -7
  1111. package/src/modules/tool-helpers/__tests__/agent-roles-write.test.ts +228 -0
  1112. package/src/modules/tool-helpers/__tests__/phase4-tools.test.ts +33 -4
  1113. package/src/modules/tool-helpers/tool-context.ts +46 -7
  1114. package/src/modules/tool-helpers/tool-handler.ts +23 -5
  1115. package/src/modules/tool-helpers/tool.contract.ts +5 -0
  1116. package/src/modules/tool-manuals/__tests__/mcp-json-discovery.test.ts +24 -1
  1117. package/src/modules/tool-manuals/__tests__/mcp-server-edit.service.test.ts +2 -1
  1118. package/src/modules/tool-manuals/__tests__/pending-tools.service.test.ts +411 -0
  1119. package/src/modules/tool-manuals/__tests__/tool-delete.route.test.ts +133 -0
  1120. package/src/modules/tool-manuals/__tests__/tool-delete.service.test.ts +469 -0
  1121. package/src/modules/tool-manuals/__tests__/tool-manuals.archive.route.test.ts +21 -2
  1122. package/src/modules/tool-manuals/__tests__/tool-manuals.cli.test.ts +11 -4
  1123. package/src/modules/tool-manuals/__tests__/tool-manuals.detail.route.test.ts +6 -1
  1124. package/src/modules/tool-manuals/__tests__/tool-manuals.invalid.routes.test.ts +254 -0
  1125. package/src/modules/tool-manuals/__tests__/tool-manuals.invalid.test.ts +577 -0
  1126. package/src/modules/tool-manuals/__tests__/tool-manuals.mcp-oauth.test.ts +6 -1
  1127. package/src/modules/tool-manuals/__tests__/tool-manuals.pending.route.test.ts +102 -0
  1128. package/src/modules/tool-manuals/__tests__/tool-manuals.restart.test.ts +285 -0
  1129. package/src/modules/tool-manuals/__tests__/tool-manuals.service.test.ts +178 -2
  1130. package/src/modules/tool-manuals/__tests__/tool-manuals.tools.test.ts +114 -2
  1131. package/src/modules/tool-manuals/index.ts +3 -0
  1132. package/src/modules/tool-manuals/mcp-json-discovery.ts +164 -95
  1133. package/src/modules/tool-manuals/mcp-server-edit.service.ts +11 -14
  1134. package/src/modules/tool-manuals/pending-tools.service.ts +179 -0
  1135. package/src/modules/tool-manuals/tool-delete.service.ts +401 -0
  1136. package/src/modules/tool-manuals/tool-manuals.contract.ts +165 -0
  1137. package/src/modules/tool-manuals/tool-manuals.routes.ts +138 -54
  1138. package/src/modules/tool-manuals/tool-manuals.service.ts +440 -104
  1139. package/src/modules/tool-manuals/tool-manuals.tools.ts +81 -6
  1140. package/src/modules/workflow/__tests__/apply-failure.test.ts +1038 -0
  1141. package/src/modules/workflow/__tests__/event-bus.test.ts +61 -9
  1142. package/src/modules/workflow/__tests__/fake-file-lock-db.ts +199 -0
  1143. package/src/modules/workflow/__tests__/file-lock.service.path-identity.test.ts +237 -0
  1144. package/src/modules/workflow/__tests__/format-affected-owners.test.ts +18 -0
  1145. package/src/modules/workflow/__tests__/merge-announces-tree.test.ts +220 -0
  1146. package/src/modules/workflow/__tests__/pending-commits.worker.concurrency.test.ts +187 -0
  1147. package/src/modules/workflow/__tests__/preserve-roles-yaml.test.ts +3 -1
  1148. package/src/modules/workflow/__tests__/sanitize-error.test.ts +10 -0
  1149. package/src/modules/workflow/__tests__/workflow.routes.folder-proposals.test.ts +128 -0
  1150. package/src/modules/workflow/__tests__/workflow.routes.fork-point-file.test.ts +149 -0
  1151. package/src/modules/workflow/__tests__/workflow.routes.lock-path.test.ts +305 -0
  1152. package/src/modules/workflow/__tests__/workflow.service.branch-in-use.test.ts +2 -0
  1153. package/src/modules/workflow/__tests__/workflow.service.commitFileWhileLocked.test.ts +3 -0
  1154. package/src/modules/workflow/__tests__/workflow.service.deleted-branch-sweep.test.ts +2 -0
  1155. package/src/modules/workflow/__tests__/workflow.service.facade.test.ts +296 -27
  1156. package/src/modules/workflow/__tests__/workflow.service.folder-proposals.test.ts +564 -0
  1157. package/src/modules/workflow/__tests__/workflow.service.platform-restore.test.ts +135 -0
  1158. package/src/modules/workflow/__tests__/workflow.service.read-gate.test.ts +172 -0
  1159. package/src/modules/workflow/__tests__/workflow.service.releaseLock.test.ts +133 -2
  1160. package/src/modules/workflow/__tests__/workflow.service.sync.test.ts +442 -0
  1161. package/src/modules/workflow/__tests__/workflow.service.update-from-target.test.ts +281 -0
  1162. package/src/modules/workflow/agent-tools/__tests__/workflow.tools.test.ts +158 -7
  1163. package/src/modules/workflow/agent-tools/workflow.tools.ts +93 -64
  1164. package/src/modules/workflow/event-bus.ts +26 -28
  1165. package/src/modules/workflow/events.routes.ts +56 -21
  1166. package/src/modules/workflow/file-lock.service.ts +27 -4
  1167. package/src/modules/workflow/git/__tests__/change-request-link.test.ts +43 -0
  1168. package/src/modules/workflow/git/__tests__/git.service.accessGating.test.ts +1 -0
  1169. package/src/modules/workflow/git/__tests__/git.service.changedFilesForPr.test.ts +238 -1
  1170. package/src/modules/workflow/git/__tests__/git.service.forkPoint.test.ts +245 -0
  1171. package/src/modules/workflow/git/__tests__/git.service.list-branches.test.ts +35 -0
  1172. package/src/modules/workflow/git/__tests__/git.service.mergeChangeRequest.test.ts +149 -0
  1173. package/src/modules/workflow/git/__tests__/git.service.postMergePull.test.ts +3 -3
  1174. package/src/modules/workflow/git/__tests__/git.service.pull.test.ts +47 -4
  1175. package/src/modules/workflow/git/__tests__/git.service.revertPathsAndPush.test.ts +177 -0
  1176. package/src/modules/workflow/git/__tests__/git.service.syncFromRemote.test.ts +253 -0
  1177. package/src/modules/workflow/git/__tests__/node-git-runner.test.ts +114 -0
  1178. package/src/modules/workflow/git/__tests__/pull-request.service.getPrDetail.test.ts +69 -2
  1179. package/src/modules/workflow/git/__tests__/pull-request.service.list-fetch.test.ts +121 -0
  1180. package/src/modules/workflow/git/__tests__/pull-request.service.placeholder.test.ts +79 -0
  1181. package/src/modules/workflow/git/__tests__/pull-request.service.test.ts +1 -1
  1182. package/src/modules/workflow/git/__tests__/pull-request.service.viewer-can-cancel.test.ts +1 -1
  1183. package/src/modules/workflow/git/__tests__/pull-request.service.viewer-can-update.test.ts +111 -0
  1184. package/src/modules/workflow/git/change-request-link.ts +35 -0
  1185. package/src/modules/workflow/git/git-version.ts +5 -2
  1186. package/src/modules/workflow/git/git.service.ts +922 -199
  1187. package/src/modules/workflow/git/node-git-runner.ts +315 -0
  1188. package/src/modules/workflow/git/pull-request.service.ts +188 -27
  1189. package/src/modules/workflow/pending-commits.service.ts +26 -7
  1190. package/src/modules/workflow/pending-commits.worker.ts +114 -69
  1191. package/src/modules/workflow/recovery-bot.ts +4 -1
  1192. package/src/modules/workflow/review-workflow/__tests__/approval-states.test.ts +180 -1
  1193. package/src/modules/workflow/review-workflow/__tests__/cancel-pr.test.ts +1 -1
  1194. package/src/modules/workflow/review-workflow/__tests__/carry-approvals-forward.test.ts +520 -0
  1195. package/src/modules/workflow/review-workflow/__tests__/erase-approver.test.ts +84 -0
  1196. package/src/modules/workflow/review-workflow/__tests__/merge-gate.test.ts +89 -77
  1197. package/src/modules/workflow/review-workflow/approval-lock.ts +122 -0
  1198. package/src/modules/workflow/review-workflow/review-workflow.interface.ts +59 -7
  1199. package/src/modules/workflow/review-workflow/review-workflow.service.ts +309 -105
  1200. package/src/modules/workflow/sanitize-error.ts +9 -4
  1201. package/src/modules/workflow/workflow.routes.ts +273 -29
  1202. package/src/modules/workflow/workflow.service.ts +1341 -174
  1203. package/src/modules/workspace/__tests__/empty-dirs.test.ts +44 -0
  1204. package/src/modules/workspace/__tests__/file-stat-access.test.ts +301 -0
  1205. package/src/modules/workspace/__tests__/git-internals.security.test.ts +792 -0
  1206. package/src/modules/workspace/__tests__/workspace-paths-inside-the-repo.test.ts +464 -0
  1207. package/src/modules/workspace/__tests__/workspace.routes.branch-errors.test.ts +139 -0
  1208. package/src/modules/workspace/__tests__/workspace.routes.create-grant.test.ts +189 -37
  1209. package/src/modules/workspace/__tests__/workspace.routes.delete.test.ts +51 -11
  1210. package/src/modules/workspace/__tests__/workspace.routes.download-implies-read.test.ts +161 -0
  1211. package/src/modules/workspace/__tests__/workspace.routes.download.test.ts +109 -10
  1212. package/src/modules/workspace/__tests__/workspace.routes.file-writes.test.ts +624 -0
  1213. package/src/modules/workspace/__tests__/workspace.routes.folders.test.ts +409 -0
  1214. package/src/modules/workspace/__tests__/workspace.routes.move-platform-files.test.ts +327 -0
  1215. package/src/modules/workspace/__tests__/workspace.routes.read-gate.test.ts +31 -13
  1216. package/src/modules/workspace/__tests__/workspace.service.ensure-remotes-fetched.test.ts +109 -0
  1217. package/src/modules/workspace/__tests__/workspace.service.list-cloned.test.ts +94 -0
  1218. package/src/modules/workspace/__tests__/workspace.service.missing-branch.test.ts +28 -0
  1219. package/src/modules/workspace/__tests__/workspace.service.move-never-overwrites.test.ts +218 -0
  1220. package/src/modules/workspace/__tests__/workspace.service.read-filter.test.ts +98 -13
  1221. package/src/modules/workspace/__tests__/workspace.service.test.ts +811 -42
  1222. package/src/modules/workspace/__tests__/workspace.service.unknown-branch.test.ts +197 -0
  1223. package/src/modules/workspace/__tests__/workspace.tools.agents-file.test.ts +128 -0
  1224. package/src/modules/workspace/__tests__/workspace.tools.branch-errors.test.ts +212 -0
  1225. package/src/modules/workspace/__tests__/workspace.tools.test.ts +2632 -110
  1226. package/src/modules/workspace/__tests__/write-denial.test.ts +75 -0
  1227. package/src/modules/workspace/empty-dirs.ts +79 -0
  1228. package/src/modules/workspace/file-readers/README.md +32 -0
  1229. package/src/modules/workspace/file-readers/__tests__/doc-extract.test.ts +59 -2
  1230. package/src/modules/workspace/file-readers/__tests__/file-reader.registry.test.ts +133 -4
  1231. package/src/modules/workspace/file-readers/__tests__/fixtures/soft-break.docx +0 -0
  1232. package/src/modules/workspace/file-readers/__tests__/fixtures/tab.docx +0 -0
  1233. package/src/modules/workspace/file-readers/content-mode.ts +87 -0
  1234. package/src/modules/workspace/file-readers/doc-extract.service.ts +4 -1
  1235. package/src/modules/workspace/file-readers/doc-extract.types.ts +8 -4
  1236. package/src/modules/workspace/file-readers/document-reader.ts +6 -0
  1237. package/src/modules/workspace/file-readers/extract-docx.ts +6 -0
  1238. package/src/modules/workspace/file-readers/file-reader.registry.ts +9 -1
  1239. package/src/modules/workspace/file-readers/file-reader.ts +27 -6
  1240. package/src/modules/workspace/file-readers/image-reader.ts +10 -5
  1241. package/src/modules/workspace/file-readers/ooxml-text.ts +38 -2
  1242. package/src/modules/workspace/file-readers/text-reader.ts +61 -4
  1243. package/src/modules/workspace/git-internals.middleware.ts +86 -0
  1244. package/src/modules/workspace/not-found.ts +119 -0
  1245. package/src/modules/workspace/session-ontology.gate.ts +1 -1
  1246. package/src/modules/workspace/startup/__tests__/beside-checkout.test.ts +181 -0
  1247. package/src/modules/workspace/startup/__tests__/kb-startup-runner.degraded.test.ts +361 -0
  1248. package/src/modules/workspace/startup/__tests__/kb-startup-runner.test.ts +53 -16
  1249. package/src/modules/workspace/startup/beside-checkout.ts +112 -0
  1250. package/src/modules/workspace/startup/kb-git.ts +43 -36
  1251. package/src/modules/workspace/startup/kb-startup-runner.ts +303 -63
  1252. package/src/modules/workspace/startup/steps/__tests__/steps.test.ts +1405 -48
  1253. package/src/modules/workspace/startup/steps/groups-to-plugins.step.ts +460 -417
  1254. package/src/modules/workspace/startup/steps/personal-spaces.step.ts +191 -0
  1255. package/src/modules/workspace/startup/steps/plugin-display-names.step.ts +169 -0
  1256. package/src/modules/workspace/startup/steps/plugin-layout.ts +110 -0
  1257. package/src/modules/workspace/startup/steps/plugin-manifests.step.ts +90 -0
  1258. package/src/modules/workspace/startup/steps/roles-yaml.step.ts +6 -12
  1259. package/src/modules/workspace/startup/steps/seed-tree.ts +177 -54
  1260. package/src/modules/workspace/startup/steps/template-files.step.ts +581 -126
  1261. package/src/modules/workspace/startup/steps/template-source.ts +168 -0
  1262. package/src/modules/workspace/workspace.routes.ts +691 -269
  1263. package/src/modules/workspace/workspace.service.ts +1071 -292
  1264. package/src/modules/workspace/workspace.tools.ts +1787 -180
  1265. package/src/modules/workspace/write-denial.ts +189 -0
  1266. package/src/shared/__tests__/canonical-file-identity.test.ts +154 -0
  1267. package/src/shared/__tests__/email-identity.test.ts +35 -0
  1268. package/src/shared/__tests__/filename.test.ts +0 -0
  1269. package/src/shared/__tests__/frontmatter-id.test.ts +5 -0
  1270. package/src/shared/__tests__/git-failure.test.ts +198 -0
  1271. package/src/shared/__tests__/git-internals.test.ts +115 -0
  1272. package/src/shared/__tests__/http-errors.test.ts +42 -0
  1273. package/src/shared/__tests__/logging.test.ts +185 -0
  1274. package/src/shared/__tests__/path-containment.test.ts +60 -0
  1275. package/src/shared/__tests__/path-order.test.ts +24 -0
  1276. package/src/shared/__tests__/pending-proposals.readAt.test.ts +102 -0
  1277. package/src/shared/__tests__/printable.test.ts +64 -0
  1278. package/src/shared/__tests__/redact-secret.test.ts +122 -0
  1279. package/src/shared/__tests__/rename-no-replace.test.ts +466 -0
  1280. package/src/shared/__tests__/ssrf.test.ts +20 -0
  1281. package/src/shared/__tests__/ttl-cache.test.ts +55 -0
  1282. package/src/shared/canonical-file-identity.ts +92 -0
  1283. package/src/shared/domain-errors.ts +178 -0
  1284. package/src/shared/email-identity.ts +38 -0
  1285. package/src/shared/fs.contract.ts +225 -0
  1286. package/src/shared/git-failure.ts +196 -0
  1287. package/src/shared/git-internals.ts +124 -0
  1288. package/src/shared/git.contract.ts +170 -0
  1289. package/src/shared/http-errors.ts +36 -0
  1290. package/src/shared/logger.contract.ts +141 -0
  1291. package/src/shared/logging.ts +96 -0
  1292. package/src/shared/path-containment.ts +34 -0
  1293. package/src/shared/path-order.ts +33 -0
  1294. package/src/shared/pending-proposals.ts +161 -0
  1295. package/src/shared/printable.ts +68 -0
  1296. package/src/shared/redact-secret.ts +118 -0
  1297. package/src/shared/rename-no-replace.ts +386 -0
  1298. package/src/shared/ssrf.ts +44 -16
  1299. package/src/shared/ttl-cache.ts +33 -1
  1300. package/src/version.ts +10 -6
  1301. package/src/modules/diff/diff-paths.ts +0 -9
  1302. package/src/modules/mcp/__tests__/mcp-session-store.test.ts +0 -151
  1303. package/src/modules/mcp/__tests__/mcp.routes.session.test.ts +0 -226
  1304. package/src/modules/mcp/mcp-session-store.ts +0 -162
  1305. package/src/shared/__tests__/fs-walk.test.ts +0 -42
  1306. package/src/shared/fs-walk.ts +0 -30
  1307. package/src/shared/hash-email.ts +0 -14
@@ -1,17 +1,30 @@
1
1
  import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
+ import { NodeFs } from '../../../../kb-fs/node-fs.js';
2
3
  import { execFile } from 'node:child_process';
3
4
  import { promisify } from 'node:util';
4
5
  import fs from 'node:fs/promises';
5
6
  import os from 'node:os';
6
7
  import path from 'node:path';
7
8
  import { KbStartupRunner } from '../../kb-startup-runner.js';
9
+ import { NodeGitRunner } from '../../../../workflow/git/node-git-runner.js';
8
10
  import type { OnServerStart, ServerStartContext, StepResult } from '../../on-server-start.js';
9
11
  import { GroupsToPluginsStep } from '../groups-to-plugins.step.js';
12
+ import { PluginDisplayNamesStep } from '../plugin-display-names.step.js';
13
+ import { PluginManifestsStep } from '../plugin-manifests.step.js';
14
+ import { PersonalSpacesStep } from '../personal-spaces.step.js';
10
15
  import { RolesYamlStep } from '../roles-yaml.step.js';
11
16
  import { renderRolesYaml } from '../../../../access-model/render-roles-yaml.js';
17
+ import { mergeGroupsIntoRoles, parseRolesYaml } from '../../../../access-model/access-grammar.js';
12
18
  import { TemplateFilesStep } from '../template-files.step.js';
13
19
  import { buildSeedTree } from '../seed-tree.js';
14
20
  import { defaultKbTemplateDir } from '../../../../../assets.js';
21
+ import {
22
+ DEFAULT_KB_LAYOUT,
23
+ agentsFilePointerSentence,
24
+ configureKbLayout,
25
+ renderKbLayoutPlaceholders,
26
+ } from '@bevel-software/platform-shared';
27
+ import { PLATFORM_HEADER, TOOL_PREFIX_LINE, composeAgentInstructions } from '../../../../agent-instructions/index.js';
15
28
 
16
29
  const execFileAsync = promisify(execFile);
17
30
 
@@ -54,11 +67,16 @@ beforeEach(async () => {
54
67
 
55
68
  afterEach(async () => {
56
69
  vi.restoreAllMocks();
70
+ configureKbLayout({ ...DEFAULT_KB_LAYOUT });
57
71
  await fs.rm(root, { recursive: true, force: true });
58
72
  });
59
73
 
60
74
  /** A populated upstream carrying `files`: one commit, both protected refs. Returns the seed clone. */
61
- async function seedUpstream(files: Record<string, string>): Promise<string> {
75
+ async function seedUpstream(
76
+ files: Record<string, string>,
77
+ /** Symlinks to seed beside them: path in the repo → the target it points at. */
78
+ symlinks: Record<string, string> = {},
79
+ ): Promise<string> {
62
80
  const seed = path.join(root, '.seed');
63
81
  await fs.mkdir(seed, { recursive: true });
64
82
  await git(seed, ['init', '-b', DEFAULT_BRANCH]);
@@ -67,6 +85,11 @@ async function seedUpstream(files: Record<string, string>): Promise<string> {
67
85
  await fs.mkdir(path.dirname(abs), { recursive: true });
68
86
  await fs.writeFile(abs, content, 'utf8');
69
87
  }
88
+ for (const [rel, target] of Object.entries(symlinks)) {
89
+ const abs = path.join(seed, rel);
90
+ await fs.mkdir(path.dirname(abs), { recursive: true });
91
+ await fs.symlink(target, abs);
92
+ }
70
93
  await git(seed, ['add', '-A']);
71
94
  await git(seed, ['commit', '-m', 'init']);
72
95
  await git(seed, ['branch', PROTECTED[1]!]);
@@ -77,6 +100,7 @@ async function seedUpstream(files: Record<string, string>): Promise<string> {
77
100
 
78
101
  function makeRunner(steps: OnServerStart[], templateDir: string = TEMPLATE_DIR) {
79
102
  return new KbStartupRunner({
103
+ gitRunner: new NodeGitRunner(),
80
104
  kbRepoUrl: () => upstream,
81
105
  gitUsername: () => 'x-access-token',
82
106
  workspacesRoot,
@@ -107,10 +131,18 @@ async function exists(dir: string, rel: string): Promise<boolean> {
107
131
  return fs.access(path.join(dir, rel)).then(() => true, () => false);
108
132
  }
109
133
 
134
+ async function readJson(dir: string, rel: string): Promise<Record<string, any>> {
135
+ return JSON.parse(await fs.readFile(path.join(dir, rel), 'utf8'));
136
+ }
137
+
110
138
  const norm = (text: string) => text.replace(/\r\n?/g, '\n');
139
+ const PREAMBLE_IGNORE_BLOCK =
140
+ '\n# Added by the platform: agent instructions are edited from External agent access.\n' +
141
+ '/mcp-description.md\n';
111
142
 
143
+ /** The template as the step writes it under the default layout — placeholders rendered. */
112
144
  async function template(name: string): Promise<string> {
113
- return fs.readFile(path.join(TEMPLATE_DIR, name), 'utf8');
145
+ return renderKbLayoutPlaceholders(await fs.readFile(path.join(TEMPLATE_DIR, name), 'utf8'), DEFAULT_KB_LAYOUT);
114
146
  }
115
147
 
116
148
  /** Every required file + reserved root already present, from the real template. */
@@ -120,19 +152,21 @@ async function fullScaffold(): Promise<Record<string, string>> {
120
152
  'AGENTS.md': await template('AGENTS.md'),
121
153
  '.bevelignore': await template('.bevelignore'),
122
154
  '.gitignore': await template('gitignore.template'),
155
+ 'mcp-description.md': await template('mcp-description.md'),
123
156
  'KnowledgeBase/.gitkeep': '',
124
157
  'Plugins/.gitkeep': '',
158
+ 'Skills/.gitkeep': '',
125
159
  };
126
160
  }
127
161
 
128
162
  describe('TemplateFilesStep', () => {
129
163
  it('adds the missing scaffolding — .gitignore arriving from its packable template spelling', async () => {
130
164
  await seedUpstream({ 'marker.txt': 'seeded' });
131
- await makeRunner([new TemplateFilesStep()]).runAll();
165
+ await makeRunner([new TemplateFilesStep(new NodeFs())]).runAll();
132
166
 
133
167
  for (const b of PROTECTED) {
134
168
  const dir = await checkout(b);
135
- for (const rel of ['access.md', 'AGENTS.md', '.bevelignore', '.gitignore']) {
169
+ for (const rel of ['access.md', 'AGENTS.md', '.bevelignore', '.gitignore', 'mcp-description.md']) {
136
170
  expect(await exists(dir, rel), `${b}: ${rel}`).toBe(true);
137
171
  }
138
172
  // The packaged template cannot ship a literal .gitignore (npm strips
@@ -144,15 +178,112 @@ describe('TemplateFilesStep', () => {
144
178
  // Reserved roots materialize as <dir>/.gitkeep.
145
179
  expect(await exists(dir, 'KnowledgeBase/.gitkeep')).toBe(true);
146
180
  expect(await exists(dir, 'Plugins/.gitkeep')).toBe(true);
181
+ expect(await exists(dir, 'Skills/.gitkeep')).toBe(true);
147
182
  const subject = (await git(dir, ['log', '--format=%s', '-1'])).trim();
148
183
  expect(subject).toMatch(/^Add missing KB scaffolding: /);
149
184
  expect(subject).toContain('.gitignore');
150
185
  }
151
186
  });
152
187
 
188
+ it('renders the managed files with the deployment\'s own root names', async () => {
189
+ // A deployment that renamed its roots must hand the agent a guide naming
190
+ // the folders it will find, and an ignore file hiding the real ones.
191
+ configureKbLayout({ knowledgeBaseDir: 'docs', skillsDir: 'skills', pluginsDir: 'plugins' });
192
+ await seedUpstream({ 'marker.txt': 'seeded' });
193
+ await makeRunner([new TemplateFilesStep(new NodeFs())]).runAll();
194
+
195
+ const dir = await checkout(DEFAULT_BRANCH);
196
+ const agents = norm(await fs.readFile(path.join(dir, 'AGENTS.md'), 'utf8'));
197
+ expect(agents).toContain('plugins/<Plugin>/plugin.json');
198
+ expect(agents).toContain('`docs/`');
199
+ expect(agents).not.toContain('{{');
200
+ expect(agents).not.toContain('KnowledgeBase/');
201
+ const ignore = norm(await fs.readFile(path.join(dir, '.bevelignore'), 'utf8')).split('\n').map((l) => l.trim());
202
+ // Both roots stay VISIBLE, whatever they are called: the Skills & Tools
203
+ // sidebar reads each from the workspace tree.
204
+ expect(ignore).not.toContain('plugins/');
205
+ expect(ignore).not.toContain('skills/');
206
+ expect(ignore).not.toContain('Plugins/');
207
+ expect(ignore).toContain('roles.yaml');
208
+ expect(await exists(dir, 'docs/.gitkeep')).toBe(true);
209
+
210
+ // And a second boot sees the rendered guide as current: no churn commit.
211
+ await makeRunner([new TemplateFilesStep(new NodeFs())]).runAll();
212
+ const again = await checkout(DEFAULT_BRANCH);
213
+ expect((await git(again, ['rev-list', '--count', 'HEAD'])).trim()).toBe('2'); // init + scaffolding
214
+ });
215
+
216
+ /**
217
+ * The placement rule. A tester's agent filed a ticket into a plugin folder
218
+ * and explained itself: it had found no convention saying where tickets go,
219
+ * and Plugins was where it already held write rights. The guide named the
220
+ * three roots but never said which KIND of file belongs in which — so the
221
+ * section has to survive into the guide agents actually read, which is this
222
+ * written file (an MCP session reads the branch's copy of it), under the
223
+ * deployment's own root names rather than the placeholders.
224
+ */
225
+ it('writes the "Where a new file goes" placement rule, named for the deployment\'s roots', async () => {
226
+ configureKbLayout({ knowledgeBaseDir: 'Docs', skillsDir: 'Abilities', pluginsDir: 'Extensions' });
227
+ await seedUpstream({ 'marker.txt': 'seeded' });
228
+ await makeRunner([new TemplateFilesStep(new NodeFs())]).runAll();
229
+
230
+ const dir = await checkout(DEFAULT_BRANCH);
231
+ const agents = norm(await fs.readFile(path.join(dir, 'AGENTS.md'), 'utf8'));
232
+ const start = agents.indexOf('## Where a new file goes\n');
233
+ expect(start, 'the written guide has no placement section').toBeGreaterThan(-1);
234
+ // The section alone, so a phrase matched elsewhere in the guide cannot
235
+ // stand in for a rule that is missing here.
236
+ const section = agents.slice(start).split('\n## ')[0]!;
237
+
238
+ // A document goes to the knowledge root, a shared skill to the skills
239
+ // root or the plugin's own skills folder, machinery inside a plugin.
240
+ expect(section).toContain('`Docs/`');
241
+ expect(section).toContain('`Abilities/`');
242
+ expect(section).toContain('Extensions/<Plugin>/skills/<skill>/SKILL.md');
243
+ expect(section).toContain('Extensions/<Plugin>/software.bevel.hexis/tools/');
244
+ for (const kind of ['notes', 'reports', 'tickets', 'mcp.json', 'plugin.json']) {
245
+ expect(section, kind).toContain(kind);
246
+ }
247
+ // And the two rules the tester's agent broke.
248
+ expect(section).toMatch(/never holds a document/);
249
+ expect(section).toMatch(/\bask\b/);
250
+
251
+ // Rendered, not raw: a leftover placeholder would name a folder that is
252
+ // not there in a section whose whole job is naming the right folder.
253
+ expect(section).not.toContain('{{');
254
+ expect(section).not.toContain('KnowledgeBase/');
255
+ });
256
+
257
+ /**
258
+ * The step is built at boot, while the process still runs the defaults; the
259
+ * save that completes first-run setup applies the admin's names afterwards.
260
+ * A root list snapshotted at construction scaffolded `Skills/` beside the
261
+ * `skills/` they had just chosen — the very bug the names exist to avoid.
262
+ */
263
+ it('scaffolds the layout in effect when it RUNS, not the one it was built under', async () => {
264
+ const builtAtBoot = new TemplateFilesStep(new NodeFs());
265
+ await seedUpstream({ 'skills/deploy/SKILL.md': '# deploy\n' });
266
+ configureKbLayout({ knowledgeBaseDir: 'Docs', skillsDir: 'skills', pluginsDir: 'Plugins' });
267
+ await makeRunner([builtAtBoot]).runAll();
268
+
269
+ const dir = await checkout(DEFAULT_BRANCH);
270
+ // Listed rather than probed: a probe for `Skills` answers yes on a
271
+ // case-insensitive filesystem that holds `skills`.
272
+ const roots = (await fs.readdir(dir, { withFileTypes: true }))
273
+ .filter((e) => e.isDirectory() && e.name !== '.git')
274
+ .map((e) => e.name)
275
+ .sort();
276
+ expect(roots).toEqual(['Docs', 'Plugins', 'skills']);
277
+ expect(await exists(dir, 'Docs/.gitkeep')).toBe(true);
278
+ const subject = (await git(dir, ['log', '--format=%s', '-1'])).trim();
279
+ expect(subject).toContain('Docs/.gitkeep');
280
+ expect(subject).not.toContain('KnowledgeBase/');
281
+ expect(subject).not.toContain('Skills/');
282
+ });
283
+
153
284
  it('replaces a drifted AGENTS.md, and says so when that is the only change', async () => {
154
285
  await seedUpstream({ ...(await fullScaffold()), 'AGENTS.md': 'stale conventions\n' });
155
- await makeRunner([new TemplateFilesStep()]).runAll();
286
+ await makeRunner([new TemplateFilesStep(new NodeFs())]).runAll();
156
287
 
157
288
  const dir = await checkout(DEFAULT_BRANCH);
158
289
  expect(norm(await fs.readFile(path.join(dir, 'AGENTS.md'), 'utf8'))).toBe(norm(await template('AGENTS.md')));
@@ -160,13 +291,79 @@ describe('TemplateFilesStep', () => {
160
291
  expect(subject).toBe('Update AGENTS.md to the current platform template');
161
292
  });
162
293
 
163
- it('rejects .git — any case — as a reserved root name', async () => {
164
- for (const bad of ['.git', '.GIT', '.Git']) {
165
- expect(() => new TemplateFilesStep([bad]), bad).toThrow(/must not be "\.git"/);
294
+ it('ships the roles-vs-groups guide, and a deployment on an older template receives it at boot', async () => {
295
+ const guide = await template('AGENTS.md');
296
+ // The section runs from its own heading to the next heading of any level (or the end).
297
+ const section = guide.split(/\n(?=#{1,6} )/).find((s) => s.startsWith('### Roles are pre-set')) ?? '';
298
+ const prose = section.replace(/\s+/g, ' ');
299
+ // What roles are, that agents never create them, the group test, what to do instead.
300
+ expect(prose).toContain('A role in `roles.yaml` is an app role');
301
+ expect(prose).toContain('**Agents never create roles.**');
302
+ expect(prose).toContain('**Is it really a group?**');
303
+ expect(prose).toContain('**What to do instead.**');
304
+ expect(prose).toContain('add people to existing roles, and use a GROUP for a task- or team-scoped set of people');
305
+
306
+ // An existing deployment's AGENTS.md from before the guide existed.
307
+ await seedUpstream({ ...(await fullScaffold()), 'AGENTS.md': guide.replace(section, '') });
308
+ await makeRunner([new TemplateFilesStep(new NodeFs())]).runAll();
309
+
310
+ const dir = await checkout(DEFAULT_BRANCH);
311
+ expect(norm(await fs.readFile(path.join(dir, 'AGENTS.md'), 'utf8'))).toContain('**Agents never create roles.**');
312
+ });
313
+
314
+ it('documents giving a role to a group, with a valid example, in the guide the step writes', async () => {
315
+ const guide = await template('AGENTS.md');
316
+ const section = guide.split(/\n(?=#{2,3} )/).find((s) => s.startsWith('### Giving a role to a group')) ?? '';
317
+ const prose = section.replace(/\s+/g, ' ');
318
+ expect(prose).toContain('`- group:<Name>`');
319
+ expect(prose).toContain('case- and whitespace-insensitively against the active group source');
320
+ expect(prose).toContain('validation error');
321
+ expect(prose).toContain('names the entry');
322
+ expect(prose).toContain("removes the role's contribution for everyone in the group");
323
+ // A bare name resolves to a same-named group first, so role denials use `role/`.
324
+ expect(prose).toContain('`deny role/Reviewer`');
325
+ // Closeness first: a person's own entry wins only inside the same access.md.
326
+ expect(prose).toContain('The nearest `access.md` that says anything about the person decides');
327
+ expect(prose).toContain('in the SAME `access.md` as the denial keeps that access');
328
+ expect(prose).toContain('A group under `Admin` makes every member a full admin');
329
+ // A change request cannot carry a roles.yaml edit — the merge restores the
330
+ // base file — so the guide says who changes roles and where, and never
331
+ // walks an agent through drafting one.
332
+ expect(prose).toContain('Only an Admin changes `roles.yaml`');
333
+ expect(prose).toContain('A change request cannot carry the edit');
334
+ expect(prose).toContain('Do not propose one');
335
+ expect(prose).toContain('`edit_file`');
336
+ for (const tool of ['create_branch', 'commit_change', 'open_change_request']) {
337
+ expect(prose).not.toContain(`\`${tool}\``);
338
+ }
339
+
340
+ // The example parses as a roles.yaml whose group entry names a real group.
341
+ const example = /```yaml\n([\s\S]*?)```/.exec(section)?.[1] ?? '';
342
+ expect(example).toContain('- group:Platform Team');
343
+ const parsed = parseRolesYaml(example);
344
+ expect(parsed.ok).toBe(true);
345
+ if (!parsed.ok) return;
346
+ const groups = new Map([['platform team', { displayName: 'Platform Team', emails: new Set(['pat@example.com']) }]]);
347
+ expect(mergeGroupsIntoRoles(parsed.index, groups, 'groups.yaml')).toEqual([]);
348
+ // The example hands out an ordinary role, never Admin.
349
+ expect(parsed.index.byEmail.get('pat@example.com')?.has('reviewer')).toBe(true);
350
+ expect(parsed.index.byEmail.get('pat@example.com')?.has('role/admin')).toBe(false);
351
+
352
+ // What the step writes — the file agents read through the MCP server — carries it.
353
+ await seedUpstream({ ...(await fullScaffold()), 'AGENTS.md': guide.replace(section, '') });
354
+ await makeRunner([new TemplateFilesStep(new NodeFs())]).runAll();
355
+ const written = norm(await fs.readFile(path.join(await checkout(DEFAULT_BRANCH), 'AGENTS.md'), 'utf8'));
356
+ expect(written).toContain('### Giving a role to a group');
357
+ expect(written).toContain(example);
358
+ });
359
+
360
+ it('rejects the git folder — in any spelling — as a reserved root name', async () => {
361
+ for (const bad of ['.git', '.GIT', '.Git', '.git.', '%2egit']) {
362
+ expect(() => new TemplateFilesStep(new NodeFs(), [bad]), bad).toThrow(/must not name the git folder/);
166
363
  }
167
364
  });
168
365
 
169
- it('appends the AGENTS.md rule to a custom template\'s .bevelignore that lacks it, at declaration time', async () => {
366
+ it('appends the managed root-file rules to a custom template that lacks them', async () => {
170
367
  // A distribution's own template whose ignore file does not carry the rule
171
368
  // the on-disk merge assumes: the merge only runs against an EXISTING file,
172
369
  // so the declared content itself must arrive with the rule in it.
@@ -178,44 +375,689 @@ describe('TemplateFilesStep', () => {
178
375
  delete scaffold['.bevelignore']; // the one file the step will declare from the template
179
376
  await seedUpstream(scaffold);
180
377
 
181
- await makeRunner([new TemplateFilesStep()], customTemplate).runAll();
378
+ await makeRunner([new TemplateFilesStep(new NodeFs())], customTemplate).runAll();
182
379
 
183
380
  const dir = await checkout(DEFAULT_BRANCH);
184
381
  const lines = norm(await fs.readFile(path.join(dir, '.bevelignore'), 'utf8')).split('\n').map((l) => l.trim());
185
382
  expect(lines).toContain('MyStuff/'); // the operator's rules survive
186
383
  expect(lines).toContain('AGENTS.md'); // the platform's rule was appended
384
+ expect(lines).toContain('/mcp-description.md');
385
+ expect(lines).not.toContain('Skills/'); // never the skills root — the Library's tree needs it
386
+ });
387
+
388
+ it('drops the Skills/ rule an earlier release appended, and its comment, keeping every other rule', async () => {
389
+ // A knowledge base whose ignore file was topped up by the release that
390
+ // hid the skills root: the platform's comment + line sit at the end. The
391
+ // Skills & Tools sidebar reads that root from the workspace tree now, so
392
+ // the rule comes out exactly as it went in — nothing of the operator's moves.
393
+ const scaffold = await fullScaffold();
394
+ scaffold['.bevelignore'] =
395
+ '# mine\n.git/\nAGENTS.md\nPlugins/\n\n# Added by the platform: the conventions doc is not node content.\nSkills/\n';
396
+ await seedUpstream(scaffold);
397
+
398
+ await makeRunner([new TemplateFilesStep(new NodeFs())]).runAll();
399
+
400
+ const dir = await checkout(DEFAULT_BRANCH);
401
+ const text = norm(await fs.readFile(path.join(dir, '.bevelignore'), 'utf8'));
402
+ // The plugins-root rule goes with it — the same sidebar draws that root now.
403
+ expect(text).toBe('# mine\n.git/\nAGENTS.md\n' + PREAMBLE_IGNORE_BLOCK);
404
+ // Idempotent: a second boot has nothing to change.
405
+ await makeRunner([new TemplateFilesStep(new NodeFs())]).runAll();
406
+ const again = await checkout(DEFAULT_BRANCH);
407
+ expect(norm(await fs.readFile(path.join(again, '.bevelignore'), 'utf8'))).toBe(text);
408
+ });
409
+
410
+ it("drops the template's own Skills/ rule from a KB seeded by that release, comment included — whatever root the comment named", async () => {
411
+ // The previous template listed the rule under its own explanatory line,
412
+ // ending with the plugins root's name of the day; a deployment may have
413
+ // renamed that root since, so the line is known by its opening.
414
+ const scaffold = await fullScaffold();
415
+ scaffold['.bevelignore'] =
416
+ 'AGENTS.md\nPlugins/\n# The shared-skills root is rendered by the Skills & Tools app, like Groups/.\nSkills/\n';
417
+ await seedUpstream(scaffold);
418
+
419
+ await makeRunner([new TemplateFilesStep(new NodeFs())]).runAll();
420
+
421
+ const dir = await checkout(DEFAULT_BRANCH);
422
+ expect(norm(await fs.readFile(path.join(dir, '.bevelignore'), 'utf8'))).toBe(
423
+ 'AGENTS.md\n' + PREAMBLE_IGNORE_BLOCK,
424
+ );
425
+ });
426
+
427
+ it('recognises the legacy comment for a renamed plugins root with a space in its name', async () => {
428
+ // The name between the fixed opening and closing is judged by the one
429
+ // root-name rule the platform has, so every name it could have rendered
430
+ // there is recognised — and nothing a root cannot be called is.
431
+ const scaffold = await fullScaffold();
432
+ scaffold['.bevelignore'] =
433
+ 'AGENTS.md\nPlugins/\n# The shared-skills root is rendered by the Skills & Tools app, like My Plugins/.\nSkills/\n';
434
+ await seedUpstream(scaffold);
435
+
436
+ await makeRunner([new TemplateFilesStep(new NodeFs())]).runAll();
437
+
438
+ const dir = await checkout(DEFAULT_BRANCH);
439
+ expect(norm(await fs.readFile(path.join(dir, '.bevelignore'), 'utf8'))).toBe(
440
+ 'AGENTS.md\n' + PREAMBLE_IGNORE_BLOCK,
441
+ );
442
+ });
443
+
444
+ it("keeps an operator's Skills/ rule whose own comment merely opens like the platform's", async () => {
445
+ // Provenance is the platform's EXACT comment. A comment that begins the
446
+ // same way and goes on differently was never written by the platform.
447
+ const scaffold = await fullScaffold();
448
+ const text =
449
+ 'AGENTS.md\n# The shared-skills root is rendered by the Skills & Tools app, and I hide it anyway\nSkills/\n';
450
+ scaffold['.bevelignore'] = text;
451
+ await seedUpstream(scaffold);
452
+
453
+ await makeRunner([new TemplateFilesStep(new NodeFs())]).runAll();
454
+
455
+ const dir = await checkout(DEFAULT_BRANCH);
456
+ expect(norm(await fs.readFile(path.join(dir, '.bevelignore'), 'utf8'))).toBe(
457
+ text + PREAMBLE_IGNORE_BLOCK,
458
+ );
459
+ });
460
+
461
+ it("keeps a Skills/ rule the operator wrote themselves — provenance is the platform's comment", async () => {
462
+ const scaffold = await fullScaffold();
463
+ scaffold['.bevelignore'] = 'AGENTS.md\n# I hide skills on purpose\nSkills/\n';
464
+ await seedUpstream(scaffold);
465
+
466
+ await makeRunner([new TemplateFilesStep(new NodeFs())]).runAll();
467
+
468
+ const dir = await checkout(DEFAULT_BRANCH);
469
+ expect(norm(await fs.readFile(path.join(dir, '.bevelignore'), 'utf8'))).toBe(
470
+ 'AGENTS.md\n# I hide skills on purpose\nSkills/\n' + PREAMBLE_IGNORE_BLOCK,
471
+ );
472
+ });
473
+
474
+ it('drops EVERY Plugins/ rule, whoever wrote it — the sidebar draws that root now, and the seed left no comment to know it by', async () => {
475
+ // The first template ever seeded hid the plugins root as a bare line at
476
+ // the end of the file, so provenance cannot tell the platform's copy
477
+ // from an operator's; both would empty the Plugins tree, and both go.
478
+ // One under the platform's own comment loses the comment with it; a
479
+ // `!Plugins/` negation is not the rule and stays.
480
+ const scaffold = await fullScaffold();
481
+ scaffold['.bevelignore'] =
482
+ '# mine\nAGENTS.md\nPlugins/\nMy-Own-Rule/\n# Added by the platform: the conventions doc is not node content.\nPlugins/\n!Plugins/\n';
483
+ await seedUpstream(scaffold);
484
+
485
+ await makeRunner([new TemplateFilesStep(new NodeFs())]).runAll();
486
+
487
+ const dir = await checkout(DEFAULT_BRANCH);
488
+ const text = norm(await fs.readFile(path.join(dir, '.bevelignore'), 'utf8'));
489
+ expect(text).toBe('# mine\nAGENTS.md\nMy-Own-Rule/\n!Plugins/\n' + PREAMBLE_IGNORE_BLOCK);
490
+ // Idempotent: a second boot has nothing to change.
491
+ await makeRunner([new TemplateFilesStep(new NodeFs())]).runAll();
492
+ const again = await checkout(DEFAULT_BRANCH);
493
+ expect(norm(await fs.readFile(path.join(again, '.bevelignore'), 'utf8'))).toBe(text);
494
+ });
495
+
496
+ it("declares a custom template's ignore file without the Plugins/ rule it still ships", async () => {
497
+ // A distribution's own template may still carry the rule the packaged
498
+ // one dropped; a KB seeded from it must not start out hiding the root.
499
+ const customTemplate = path.join(root, 'custom-template-plugins-rule');
500
+ await fs.cp(TEMPLATE_DIR, customTemplate, { recursive: true });
501
+ await fs.writeFile(path.join(customTemplate, '.bevelignore'), '.git/\nAGENTS.md\n\nPlugins/\n');
502
+ await seedUpstream({ 'marker.txt': 'seeded' });
503
+
504
+ await makeRunner([new TemplateFilesStep(new NodeFs())], customTemplate).runAll();
505
+
506
+ const dir = await checkout(DEFAULT_BRANCH);
507
+ const lines = norm(await fs.readFile(path.join(dir, '.bevelignore'), 'utf8')).split('\n').map((l) => l.trim());
508
+ expect(lines).not.toContain('Plugins/');
509
+ expect(lines).toContain('AGENTS.md');
510
+ });
511
+
512
+ it('never seeds a template entry that links into a git folder', async () => {
513
+ // A template that IS a working tree (this repo in a Docker build) can hold
514
+ // a link into its own .git. The name rule cannot see it; the target can.
515
+ const customTemplate = path.join(root, 'custom-template-gitlink');
516
+ await fs.cp(TEMPLATE_DIR, customTemplate, { recursive: true });
517
+ await fs.mkdir(path.join(customTemplate, '.git'), { recursive: true });
518
+ await fs.writeFile(path.join(customTemplate, '.git', 'config'), '[credential]\n\thelper = store\n');
519
+ await fs.symlink(path.join('.git', 'config'), path.join(customTemplate, 'notes.md'));
520
+
521
+ const dir = path.join(root, 'seeded-gitlink');
522
+ await fs.mkdir(dir, { recursive: true });
523
+ await buildSeedTree(new NodeFs(), customTemplate, [], ['admin@example.com'])(dir);
524
+
525
+ await expect(fs.readFile(path.join(dir, 'notes.md'), 'utf8')).rejects.toMatchObject({ code: 'ENOENT' });
526
+ await expect(fs.readdir(path.join(dir, '.git'))).rejects.toMatchObject({ code: 'ENOENT' });
527
+ // The rest of the template still seeded.
528
+ expect(await fs.readFile(path.join(dir, 'AGENTS.md'), 'utf8')).toContain('#');
529
+ });
530
+
531
+ it('seeds a binary template file byte for byte and keeps a script executable — text is what decodes', async () => {
532
+ const customTemplate = path.join(root, 'custom-template-bytes');
533
+ // The packaged template as a base, plus two files it does not ship.
534
+ await fs.cp(TEMPLATE_DIR, customTemplate, { recursive: true });
535
+ await fs.mkdir(path.join(customTemplate, 'assets'), { recursive: true });
536
+ await fs.mkdir(path.join(customTemplate, 'scripts'), { recursive: true });
537
+ // Not UTF-8, and holding a NUL: text by no reading of the bytes. A
538
+ // name-based rule once sent this through the decoder and changed it.
539
+ const binary = Buffer.from([0x89, 0x50, 0x00, 0xff, 0xfe, 0x7b, 0x7b, 0x7d, 0x7d]);
540
+ await fs.writeFile(path.join(customTemplate, 'assets', '.logo.bin'), binary);
541
+ await fs.writeFile(path.join(customTemplate, 'scripts', 'run.sh'), '#!/bin/sh\necho {{skillsDir}}\n', { mode: 0o755 });
542
+ // The empty-remote seed is the one path that copies a whole template.
543
+ const dir = path.join(root, 'seeded-bytes');
544
+ await fs.mkdir(dir, { recursive: true });
545
+ await buildSeedTree(new NodeFs(),customTemplate, [], ['admin@example.com'])(dir);
546
+
547
+ expect(await fs.readFile(path.join(dir, 'assets', '.logo.bin'))).toEqual(binary);
548
+ expect(norm(await fs.readFile(path.join(dir, 'scripts', 'run.sh'), 'utf8'))).toBe('#!/bin/sh\necho Skills\n');
549
+ if (process.platform !== 'win32') {
550
+ expect((await fs.stat(path.join(dir, 'scripts', 'run.sh'))).mode & 0o111).not.toBe(0);
551
+ }
552
+ });
553
+
554
+ it("declares a custom template's ignore file without the stale Skills/ rule it still ships", async () => {
555
+ // A KB with NO ignore file gets the template's copy — and a distribution's
556
+ // template may still carry the rule the previous release had. The on-disk
557
+ // reconciliation never runs on an absent file, so the declared content
558
+ // must arrive already reconciled.
559
+ const customTemplate = path.join(root, 'custom-template-stale');
560
+ await fs.mkdir(customTemplate, { recursive: true });
561
+ await fs.writeFile(path.join(customTemplate, 'AGENTS.md'), await template('AGENTS.md'), 'utf8');
562
+ await fs.writeFile(
563
+ path.join(customTemplate, '.bevelignore'),
564
+ '# custom\nMyStuff/\n# The shared-skills root is rendered by the Skills & Tools app, like {{pluginsDir}}/.\n{{skillsDir}}/\n',
565
+ 'utf8',
566
+ );
567
+ const scaffold = await fullScaffold();
568
+ delete scaffold['.bevelignore'];
569
+ await seedUpstream(scaffold);
570
+
571
+ await makeRunner([new TemplateFilesStep(new NodeFs())], customTemplate).runAll();
572
+
573
+ const dir = await checkout(DEFAULT_BRANCH);
574
+ const text = norm(await fs.readFile(path.join(dir, '.bevelignore'), 'utf8'));
575
+ expect(text).toContain('MyStuff/');
576
+ expect(text.split('\n').map((l) => l.trim())).not.toContain('Skills/');
577
+ expect(text).not.toContain('shared-skills root');
578
+ });
579
+
580
+ it("leaves an operator's !Skills/ negation alone — there is nothing of the platform's to remove", async () => {
581
+ const scaffold = await fullScaffold();
582
+ scaffold['.bevelignore'] = 'AGENTS.md\n!Skills/\n';
583
+ await seedUpstream(scaffold);
584
+
585
+ await makeRunner([new TemplateFilesStep(new NodeFs())]).runAll();
586
+
587
+ const dir = await checkout(DEFAULT_BRANCH);
588
+ expect(norm(await fs.readFile(path.join(dir, '.bevelignore'), 'utf8'))).toBe(
589
+ 'AGENTS.md\n!Skills/\n' + PREAMBLE_IGNORE_BLOCK,
590
+ );
187
591
  });
188
592
 
189
593
  it('respects an explicit !AGENTS.md negation — hiding the doc is a default, not a mandate', async () => {
190
594
  // Appending the positive rule after the negation would WIN under ordered
191
595
  // matching and silently defeat the operator's stated choice to show it.
192
596
  const scaffold = await fullScaffold();
193
- scaffold['.bevelignore'] = '# operator wants the doc visible\n!AGENTS.md\n';
597
+ scaffold['.bevelignore'] = '# operator wants the doc visible\n!AGENTS.md\nMyStuff/\n';
598
+ await seedUpstream(scaffold);
599
+
600
+ await makeRunner([new TemplateFilesStep(new NodeFs())]).runAll();
601
+
602
+ const dir = await checkout(DEFAULT_BRANCH);
603
+ expect(norm(await fs.readFile(path.join(dir, '.bevelignore'), 'utf8'))).toBe(
604
+ '# operator wants the doc visible\n!AGENTS.md\nMyStuff/\n' + PREAMBLE_IGNORE_BLOCK,
605
+ );
606
+ });
607
+
608
+ it('respects an explicit !mcp-description.md negation', async () => {
609
+ const scaffold = await fullScaffold();
610
+ scaffold['.bevelignore'] = 'AGENTS.md\n!mcp-description.md\n';
611
+ await seedUpstream(scaffold);
612
+
613
+ await makeRunner([new TemplateFilesStep(new NodeFs())]).runAll();
614
+
615
+ const dir = await checkout(DEFAULT_BRANCH);
616
+ expect(norm(await fs.readFile(path.join(dir, '.bevelignore'), 'utf8'))).toBe(
617
+ 'AGENTS.md\n!mcp-description.md\n',
618
+ );
619
+ });
620
+
621
+ it("respells the platform's OWN unanchored preamble rule instead of leaving it standing", async () => {
622
+ // What the release that shipped the unanchored spelling wrote. The bare
623
+ // name hides a nested `mcp-description.md` page too, so the platform's own
624
+ // line is corrected rather than treated as somebody's choice.
625
+ const scaffold = await fullScaffold();
626
+ scaffold['.bevelignore'] =
627
+ 'AGENTS.md\n# Added by the platform: agent instructions are edited from External agent access.\nmcp-description.md\n';
628
+ await seedUpstream(scaffold);
629
+
630
+ await makeRunner([new TemplateFilesStep(new NodeFs())]).runAll();
631
+
632
+ const dir = await checkout(DEFAULT_BRANCH);
633
+ expect(norm(await fs.readFile(path.join(dir, '.bevelignore'), 'utf8'))).toBe(
634
+ 'AGENTS.md\n# Added by the platform: agent instructions are edited from External agent access.\n/mcp-description.md\n',
635
+ );
636
+ // Idempotent: a second boot has nothing to change.
637
+ await makeRunner([new TemplateFilesStep(new NodeFs())]).runAll();
638
+ const again = await checkout(DEFAULT_BRANCH);
639
+ expect(norm(await fs.readFile(path.join(again, '.bevelignore'), 'utf8'))).toBe(
640
+ 'AGENTS.md\n# Added by the platform: agent instructions are edited from External agent access.\n/mcp-description.md\n',
641
+ );
642
+ });
643
+
644
+ it("keeps an unanchored preamble rule the operator wrote themselves, and adds nothing beside it", async () => {
645
+ const scaffold = await fullScaffold();
646
+ scaffold['.bevelignore'] = 'AGENTS.md\n# I hide it everywhere on purpose\nmcp-description.md\n';
194
647
  await seedUpstream(scaffold);
195
648
 
196
- await makeRunner([new TemplateFilesStep()]).runAll();
649
+ await makeRunner([new TemplateFilesStep(new NodeFs())]).runAll();
197
650
 
198
651
  const dir = await checkout(DEFAULT_BRANCH);
199
652
  expect(norm(await fs.readFile(path.join(dir, '.bevelignore'), 'utf8'))).toBe(
200
- '# operator wants the doc visible\n!AGENTS.md\n',
653
+ 'AGENTS.md\n# I hide it everywhere on purpose\nmcp-description.md\n',
201
654
  );
202
655
  });
203
656
 
657
+ it('seeds mcp-description.md on every protected branch, from the template, when it is missing', async () => {
658
+ const scaffold = await fullScaffold();
659
+ delete scaffold['mcp-description.md'];
660
+ await seedUpstream(scaffold);
661
+ await makeRunner([new TemplateFilesStep(new NodeFs())]).runAll();
662
+
663
+ for (const b of PROTECTED) {
664
+ const dir = await checkout(b);
665
+ expect(norm(await fs.readFile(path.join(dir, 'mcp-description.md'), 'utf8'))).toBe(
666
+ norm(await template('mcp-description.md')),
667
+ );
668
+ const subject = (await git(dir, ['log', '--format=%s', '-1'])).trim();
669
+ expect(subject).toBe('Add missing KB scaffolding: mcp-description.md');
670
+ }
671
+ });
672
+
673
+ it('leaves an existing mcp-description.md untouched: seeded once, never refreshed', async () => {
674
+ const mine = 'Acme builds solar farms.\n\n## What is where\n\n- Projects/\n';
675
+ await seedUpstream({ ...(await fullScaffold()), 'mcp-description.md': mine });
676
+ await makeRunner([new TemplateFilesStep(new NodeFs())]).runAll();
677
+
678
+ const dir = await checkout(DEFAULT_BRANCH);
679
+ expect(norm(await fs.readFile(path.join(dir, 'mcp-description.md'), 'utf8'))).toBe(mine);
680
+ expect((await git(dir, ['rev-list', '--count', 'HEAD'])).trim()).toBe('1'); // init only
681
+ });
682
+
683
+ it('keeps a deliberately emptied mcp-description.md empty: the top-up restores only a MISSING file', async () => {
684
+ await seedUpstream({ ...(await fullScaffold()), 'mcp-description.md': '' });
685
+ await makeRunner([new TemplateFilesStep(new NodeFs())]).runAll();
686
+
687
+ const dir = await checkout(DEFAULT_BRANCH);
688
+ expect(await fs.readFile(path.join(dir, 'mcp-description.md'), 'utf8')).toBe('');
689
+ expect((await git(dir, ['rev-list', '--count', 'HEAD'])).trim()).toBe('1');
690
+ });
691
+
692
+ it('ships a template that composes to the platform header alone: one comment, nothing broadcast', async () => {
693
+ const shipped = await template('mcp-description.md');
694
+ expect(shipped.trimStart().startsWith('<!--')).toBe(true);
695
+ expect(shipped.trimEnd().endsWith('-->')).toBe(true);
696
+ // The first line says how to turn the text on; the caps are stated inside.
697
+ expect(shipped.split('\n')[0]).toMatch(/Remove this comment wrapper/);
698
+ expect(shipped).toContain('6,000 characters');
699
+ expect(shipped).toContain('first paragraph under about 220');
700
+ expect(shipped).not.toContain('{{');
701
+ const composed = composeAgentInstructions(shipped);
702
+ expect(composed.instructions).toBe(PLATFORM_HEADER);
703
+ expect(composed.toolPrefix).toBe(TOOL_PREFIX_LINE);
704
+ expect(composed.unterminatedComment).toBe(false);
705
+ expect(composed.preambleChars).toBe(0);
706
+ });
707
+
708
+ it('seeds mcp-description.md from the packaged template when a custom template predates it, instead of failing the boot', async () => {
709
+ // A distribution's own KB_TEMPLATE_DIR forked before this file existed:
710
+ // the upgrade must not stop startup over a file whose content is one
711
+ // comment. The packaged copy stands in, and the log says so once.
712
+ const customTemplate = path.join(root, 'custom-template-old');
713
+ await fs.mkdir(customTemplate, { recursive: true });
714
+ await fs.writeFile(path.join(customTemplate, 'AGENTS.md'), await template('AGENTS.md'), 'utf8');
715
+ await fs.writeFile(path.join(customTemplate, 'access.md'), await template('access.md'), 'utf8');
716
+ const scaffold = await fullScaffold();
717
+ delete scaffold['mcp-description.md'];
718
+ await seedUpstream(scaffold);
719
+ const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
720
+
721
+ await makeRunner([new TemplateFilesStep(new NodeFs())], customTemplate).runAll();
722
+
723
+ const dir = await checkout(DEFAULT_BRANCH);
724
+ expect(norm(await fs.readFile(path.join(dir, 'mcp-description.md'), 'utf8'))).toBe(
725
+ norm(await template('mcp-description.md')),
726
+ );
727
+ expect(warn).toHaveBeenCalledWith(expect.stringContaining('no "mcp-description.md"'));
728
+ });
729
+
730
+ it('gives the stand-in to that file only: a custom template missing access.md still fails loudly', async () => {
731
+ const stricter = path.join(root, 'custom-template-broken');
732
+ await fs.mkdir(stricter, { recursive: true });
733
+ await fs.writeFile(path.join(stricter, 'AGENTS.md'), await template('AGENTS.md'), 'utf8');
734
+ const scaffold = await fullScaffold();
735
+ delete scaffold['access.md'];
736
+ await seedUpstream(scaffold);
737
+ await expect(makeRunner([new TemplateFilesStep(new NodeFs())], stricter).runAll()).rejects.toThrow(/ENOENT/);
738
+ });
739
+
740
+ it('points a clone at mcp-description.md from the managed AGENTS.md, before the platform mechanics', async () => {
741
+ const agents = await template('AGENTS.md');
742
+ const pointer = agents.indexOf('mcp-description.md');
743
+ expect(pointer).toBeGreaterThan(-1);
744
+ expect(pointer).toBeLessThan(agents.indexOf('## Directory Structure'));
745
+ expect(agents).toMatch(/default branch's copy inline/);
746
+ });
747
+
204
748
  it('treats a CRLF checkout of identical AGENTS.md content as current — no churn commit', async () => {
205
749
  const scaffold = await fullScaffold();
206
750
  scaffold['AGENTS.md'] = norm(scaffold['AGENTS.md']!).replace(/\n/g, '\r\n');
207
751
  await seedUpstream(scaffold);
208
- await makeRunner([new TemplateFilesStep()]).runAll();
752
+ await makeRunner([new TemplateFilesStep(new NodeFs())]).runAll();
209
753
 
210
754
  const dir = await checkout(DEFAULT_BRANCH);
211
755
  expect((await git(dir, ['rev-list', '--count', 'HEAD'])).trim()).toBe('1'); // init only
212
756
  });
757
+
758
+ /**
759
+ * The guide under a name of the deployment's own — and what becomes of the
760
+ * `AGENTS.md` beside it, which on such a deployment is the CUSTOMER'S file.
761
+ */
762
+ describe('a renamed agent guide', () => {
763
+ /** The scaffold minus the guide, so each test says for itself what `AGENTS.md` holds. */
764
+ async function scaffoldWithoutGuide(): Promise<Record<string, string>> {
765
+ const scaffold = await fullScaffold();
766
+ delete scaffold['AGENTS.md'];
767
+ return scaffold;
768
+ }
769
+
770
+ const CUSTOMER_GUIDE = '# Acme conventions\n\nWrite tickets in the present tense.\n';
771
+
772
+ it('writes and refreshes the guide under the configured name, on every protected branch', async () => {
773
+ configureKbLayout({ ...DEFAULT_KB_LAYOUT, agentsFile: 'HEXIS.md' });
774
+ await seedUpstream({ 'marker.txt': 'seeded' });
775
+ await makeRunner([new TemplateFilesStep(new NodeFs())]).runAll();
776
+
777
+ for (const branch of PROTECTED) {
778
+ const dir = await checkout(branch);
779
+ const guide = norm(await fs.readFile(path.join(dir, 'HEXIS.md'), 'utf8'));
780
+ // The header names the file it lives in, not the name it ships under.
781
+ expect(guide).toContain('`HEXIS.md`');
782
+ expect(guide).not.toContain('{{');
783
+ expect(await exists(dir, 'AGENTS.md')).toBe(false);
784
+ }
785
+
786
+ // A second boot sees the rendered guide as current: no churn commit…
787
+ await makeRunner([new TemplateFilesStep(new NodeFs())]).runAll();
788
+ expect((await git(await checkout(DEFAULT_BRANCH), ['rev-list', '--count', 'HEAD'])).trim()).toBe('2');
789
+
790
+ // …and a drifted copy is replaced, exactly as `AGENTS.md` is by default.
791
+ const dir = await checkout(DEFAULT_BRANCH);
792
+ await fs.writeFile(path.join(dir, 'HEXIS.md'), 'stale conventions\n', 'utf8');
793
+ await git(dir, ['commit', '-am', 'drift']);
794
+ await git(dir, ['push', 'origin', DEFAULT_BRANCH]);
795
+ await makeRunner([new TemplateFilesStep(new NodeFs())]).runAll();
796
+ const after = await checkout(DEFAULT_BRANCH);
797
+ expect(norm(await fs.readFile(path.join(after, 'HEXIS.md'), 'utf8'))).toContain('# Knowledge base');
798
+ expect((await git(after, ['log', '--format=%s', '-1'])).trim()).toBe(
799
+ 'Update HEXIS.md to the current platform template',
800
+ );
801
+ });
802
+
803
+ it('leaves a customer AGENTS.md byte for byte alone, and shows it in the tree', async () => {
804
+ configureKbLayout({ ...DEFAULT_KB_LAYOUT, agentsFile: 'HEXIS.md' });
805
+ // Their file already names the guide, so the pointer has nothing to add
806
+ // — this test is about the bytes and the ignore rules alone.
807
+ const theirs = `${CUSTOMER_GUIDE}\nSee HEXIS.md for the platform.\n`;
808
+ await seedUpstream({ ...(await scaffoldWithoutGuide()), 'AGENTS.md': theirs });
809
+ await makeRunner([new TemplateFilesStep(new NodeFs())]).runAll();
810
+
811
+ const dir = await checkout(DEFAULT_BRANCH);
812
+ expect(norm(await fs.readFile(path.join(dir, 'AGENTS.md'), 'utf8'))).toBe(theirs);
813
+ expect(await exists(dir, 'HEXIS.md')).toBe(true);
814
+ const ignore = norm(await fs.readFile(path.join(dir, '.bevelignore'), 'utf8'))
815
+ .split('\n')
816
+ .map((l) => l.trim());
817
+ // The configured file is hidden; the customer's shows.
818
+ expect(ignore).toContain('HEXIS.md');
819
+ expect(ignore).not.toContain('AGENTS.md');
820
+ });
821
+
822
+ it('removes the platform-written AGENTS.md — only its own header proves it ours', async () => {
823
+ // A knowledge base the platform seeded, whose admin then renames the guide.
824
+ await seedUpstream({ 'marker.txt': 'seeded' });
825
+ await makeRunner([new TemplateFilesStep(new NodeFs())]).runAll();
826
+ expect(await exists(await checkout(DEFAULT_BRANCH), 'AGENTS.md')).toBe(true);
827
+
828
+ configureKbLayout({ ...DEFAULT_KB_LAYOUT, agentsFile: 'HEXIS.md' });
829
+ await makeRunner([new TemplateFilesStep(new NodeFs())]).runAll();
830
+
831
+ // On EVERY protected branch: the step tops up each one, and a rename
832
+ // that only reached the default branch would leave the others carrying a
833
+ // stale platform guide under the customer's name.
834
+ for (const branch of PROTECTED) {
835
+ const dir = await checkout(branch);
836
+ expect(await exists(dir, 'AGENTS.md'), branch).toBe(false);
837
+ expect(await exists(dir, 'HEXIS.md'), branch).toBe(true);
838
+ const message = await git(dir, ['log', '--format=%B', '-1']);
839
+ expect(message, branch).toContain(
840
+ 'Remove the platform-written AGENTS.md — the agent guide is now HEXIS.md',
841
+ );
842
+ }
843
+ });
844
+
845
+ it('keeps an AGENTS.md it cannot prove it wrote, and says so', async () => {
846
+ configureKbLayout({ ...DEFAULT_KB_LAYOUT, agentsFile: 'HEXIS.md' });
847
+ // Edited by the customer: the managed header is gone, so it is theirs.
848
+ const edited = `${CUSTOMER_GUIDE}\nMentions HEXIS.md already.\n`;
849
+ await seedUpstream({ ...(await scaffoldWithoutGuide()), 'AGENTS.md': edited });
850
+ await makeRunner([new TemplateFilesStep(new NodeFs())]).runAll();
851
+
852
+ for (const branch of PROTECTED) {
853
+ const dir = await checkout(branch);
854
+ expect(norm(await fs.readFile(path.join(dir, 'AGENTS.md'), 'utf8')), branch).toBe(edited);
855
+ expect(await git(dir, ['log', '--format=%B', '-1']), branch).toContain('Keep AGENTS.md');
856
+ }
857
+ });
858
+
859
+ it('leaves a root AGENTS.md that is a symbolic link exactly where it points', async () => {
860
+ // Following the link would read the managed header at the other end and
861
+ // delete the customer's entry — or, with the pointer on, write our
862
+ // sentence into a file nobody asked us to edit.
863
+ configureKbLayout({ ...DEFAULT_KB_LAYOUT, agentsFile: 'HEXIS.md' });
864
+ await seedUpstream({ ...(await scaffoldWithoutGuide()), 'conventions/ours.md': await template('AGENTS.md') });
865
+ // Committed as a link, so every checkout the step makes carries one.
866
+ const seeding = await checkout(DEFAULT_BRANCH);
867
+ await fs.symlink('conventions/ours.md', path.join(seeding, 'AGENTS.md'));
868
+ await git(seeding, ['add', '-A']);
869
+ await git(seeding, ['commit', '-m', 'link AGENTS.md at our own file']);
870
+ await git(seeding, ['push', 'origin', DEFAULT_BRANCH]);
871
+
872
+ await makeRunner([new TemplateFilesStep(new NodeFs())]).runAll();
873
+
874
+ const dir = await checkout(DEFAULT_BRANCH);
875
+ expect((await fs.lstat(path.join(dir, 'AGENTS.md'))).isSymbolicLink()).toBe(true);
876
+ expect(await fs.readlink(path.join(dir, 'AGENTS.md'))).toBe('conventions/ours.md');
877
+ // The file at the other end is untouched — neither removed nor appended to.
878
+ expect(norm(await fs.readFile(path.join(dir, 'conventions/ours.md'), 'utf8'))).toBe(
879
+ norm(await template('AGENTS.md')),
880
+ );
881
+ });
882
+
883
+ it("keeps an operator's own AGENTS.md ignore rule while dropping the platform's", async () => {
884
+ configureKbLayout({ ...DEFAULT_KB_LAYOUT, agentsFile: 'HEXIS.md' });
885
+ const scaffold = await scaffoldWithoutGuide();
886
+ // Their own rule, written by hand, nowhere near the template's slot.
887
+ scaffold['.bevelignore'] = '# mine\nAGENTS.md\nMyStuff/\n';
888
+ scaffold['AGENTS.md'] = `${CUSTOMER_GUIDE}\nSee HEXIS.md.\n`;
889
+ await seedUpstream(scaffold);
890
+ await makeRunner([new TemplateFilesStep(new NodeFs())]).runAll();
891
+
892
+ const text = norm(
893
+ await fs.readFile(path.join(await checkout(DEFAULT_BRANCH), '.bevelignore'), 'utf8'),
894
+ );
895
+ expect(text).toContain('# mine\nAGENTS.md\nMyStuff/');
896
+ expect(text.split('\n').map((l) => l.trim())).toContain('HEXIS.md');
897
+ });
898
+
899
+ it("keeps an operator's rule that merely happens to sit under .gitattributes", async () => {
900
+ configureKbLayout({ ...DEFAULT_KB_LAYOUT, agentsFile: 'HEXIS.md' });
901
+ const scaffold = await scaffoldWithoutGuide();
902
+ // Hand-written, and by coincidence in the position the template's own
903
+ // rule used to occupy. One matching line above is not provenance — the
904
+ // block the template wrote is not there, so the rule is theirs.
905
+ scaffold['.bevelignore'] = '# ours\n.gitattributes\nAGENTS.md\nMyStuff/\n';
906
+ scaffold['AGENTS.md'] = `${CUSTOMER_GUIDE}\nSee HEXIS.md.\n`;
907
+ await seedUpstream(scaffold);
908
+ await makeRunner([new TemplateFilesStep(new NodeFs())]).runAll();
909
+
910
+ const text = norm(
911
+ await fs.readFile(path.join(await checkout(DEFAULT_BRANCH), '.bevelignore'), 'utf8'),
912
+ );
913
+ expect(text).toContain('# ours\n.gitattributes\nAGENTS.md\nMyStuff/');
914
+ expect(text.split('\n').map((l) => l.trim())).toContain('HEXIS.md');
915
+ });
916
+
917
+ it("drops the bare rule an earlier release seeded in the template's hygiene block", async () => {
918
+ configureKbLayout({ ...DEFAULT_KB_LAYOUT, agentsFile: 'HEXIS.md' });
919
+ const scaffold = await scaffoldWithoutGuide();
920
+ // The `.bevelignore` of a knowledge base seeded before the rule had a
921
+ // comment above it: bare, at the end of the template's own block.
922
+ scaffold['.bevelignore'] =
923
+ '# Repo hygiene files that clutter the tree without being node content.\n' +
924
+ '.gitignore\n.gitattributes\nAGENTS.md\nroles.yaml\n';
925
+ scaffold['AGENTS.md'] = `${CUSTOMER_GUIDE}\nSee HEXIS.md.\n`;
926
+ await seedUpstream(scaffold);
927
+ await makeRunner([new TemplateFilesStep(new NodeFs())]).runAll();
928
+
929
+ const lines = norm(
930
+ await fs.readFile(path.join(await checkout(DEFAULT_BRANCH), '.bevelignore'), 'utf8'),
931
+ )
932
+ .split('\n')
933
+ .map((l) => l.trim());
934
+ // The platform's line goes; the block above it is content and stays.
935
+ expect(lines).not.toContain('AGENTS.md');
936
+ expect(lines).toContain('.gitattributes');
937
+ expect(lines).toContain('.gitignore');
938
+ expect(lines).toContain('HEXIS.md');
939
+ });
940
+
941
+ describe('the pointer in the customer\'s own AGENTS.md', () => {
942
+ /** Boot once with the guide renamed, against a root `AGENTS.md` holding `theirs`. */
943
+ async function bootWith(theirs: string | null, link = true): Promise<string> {
944
+ configureKbLayout({ ...DEFAULT_KB_LAYOUT, agentsFile: 'HEXIS.md' });
945
+ const scaffold = await scaffoldWithoutGuide();
946
+ if (theirs !== null) scaffold['AGENTS.md'] = theirs;
947
+ await seedUpstream(scaffold);
948
+ await makeRunner([new TemplateFilesStep(new NodeFs(), [], () => link)]).runAll();
949
+ return checkout(DEFAULT_BRANCH);
950
+ }
951
+
952
+ const SENTENCE = agentsFilePointerSentence('HEXIS.md');
953
+
954
+ it('appends the sentence when the guide is not mentioned, and says so', async () => {
955
+ const dir = await bootWith(CUSTOMER_GUIDE);
956
+ const text = norm(await fs.readFile(path.join(dir, 'AGENTS.md'), 'utf8'));
957
+ // Their bytes, then a blank line, then exactly the shared sentence.
958
+ expect(text).toBe(`${CUSTOMER_GUIDE.replace(/\n+$/, '')}\n\n${SENTENCE}\n`);
959
+ expect(await git(dir, ['log', '--format=%B', '-1'])).toContain(
960
+ 'Add a pointer to HEXIS.md at the end of AGENTS.md',
961
+ );
962
+ });
963
+
964
+ /**
965
+ * The sentence spells a punctuated name twice and neither spelling is
966
+ * the raw one — the label is escaped, the destination percent-encoded.
967
+ * A second boot must still see the guide as mentioned, or it appends
968
+ * another copy, and the boot after that a third.
969
+ */
970
+ it('appends once and only once, even when the name is nothing but punctuation', async () => {
971
+ const guide = 'Our [Agent] Guide (v2).md';
972
+ configureKbLayout({ ...DEFAULT_KB_LAYOUT, agentsFile: guide });
973
+ await seedUpstream({ ...(await scaffoldWithoutGuide()), 'AGENTS.md': CUSTOMER_GUIDE });
974
+ const boot = () => makeRunner([new TemplateFilesStep(new NodeFs())]).runAll();
975
+
976
+ await boot();
977
+ const once = norm(await fs.readFile(path.join(await checkout(DEFAULT_BRANCH), 'AGENTS.md'), 'utf8'));
978
+ expect(once).toBe(
979
+ `${CUSTOMER_GUIDE.replace(/\n+$/, '')}\n\n${agentsFilePointerSentence(guide)}\n`,
980
+ );
981
+
982
+ const head = (await git(await checkout(DEFAULT_BRANCH), ['rev-parse', 'HEAD'])).trim();
983
+ await boot();
984
+ const after = await checkout(DEFAULT_BRANCH);
985
+ expect(norm(await fs.readFile(path.join(after, 'AGENTS.md'), 'utf8'))).toBe(once);
986
+ // And the second boot committed nothing at all.
987
+ expect((await git(after, ['rev-parse', 'HEAD'])).trim()).toBe(head);
988
+ });
989
+
990
+ /**
991
+ * The guide's name is a setting, and a setting can be changed twice. The
992
+ * sentence the last rename wrote points at a file that is no longer
993
+ * there, and the new name is nowhere in the text — so "is it mentioned?"
994
+ * says no and an unaimed sentence would be joined by a second one.
995
+ */
996
+ it('aims the sentence it already wrote at the new guide, rather than adding another', async () => {
997
+ configureKbLayout({ ...DEFAULT_KB_LAYOUT, agentsFile: 'HEXIS.md' });
998
+ await seedUpstream({ ...(await scaffoldWithoutGuide()), 'AGENTS.md': CUSTOMER_GUIDE });
999
+ await makeRunner([new TemplateFilesStep(new NodeFs())]).runAll();
1000
+ expect(norm(await fs.readFile(path.join(await checkout(DEFAULT_BRANCH), 'AGENTS.md'), 'utf8'))).toContain(
1001
+ agentsFilePointerSentence('HEXIS.md'),
1002
+ );
1003
+
1004
+ // The admin renames the guide a second time.
1005
+ configureKbLayout({ ...DEFAULT_KB_LAYOUT, agentsFile: 'GUIDE.md' });
1006
+ await makeRunner([new TemplateFilesStep(new NodeFs())]).runAll();
1007
+
1008
+ const dir = await checkout(DEFAULT_BRANCH);
1009
+ const text = norm(await fs.readFile(path.join(dir, 'AGENTS.md'), 'utf8'));
1010
+ // Their bytes, then ONE sentence, pointing at the guide that exists.
1011
+ expect(text).toBe(
1012
+ `${CUSTOMER_GUIDE.replace(/\n+$/, '')}\n\n${agentsFilePointerSentence('GUIDE.md')}\n`,
1013
+ );
1014
+ expect(text).not.toContain('HEXIS.md');
1015
+ expect(await git(dir, ['log', '--format=%B', '-1'])).toContain(
1016
+ 'Point the sentence in AGENTS.md at GUIDE.md',
1017
+ );
1018
+ });
1019
+
1020
+ it('writes nothing when the name appears anywhere in the text, in any wording', async () => {
1021
+ const theirs = `${CUSTOMER_GUIDE}\nThe platform keeps its own notes in HEXIS.md; read those too.\n`;
1022
+ const dir = await bootWith(theirs);
1023
+ expect(norm(await fs.readFile(path.join(dir, 'AGENTS.md'), 'utf8'))).toBe(theirs);
1024
+ });
1025
+
1026
+ it('writes nothing while the setting is off', async () => {
1027
+ const dir = await bootWith(CUSTOMER_GUIDE, false);
1028
+ expect(norm(await fs.readFile(path.join(dir, 'AGENTS.md'), 'utf8'))).toBe(CUSTOMER_GUIDE);
1029
+ });
1030
+
1031
+ it('never creates an AGENTS.md for the pointer', async () => {
1032
+ const dir = await bootWith(null);
1033
+ expect(await exists(dir, 'AGENTS.md')).toBe(false);
1034
+ expect(await exists(dir, 'HEXIS.md')).toBe(true);
1035
+ });
1036
+
1037
+ it('never appends to a file the platform wrote — it removes that one instead', async () => {
1038
+ const dir = await bootWith(await template('AGENTS.md'));
1039
+ expect(await exists(dir, 'AGENTS.md')).toBe(false);
1040
+ });
1041
+
1042
+ it('does nothing at all under the default name', async () => {
1043
+ const theirs = `${CUSTOMER_GUIDE}`;
1044
+ // The default guide IS AGENTS.md, so the platform owns that file and
1045
+ // the pointer question never arises: the drifted copy is replaced.
1046
+ await seedUpstream({ ...(await fullScaffold()), 'AGENTS.md': theirs });
1047
+ await makeRunner([new TemplateFilesStep(new NodeFs())]).runAll();
1048
+ const dir = await checkout(DEFAULT_BRANCH);
1049
+ const text = norm(await fs.readFile(path.join(dir, 'AGENTS.md'), 'utf8'));
1050
+ expect(text).toBe(norm(await template('AGENTS.md')));
1051
+ expect(text).not.toContain(agentsFilePointerSentence('AGENTS.md'));
1052
+ });
1053
+ });
1054
+ });
213
1055
  });
214
1056
 
215
1057
  describe('RolesYamlStep', () => {
216
1058
  it('generates roles.yaml with the configured admins on every protected branch', async () => {
217
1059
  await seedUpstream({ 'marker.txt': 'seeded' });
218
- await makeRunner([new RolesYamlStep(['admin@example.com'])]).runAll();
1060
+ await makeRunner([new RolesYamlStep(new NodeFs(), ['admin@example.com'])]).runAll();
219
1061
 
220
1062
  for (const b of PROTECTED) {
221
1063
  const dir = await checkout(b);
@@ -233,7 +1075,7 @@ describe('RolesYamlStep', () => {
233
1075
  // skip-if-present check, reporting success over a KB whose access roster
234
1076
  // cannot be read. Fail closed instead.
235
1077
  await seedUpstream({ 'roles.yaml/placeholder.txt': 'squatter' });
236
- await expect(makeRunner([new RolesYamlStep(['admin@example.com'])]).runAll()).rejects.toThrow(
1078
+ await expect(makeRunner([new RolesYamlStep(new NodeFs(), ['admin@example.com'])]).runAll()).rejects.toThrow(
237
1079
  /"roles\.yaml" on branch "current-company-state" exists but is not a regular file \(directory\)/,
238
1080
  );
239
1081
  });
@@ -241,7 +1083,7 @@ describe('RolesYamlStep', () => {
241
1083
  it('declares a skip when the file is missing and no admins are configured', async () => {
242
1084
  await seedUpstream({ 'marker.txt': 'seeded' });
243
1085
  const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
244
- await makeRunner([new RolesYamlStep([])]).runAll(); // resolves — a declared skip, not a failure
1086
+ await makeRunner([new RolesYamlStep(new NodeFs(), [])]).runAll(); // resolves — a declared skip, not a failure
245
1087
 
246
1088
  expect(warn.mock.calls.some((c) => String(c[0]).includes('roles-yaml: skipped'))).toBe(true);
247
1089
  const dir = await checkout(DEFAULT_BRANCH);
@@ -287,16 +1129,437 @@ describe('buildSeedTree', () => {
287
1129
 
288
1130
  const dest = path.join(root, 'seed-dest');
289
1131
  await fs.mkdir(dest, { recursive: true });
290
- const generated = await buildSeedTree(templateDir, [], ['admin@example.com'])(dest);
1132
+ const generated = await buildSeedTree(new NodeFs(),templateDir, [], ['admin@example.com'])(dest);
291
1133
 
292
1134
  expect(await exists(dest, '.git')).toBe(false);
293
1135
  expect(await exists(dest, 'docs/.git')).toBe(false);
294
1136
  expect(await fs.readFile(path.join(dest, 'docs/guide.md'), 'utf8')).toBe('guide');
295
1137
  expect(await fs.readFile(path.join(dest, 'access.md'), 'utf8')).toBe('policy');
296
1138
  // The generated paths — what the runner force-adds past a template .gitignore.
297
- expect(generated.sort()).toEqual(['KnowledgeBase/.gitkeep', 'Plugins/.gitkeep', 'roles.yaml']);
1139
+ expect(generated.sort()).toEqual(['KnowledgeBase/.gitkeep', 'Plugins/.gitkeep', 'Skills/.gitkeep', 'roles.yaml']);
298
1140
  expect(await exists(dest, 'roles.yaml')).toBe(true);
299
1141
  });
1142
+
1143
+ it('seeds the layout in effect when it seeds, not the one it was composed under', async () => {
1144
+ const templateDir = path.join(root, 'seed-template-layout');
1145
+ await fs.mkdir(templateDir, { recursive: true });
1146
+ await fs.writeFile(path.join(templateDir, 'access.md'), 'policy', 'utf8');
1147
+ // Composed at boot, under the defaults…
1148
+ const seed = buildSeedTree(new NodeFs(), templateDir, [], ['admin@example.com']);
1149
+ // …and run after the completing save applied the admin's names.
1150
+ configureKbLayout({ knowledgeBaseDir: 'Docs', skillsDir: 'skills', pluginsDir: 'Plugins' });
1151
+ const dest = path.join(root, 'seed-dest-layout');
1152
+ await fs.mkdir(dest, { recursive: true });
1153
+ const generated = await seed(dest);
1154
+ expect(generated.sort()).toEqual(['Docs/.gitkeep', 'Plugins/.gitkeep', 'roles.yaml', 'skills/.gitkeep']);
1155
+ });
1156
+
1157
+ /**
1158
+ * An empty remote seeded by a deployment that renamed its guide. Done in the
1159
+ * SEED rather than left to the top-up step, which would otherwise lay down
1160
+ * `AGENTS.md` and delete it one commit later — two commits saying opposite
1161
+ * things about a knowledge base nobody has used yet.
1162
+ */
1163
+ it('seeds the packaged guide under the configured name, and no AGENTS.md beside it', async () => {
1164
+ configureKbLayout({ ...DEFAULT_KB_LAYOUT, agentsFile: 'HEXIS.md' });
1165
+ const dest = path.join(root, 'seed-dest-guide');
1166
+ await fs.mkdir(dest, { recursive: true });
1167
+ await buildSeedTree(new NodeFs(), TEMPLATE_DIR, [], ['admin@example.com'])(dest);
1168
+ expect(await exists(dest, 'AGENTS.md')).toBe(false);
1169
+ const guide = norm(await fs.readFile(path.join(dest, 'HEXIS.md'), 'utf8'));
1170
+ expect(guide).toContain('`HEXIS.md`');
1171
+ expect(guide).not.toContain('{{');
1172
+ // And the ignore file hides the name it actually seeded.
1173
+ const ignore = norm(await fs.readFile(path.join(dest, '.bevelignore'), 'utf8'))
1174
+ .split('\n')
1175
+ .map((l) => l.trim());
1176
+ expect(ignore).toContain('HEXIS.md');
1177
+ expect(ignore).not.toContain('AGENTS.md');
1178
+ });
1179
+
1180
+ it('lets the managed guide win a template that carries a root file of the same name', async () => {
1181
+ // A custom KB_TEMPLATE_DIR that happens to ship `HEXIS.md` at its root, on
1182
+ // a deployment that named its guide `HEXIS.md`: two walk entries, one
1183
+ // destination. Whichever the walk reached last used to decide what an
1184
+ // empty deployment was seeded with. The managed guide decides.
1185
+ const customTemplate = path.join(root, 'custom-template-guide-clash');
1186
+ await fs.cp(TEMPLATE_DIR, customTemplate, { recursive: true });
1187
+ await fs.writeFile(path.join(customTemplate, 'HEXIS.md'), '# Not the platform guide\n', 'utf8');
1188
+
1189
+ configureKbLayout({ ...DEFAULT_KB_LAYOUT, agentsFile: 'HEXIS.md' });
1190
+ const dest = path.join(root, 'seed-dest-guide-clash');
1191
+ await fs.mkdir(dest, { recursive: true });
1192
+ await buildSeedTree(new NodeFs(), customTemplate, [], ['admin@example.com'])(dest);
1193
+
1194
+ const guide = norm(await fs.readFile(path.join(dest, 'HEXIS.md'), 'utf8'));
1195
+ expect(guide).toContain('`HEXIS.md`');
1196
+ expect(guide).not.toContain('Not the platform guide');
1197
+ expect(await exists(dest, 'AGENTS.md')).toBe(false);
1198
+ });
1199
+
1200
+ it('names a template that is not there — a missing directory is a broken build, not an empty seed', async () => {
1201
+ const dest = path.join(root, 'seed-dest-missing');
1202
+ await fs.mkdir(dest, { recursive: true });
1203
+ await expect(buildSeedTree(new NodeFs(),path.join(root, 'no-such-template'), [], ['admin@example.com'])(dest)).rejects.toThrow(
1204
+ /KB template ".*no-such-template" is not a directory/,
1205
+ );
1206
+ });
1207
+
1208
+ it('seeds a file the template links to, and refuses a link to anything else', async () => {
1209
+ const templateDir = path.join(root, 'seed-template-links');
1210
+ const elsewhere = path.join(root, 'elsewhere-template');
1211
+ await fs.mkdir(path.join(templateDir, 'docs'), { recursive: true });
1212
+ await fs.mkdir(elsewhere, { recursive: true });
1213
+ await fs.writeFile(path.join(elsewhere, 'shared.md'), 'shared', 'utf8');
1214
+ await fs.writeFile(path.join(templateDir, 'access.md'), 'policy', 'utf8');
1215
+ await fs.symlink(path.join(elsewhere, 'shared.md'), path.join(templateDir, 'docs', 'linked.md'), 'file');
1216
+
1217
+ const dest = path.join(root, 'seed-dest-links');
1218
+ await fs.mkdir(dest, { recursive: true });
1219
+ await buildSeedTree(new NodeFs(),templateDir, [], ['admin@example.com'])(dest);
1220
+ // The link's CONTENT, under the link's own name, as a real file.
1221
+ expect(await fs.readFile(path.join(dest, 'docs/linked.md'), 'utf8')).toBe('shared');
1222
+ expect((await fs.lstat(path.join(dest, 'docs/linked.md'))).isSymbolicLink()).toBe(false);
1223
+
1224
+ // A link to nothing is a broken template, named as such…
1225
+ await fs.symlink(path.join(root, 'nowhere.md'), path.join(templateDir, 'dangling.md'), 'file');
1226
+ const dest2 = path.join(root, 'seed-dest-links-2');
1227
+ await fs.mkdir(dest2, { recursive: true });
1228
+ await expect(buildSeedTree(new NodeFs(),templateDir, [], ['admin@example.com'])(dest2)).rejects.toThrow(
1229
+ /KB template entry "dangling.md" links to nothing/,
1230
+ );
1231
+ // …and so is a link to a folder, named for what it is.
1232
+ await fs.rm(path.join(templateDir, 'dangling.md'));
1233
+ await fs.symlink(elsewhere, path.join(templateDir, 'dirlink'), process.platform === 'win32' ? 'junction' : 'dir');
1234
+ const dest3 = path.join(root, 'seed-dest-links-3');
1235
+ await fs.mkdir(dest3, { recursive: true });
1236
+ await expect(buildSeedTree(new NodeFs(),templateDir, [], ['admin@example.com'])(dest3)).rejects.toThrow(
1237
+ /KB template entry "dirlink" links to a directory/,
1238
+ );
1239
+ });
1240
+
1241
+ it('seeds a template file whose name collides with Object.prototype', async () => {
1242
+ // The packable-spelling lookup is keyed by FILENAME. Asked about a file
1243
+ // called `constructor` an object map answers from the prototype and hands
1244
+ // a function to path.join, failing the seed of a whole knowledge base
1245
+ // with a type error that names nothing.
1246
+ const templateDir = path.join(root, 'proto-template');
1247
+ await fs.mkdir(templateDir, { recursive: true });
1248
+ await fs.writeFile(path.join(templateDir, 'access.md'), 'policy', 'utf8');
1249
+ await fs.writeFile(path.join(templateDir, 'constructor'), 'not a function', 'utf8');
1250
+ await fs.writeFile(path.join(templateDir, 'toString'), 'nor this', 'utf8');
1251
+
1252
+ const dest = path.join(root, 'proto-dest');
1253
+ await fs.mkdir(dest, { recursive: true });
1254
+ await buildSeedTree(new NodeFs(), templateDir, [], ['admin@example.com'])(dest);
1255
+ expect(await fs.readFile(path.join(dest, 'constructor'), 'utf8')).toBe('not a function');
1256
+ expect(await fs.readFile(path.join(dest, 'toString'), 'utf8')).toBe('nor this');
1257
+ });
1258
+ });
1259
+
1260
+ describe('PluginManifestsStep', () => {
1261
+ it('writes plugin.json into legacy plugin folders on every branch, and leaves scopes and real plugins alone', async () => {
1262
+ const scaffold = await fullScaffold();
1263
+ await seedUpstream({
1264
+ ...scaffold,
1265
+ // Legacy shapes: access.md only; mcp.json only; a bare skill tree.
1266
+ 'Plugins/GTM/access.md': '---\n---\nread:\n - everyone\n',
1267
+ 'Plugins/GTM/outreach/SKILL.md': '---\ndescription: x\n---\n',
1268
+ 'Plugins/Servers/mcp.json': '{"mcpServers":{}}',
1269
+ 'Plugins/Bare/deploy/SKILL.md': '---\ndescription: y\n---\n',
1270
+ // Already a plugin, both shapes.
1271
+ 'Plugins/Modern/plugin.json': '{"name":"modern"}',
1272
+ 'Plugins/functional/cluster/example/plugin.bundle.json': '{"name":"example"}',
1273
+ // A scope with rules of its own above a bundle: not a plugin.
1274
+ 'Plugins/functional/access.md': '---\n---\nread:\n - everyone\n',
1275
+ // Nothing plugin-shaped at all.
1276
+ 'Plugins/notes/README.md': 'just a folder',
1277
+ // A legacy plugin whose vendored dependency ships a manifest: the walk
1278
+ // does not enter node_modules, so the folder is the plugin, not a
1279
+ // grouping folder over one.
1280
+ 'Plugins/Vendored/access.md': '---\n---\nread:\n - everyone\n',
1281
+ 'Plugins/Vendored/node_modules/some-pkg/plugin.json': '{"name":"some-pkg"}',
1282
+ // A plain folder whose only SKILL.md is vendored: not a plugin either.
1283
+ 'Plugins/deps/node_modules/some-pkg/SKILL.md': '---\ndescription: z\n---\n',
1284
+ });
1285
+
1286
+ await makeRunner([new PluginManifestsStep(new NodeFs())]).runAll();
1287
+
1288
+ for (const branch of PROTECTED) {
1289
+ const dir = await checkout(branch);
1290
+ for (const legacy of ['GTM', 'Servers', 'Bare', 'Vendored']) {
1291
+ const manifest = JSON.parse(await fs.readFile(path.join(dir, `Plugins/${legacy}/plugin.json`), 'utf8'));
1292
+ expect(manifest.name).toBe(legacy.toLowerCase());
1293
+ }
1294
+ expect(await exists(dir, 'Plugins/functional/plugin.json')).toBe(false);
1295
+ expect(await exists(dir, 'Plugins/notes/plugin.json')).toBe(false);
1296
+ expect(await exists(dir, 'Plugins/deps/plugin.json')).toBe(false);
1297
+ expect(await fs.readFile(path.join(dir, 'Plugins/Modern/plugin.json'), 'utf8')).toBe('{"name":"modern"}');
1298
+ }
1299
+ const dir = await checkout(DEFAULT_BRANCH);
1300
+ const log = (await git(dir, ['log', '-1', '--format=%B'])).trim();
1301
+ expect(log).toContain('Add plugin manifests to 4 legacy plugin folders');
1302
+ expect(log).toContain('Plugins/GTM: plugin.json written');
1303
+
1304
+ // Idempotent: nothing left to write on the next boot.
1305
+ await makeRunner([new PluginManifestsStep(new NodeFs())]).runAll();
1306
+ const again = await checkout(DEFAULT_BRANCH);
1307
+ expect((await git(again, ['rev-list', '--count', 'HEAD'])).trim()).toBe('2'); // init + one migration commit
1308
+ });
1309
+ });
1310
+
1311
+ describe('PluginDisplayNamesStep', () => {
1312
+ /**
1313
+ * The one-time repair behind the display-name rule: a manifest is now the
1314
+ * only thing any reader consults for what a plugin is called, so every
1315
+ * manifest written under the old rule — which stored the field only when
1316
+ * the folder was spelled differently from the identifier — has to be told
1317
+ * what its plugin was already called, or it would appear to rename itself
1318
+ * on the boot that introduces the rule.
1319
+ */
1320
+ it('writes the folder\'s spelling into the manifests that lack the field, and touches nothing else', async () => {
1321
+ const scaffold = await fullScaffold();
1322
+ await seedUpstream({
1323
+ ...scaffold,
1324
+ // Spelled differently from its identifier and saying nothing about it:
1325
+ // exactly the plugin that would start showing `sales-team`.
1326
+ 'Plugins/Sales Team/plugin.json': '{\n "name": "sales-team",\n "version": "1.2.0"\n}\n',
1327
+ 'Plugins/Sales Team/access.md': '---\n---\nread:\n - everyone\n',
1328
+ // Nested just as deep as discovery looks.
1329
+ 'Plugins/Teams/EU Field/plugin.json': '{"name":"eu-field"}',
1330
+ // Already spelled as its identifier: called the same thing under both
1331
+ // rules, so there is nothing the folder knows that the manifest does not.
1332
+ 'Plugins/imported-tools/plugin.json': '{"name":"imported-tools"}',
1333
+ // Already carries the field — the author's own answer, never overwritten,
1334
+ // even though the folder disagrees with both of its names.
1335
+ 'Plugins/Ops Desk/plugin.json': '{"name":"ops","displayName":"Operations"}',
1336
+ // A bundle: a foreign repository's file this platform reads, never writes.
1337
+ 'Plugins/functional/cluster/example/plugin.bundle.json': '{"name":"example"}',
1338
+ });
1339
+
1340
+ await makeRunner([new PluginDisplayNamesStep(new NodeFs())]).runAll();
1341
+
1342
+ for (const branch of PROTECTED) {
1343
+ const dir = await checkout(branch);
1344
+ // Backfilled from the folder — what the plugin was called the moment
1345
+ // before the boot — with everything else in the manifest kept, and the
1346
+ // field where the renderer puts it.
1347
+ const sales = await readJson(dir, 'Plugins/Sales Team/plugin.json');
1348
+ expect(sales).toEqual({ name: 'sales-team', displayName: 'Sales Team', version: '1.2.0' });
1349
+ expect(Object.keys(sales)).toEqual(['name', 'displayName', 'version']);
1350
+ expect(await readJson(dir, 'Plugins/Teams/EU Field/plugin.json')).toEqual({
1351
+ name: 'eu-field',
1352
+ displayName: 'EU Field',
1353
+ });
1354
+ // Untouched, byte for byte.
1355
+ expect(await fs.readFile(path.join(dir, 'Plugins/imported-tools/plugin.json'), 'utf8')).toBe(
1356
+ '{"name":"imported-tools"}',
1357
+ );
1358
+ expect(await fs.readFile(path.join(dir, 'Plugins/Ops Desk/plugin.json'), 'utf8')).toBe(
1359
+ '{"name":"ops","displayName":"Operations"}',
1360
+ );
1361
+ expect(await fs.readFile(path.join(dir, 'Plugins/functional/cluster/example/plugin.bundle.json'), 'utf8')).toBe(
1362
+ '{"name":"example"}',
1363
+ );
1364
+ }
1365
+
1366
+ const dir = await checkout(DEFAULT_BRANCH);
1367
+ const log = (await git(dir, ['log', '-1', '--format=%B'])).trim();
1368
+ expect(log).toContain('Record the display names of 2 plugins in their manifests');
1369
+ expect(log).toContain('Plugins/Sales Team: displayName "Sales Team"');
1370
+
1371
+ // Idempotent: every manifest it would touch now carries the field.
1372
+ await makeRunner([new PluginDisplayNamesStep(new NodeFs())]).runAll();
1373
+ const again = await checkout(DEFAULT_BRANCH);
1374
+ expect((await git(again, ['rev-list', '--count', 'HEAD'])).trim()).toBe('2'); // init + one backfill commit
1375
+ });
1376
+
1377
+ /**
1378
+ * The three shapes the plain "lacks the field, folder differs" reading gets
1379
+ * wrong: a field that is present and says nothing, a manifest naming the
1380
+ * folder's own spelling without being an identifier, and a `plugin.json`
1381
+ * that is not this platform's file to write.
1382
+ */
1383
+ it('repairs a blank field, leaves a manifest that already shows its folder, and never writes through a link', async () => {
1384
+ const scaffold = await fullScaffold();
1385
+ await seedUpstream(
1386
+ {
1387
+ ...scaffold,
1388
+ // Present but BLANK. The new reader falls through it to `name` where
1389
+ // the old one fell through to the folder, so leaving it be is leaving
1390
+ // the rename this step exists to prevent: blank is absent, and repaired.
1391
+ 'Plugins/Growth Team/plugin.json': '{"name":"growth-team","displayName":" "}',
1392
+ // A `name` that is no identifier but IS the folder's spelling: the
1393
+ // identity is the slug `my-plugin`, and what people see is already
1394
+ // "My Plugin". The folder knows nothing the manifest does not.
1395
+ 'Plugins/My Plugin/plugin.json': '{"name":"My Plugin"}',
1396
+ // An own `__proto__` key — `JSON.parse` makes it one. Copied as DATA:
1397
+ // assignment would have set the object's prototype instead, dropping
1398
+ // the field and letting a prototype `displayName` stand in for the
1399
+ // one being written.
1400
+ 'Plugins/Odd One/plugin.json': '{"name":"odd-one","__proto__":{"displayName":"from the prototype"}}',
1401
+ // A bundle makes this folder a plugin; the `plugin.json` beside it is
1402
+ // a LINK, which discovery does not count as a native manifest.
1403
+ 'Plugins/Kit Pro/plugin.bundle.json': '{"name":"kit"}',
1404
+ 'vendor-manifest.json': '{"name":"kit"}',
1405
+ },
1406
+ { 'Plugins/Kit Pro/plugin.json': '../../vendor-manifest.json' },
1407
+ );
1408
+
1409
+ await makeRunner([new PluginDisplayNamesStep(new NodeFs())]).runAll();
1410
+
1411
+ const dir = await checkout(DEFAULT_BRANCH);
1412
+ expect(await readJson(dir, 'Plugins/Growth Team/plugin.json')).toEqual({
1413
+ name: 'growth-team',
1414
+ displayName: 'Growth Team',
1415
+ });
1416
+ // Untouched, byte for byte.
1417
+ expect(await fs.readFile(path.join(dir, 'Plugins/My Plugin/plugin.json'), 'utf8')).toBe('{"name":"My Plugin"}');
1418
+ expect(await fs.readFile(path.join(dir, 'Plugins/Odd One/plugin.json'), 'utf8')).toBe(
1419
+ '{\n "name": "odd-one",\n "displayName": "Odd One",\n "__proto__": {\n "displayName": "from the prototype"\n }\n}\n',
1420
+ );
1421
+ // Still a link, and the file it points at is still the vendor's own.
1422
+ expect((await fs.lstat(path.join(dir, 'Plugins/Kit Pro/plugin.json'))).isSymbolicLink()).toBe(true);
1423
+ expect(await fs.readFile(path.join(dir, 'vendor-manifest.json'), 'utf8')).toBe('{"name":"kit"}');
1424
+
1425
+ const log = (await git(dir, ['log', '-1', '--format=%B'])).trim();
1426
+ expect(log).toContain('Record the display names of 2 plugins in their manifests');
1427
+ expect(log).toContain('Plugins/Growth Team: displayName "Growth Team"');
1428
+ expect(log).not.toContain('Kit Pro');
1429
+
1430
+ // Idempotent over all of it: the next boot finds nothing to write.
1431
+ await makeRunner([new PluginDisplayNamesStep(new NodeFs())]).runAll();
1432
+ const again = await checkout(DEFAULT_BRANCH);
1433
+ expect((await git(again, ['rev-list', '--count', 'HEAD'])).trim()).toBe('2'); // init + one backfill commit
1434
+ });
1435
+
1436
+ it('leaves a plugin the manifests step just made alone — that manifest already says it', async () => {
1437
+ const scaffold = await fullScaffold();
1438
+ await seedUpstream({
1439
+ ...scaffold,
1440
+ // Legacy: no manifest at all. The manifests step renders one, which the
1441
+ // renderer has already given both names; the backfill then finds nothing.
1442
+ 'Plugins/Sales Team/access.md': '---\n---\nread:\n - everyone\n',
1443
+ });
1444
+
1445
+ await makeRunner([new PluginManifestsStep(new NodeFs()), new PluginDisplayNamesStep(new NodeFs())]).runAll();
1446
+
1447
+ const dir = await checkout(DEFAULT_BRANCH);
1448
+ expect(await readJson(dir, 'Plugins/Sales Team/plugin.json')).toEqual({
1449
+ $schema: 'https://agent-plugins.org/schemas/1.0.0/plugin.schema.json',
1450
+ name: 'sales-team',
1451
+ displayName: 'Sales Team',
1452
+ });
1453
+ // ONE commit — the backfill declared nothing of its own to caption.
1454
+ const log = (await git(dir, ['log', '-1', '--format=%B'])).trim();
1455
+ expect(log).toContain('Add plugin manifests to a legacy plugin folder');
1456
+ expect(log).not.toContain('Record the display name');
1457
+ });
1458
+ });
1459
+
1460
+ describe('PersonalSpacesStep', () => {
1461
+ // A personal folder as the previous template seeded it: the owner's grants
1462
+ // and nothing else — private only while nothing above grants read.
1463
+ const LEGACY_PERSONAL =
1464
+ '---\nread:\n - Ali Vega <ali@x.io>\nwrite:\n - Ali Vega <ali@x.io>\nowner:\n - Ali Vega <ali@x.io>\n---\nread: []\n';
1465
+
1466
+ it("adds `deny everyone` to every personal space's read rules on every branch, keeping the rest, once", async () => {
1467
+ const scaffold = await fullScaffold();
1468
+ const seed = await seedUpstream({
1469
+ ...scaffold,
1470
+ 'Plugins/personal-u1/plugin.json': '{"name":"personal-u1"}',
1471
+ 'Plugins/personal-u1/access.md': LEGACY_PERSONAL,
1472
+ // A space someone already closed (its folder rules stay; only the
1473
+ // file's own block, which said nothing, gets the same statement), one
1474
+ // its owner opened on purpose (a denial beside that grant would be
1475
+ // overruled and read as a contradiction — and the file stays open
1476
+ // with the folder), and a shared plugin: untouched.
1477
+ 'Plugins/personal-u2/access.md': '---\nread: []\n---\nread:\n - deny everyone\n - Bo <bo@x.io>\n',
1478
+ 'Plugins/personal-u3/access.md': '---\nread: []\n---\nread:\n - everyone\nowner:\n - Cy <cy@x.io>\n',
1479
+ // Entries that say nothing about READ — a denial under write — leave the
1480
+ // space open to an inherited `read: everyone`, so it is closed like any
1481
+ // other.
1482
+ 'Plugins/personal-u4/access.md': '---\nread: []\n---\nwrite:\n - deny everyone\nowner:\n - Di <di@x.io>\n',
1483
+ // The old seed's frontmatter grants are carried into the body ONLY where
1484
+ // the body has no word on that person: a denial someone wrote there
1485
+ // stands, and is not overridden by the older grant.
1486
+ 'Plugins/personal-u5/access.md': '---\nread:\n - Ed <ed@x.io>\nowner:\n - Ed <ed@x.io>\n---\nread:\n - deny Ed <ed@x.io>\n',
1487
+ // A folder whose read denial is overruled by `write: everyone` (a
1488
+ // same-scope grant wins, and write folds into read) is OPEN: the file
1489
+ // stays open with it, and nothing is written.
1490
+ 'Plugins/personal-u6/access.md': '---\nread: []\n---\nread:\n - deny everyone\nwrite:\n - everyone\nowner:\n - Fy <fy@x.io>\n',
1491
+ // `download: everyone` is a deliberate opening now that download folds
1492
+ // into read: the space is already settled OPEN, so nothing is written —
1493
+ // the same treatment `write: everyone` gets above.
1494
+ 'Plugins/personal-u7/access.md': '---\nread: []\n---\ndownload:\n - everyone\nowner:\n - Gi <gi@x.io>\n',
1495
+ 'Plugins/GTM/plugin.json': '{"name":"gtm"}',
1496
+ 'Plugins/GTM/access.md': '---\nread:\n - everyone\n---\nread:\n - Ali Vega <ali@x.io>\n',
1497
+ });
1498
+ // A draft carrying a personal space too: the step visits every branch,
1499
+ // not only the protected ones.
1500
+ await git(seed, ['checkout', '-b', 'ali/draft']);
1501
+ await git(seed, ['push', 'origin', 'ali/draft']);
1502
+
1503
+ await makeRunner([new PersonalSpacesStep(new NodeFs())]).runAll();
1504
+
1505
+ for (const branch of [...PROTECTED, 'ali/draft']) {
1506
+ const dir = await checkout(branch);
1507
+ const closed = norm(await fs.readFile(path.join(dir, 'Plugins/personal-u1/access.md'), 'utf8'));
1508
+ // The body — which governs the folder — now denies everyone (Admin
1509
+ // included: nobody is named) and carries the owner's grants so they
1510
+ // can still read, write and own their space; the old seed's
1511
+ // frontmatter grants stay, joined by the same denial, so the file says
1512
+ // of itself what the folder says.
1513
+ expect(closed).toBe(
1514
+ '---\nread:\n - Ali Vega <ali@x.io>\n - deny everyone\nwrite:\n - Ali Vega <ali@x.io>\nowner:\n - Ali Vega <ali@x.io>\n---\n' +
1515
+ 'read:\n - Ali Vega <ali@x.io>\n - deny everyone\n\nwrite:\n - Ali Vega <ali@x.io>\nowner:\n - Ali Vega <ali@x.io>',
1516
+ );
1517
+ // Already closed: the folder rules are untouched, the empty file block
1518
+ // now states the same — the denial, then the people the folder admits.
1519
+ expect(norm(await fs.readFile(path.join(dir, 'Plugins/personal-u2/access.md'), 'utf8'))).toBe(
1520
+ '---\nread:\n - deny everyone\n - Bo <bo@x.io>\n---\nread:\n - deny everyone\n - Bo <bo@x.io>\n',
1521
+ );
1522
+ expect(norm(await fs.readFile(path.join(dir, 'Plugins/personal-u3/access.md'), 'utf8'))).toBe(
1523
+ '---\nread: []\n---\nread:\n - everyone\nowner:\n - Cy <cy@x.io>\n',
1524
+ );
1525
+ const u4 = norm(await fs.readFile(path.join(dir, 'Plugins/personal-u4/access.md'), 'utf8'));
1526
+ expect(u4.slice(0, u4.indexOf('\n---\n', 4))).toBe('---\nread:\n - deny everyone\n - Di <di@x.io>');
1527
+ expect(u4.trimEnd().endsWith('\nread:\n - deny everyone')).toBe(true);
1528
+ expect(u4).toContain('write:\n - deny everyone');
1529
+ expect(u4).toContain('owner:\n - Di <di@x.io>');
1530
+ const u5 = norm(await fs.readFile(path.join(dir, 'Plugins/personal-u5/access.md'), 'utf8'));
1531
+ const u5Body = u5.slice(u5.indexOf('\n---\n', 4) + 5);
1532
+ // Ed's read denial stands — the frontmatter's read grant is NOT carried
1533
+ // beside it — while the owner grant, which the body said nothing about, is.
1534
+ expect(u5Body.match(/read:\n((?: - [^\n]*\n)*)/)![1]).toBe(' - deny Ed <ed@x.io>\n - deny everyone\n');
1535
+ expect(u5Body).toMatch(/owner:\n - Ed <ed@x.io>/);
1536
+ // The file block keeps Ed's grant as written and gains the denial.
1537
+ expect(u5.slice(0, u5.indexOf('\n---\n', 4))).toBe('---\nread:\n - Ed <ed@x.io>\n - deny everyone\nowner:\n - Ed <ed@x.io>');
1538
+ expect(norm(await fs.readFile(path.join(dir, 'Plugins/personal-u6/access.md'), 'utf8'))).toBe(
1539
+ '---\nread: []\n---\nread:\n - deny everyone\nwrite:\n - everyone\nowner:\n - Fy <fy@x.io>\n',
1540
+ );
1541
+ // Untouched: `download: everyone` settles read as GRANTED (download folds
1542
+ // into read), so the step reads the space as opened on purpose.
1543
+ expect(norm(await fs.readFile(path.join(dir, 'Plugins/personal-u7/access.md'), 'utf8'))).toBe(
1544
+ '---\nread: []\n---\ndownload:\n - everyone\nowner:\n - Gi <gi@x.io>\n',
1545
+ );
1546
+ expect(norm(await fs.readFile(path.join(dir, 'Plugins/GTM/access.md'), 'utf8'))).toBe(
1547
+ '---\nread:\n - everyone\n---\nread:\n - Ali Vega <ali@x.io>\n',
1548
+ );
1549
+ }
1550
+ const dir = await checkout(DEFAULT_BRANCH);
1551
+ const log = (await git(dir, ['log', '-1', '--format=%B'])).trim();
1552
+ expect(log).toContain('Keep 4 personal spaces private');
1553
+ expect(log).toContain('Plugins/personal-u1/access.md: read denies everyone');
1554
+ expect(log).toContain('Plugins/personal-u2/access.md: read denies everyone');
1555
+ expect(log).toContain('Plugins/personal-u4/access.md: read denies everyone');
1556
+ expect(log).toContain('Plugins/personal-u5/access.md: read denies everyone');
1557
+
1558
+ // Idempotent: the next boot finds every space already closed.
1559
+ await makeRunner([new PersonalSpacesStep(new NodeFs())]).runAll();
1560
+ const again = await checkout(DEFAULT_BRANCH);
1561
+ expect((await git(again, ['rev-list', '--count', 'HEAD'])).trim()).toBe('2');
1562
+ });
300
1563
  });
301
1564
 
302
1565
  describe('GroupsToPluginsStep', () => {
@@ -322,7 +1585,7 @@ describe('GroupsToPluginsStep', () => {
322
1585
  await git(seed, ['checkout', '-b', 'alice/draft']);
323
1586
  await git(seed, ['push', 'origin', 'alice/draft']);
324
1587
 
325
- await makeRunner([new GroupsToPluginsStep()]).runAll();
1588
+ await makeRunner([new GroupsToPluginsStep(new NodeFs())]).runAll();
326
1589
 
327
1590
  for (const b of [DEFAULT_BRANCH, 'alice/draft']) {
328
1591
  const dir = await checkout(b);
@@ -337,10 +1600,11 @@ describe('GroupsToPluginsStep', () => {
337
1600
  expect(mcp.mcpServers.notion).toEqual({ type: 'streamable-http', url: 'https://mcp.notion.com/mcp' });
338
1601
  const manifest = JSON.parse(await fs.readFile(path.join(dir, 'Plugins/GTM/plugin.json'), 'utf8'));
339
1602
  expect(manifest.name).toBe('gtm');
340
- // The rename's companion edit: the stale ignore rule now names Plugins/.
1603
+ // The rename's companion edit: the stale ignore rule is retired — and
1604
+ // not replaced, since the plugins root is drawn by the sidebar now.
341
1605
  const ignore = await fs.readFile(path.join(dir, '.bevelignore'), 'utf8');
342
- expect(ignore).toContain('Plugins/');
343
1606
  expect(ignore).not.toContain('Groups/');
1607
+ expect(ignore.split('\n').map((l) => l.trim())).not.toContain('Plugins/');
344
1608
  }
345
1609
 
346
1610
  const dir = await checkout(DEFAULT_BRANCH);
@@ -356,7 +1620,7 @@ describe('GroupsToPluginsStep', () => {
356
1620
  // silently skipping a branch whose reserved root cannot be a plugin tree
357
1621
  // (and on a draft nothing later would ever report it).
358
1622
  await seedUpstream({ Plugins: 'i am a file, not a folder' });
359
- await expect(makeRunner([new GroupsToPluginsStep()]).runAll()).rejects.toThrow(
1623
+ await expect(makeRunner([new GroupsToPluginsStep(new NodeFs())]).runAll()).rejects.toThrow(
360
1624
  /"Plugins" exists but is not a directory/,
361
1625
  );
362
1626
  });
@@ -368,7 +1632,7 @@ describe('GroupsToPluginsStep', () => {
368
1632
  'Plugins/B/y.md': 'new',
369
1633
  });
370
1634
  await makeRunner([
371
- new GroupsToPluginsStep(),
1635
+ new GroupsToPluginsStep(new NodeFs()),
372
1636
  // A later step dirties the branch so the refusal note surfaces in the commit.
373
1637
  step('dirty', async (ctx) => {
374
1638
  for (const b of await ctx.protectedBranches()) {
@@ -398,11 +1662,7 @@ describe('GroupsToPluginsStep', () => {
398
1662
  */
399
1663
  describe('GroupsToPluginsStep — migration edge cases', () => {
400
1664
  async function migrate(): Promise<void> {
401
- await makeRunner([new GroupsToPluginsStep()]).runAll();
402
- }
403
-
404
- async function readJson(dir: string, rel: string): Promise<Record<string, any>> {
405
- return JSON.parse(await fs.readFile(path.join(dir, rel), 'utf8'));
1665
+ await makeRunner([new GroupsToPluginsStep(new NodeFs())]).runAll();
406
1666
  }
407
1667
 
408
1668
  /** What the runner's `partial` warning carried — the named refusals. */
@@ -570,6 +1830,36 @@ describe('GroupsToPluginsStep — migration edge cases', () => {
570
1830
  });
571
1831
  });
572
1832
 
1833
+ it('leaves a plain folder under the root alone — a .gitkeep is not a plugin, and a grouping folder holding one is not either', async () => {
1834
+ await seedUpstream({
1835
+ 'Plugins/GTM/access.md': 'write:\n - Admin\n',
1836
+ // Made with "New folder" in the tree: nothing plugin-shaped in it.
1837
+ 'Plugins/TestFolder/.gitkeep': '',
1838
+ // A grouping folder with a plugin INSIDE: a manifest on the folder
1839
+ // would hide the plugin beneath it from every catalog.
1840
+ 'Plugins/Teams/Agent Made/plugin.json': '{"name":"agent-made"}',
1841
+ // A legacy plugin whose vendored dependency happens to ship a
1842
+ // manifest: `node_modules` is nothing to the walk, so the folder is
1843
+ // still the plugin it was.
1844
+ 'Plugins/Vendored/access.md': 'write:\n - Admin\n',
1845
+ 'Plugins/Vendored/node_modules/some-pkg/plugin.json': '{"name":"some-pkg"}',
1846
+ // A folder whose ONLY plugin-shaped entry is one the walk skips. The
1847
+ // migration's own sweep never moves a dot-entry, so counting it as
1848
+ // legacy content wrote a manifest for a folder and then moved nothing
1849
+ // into it — and the manifests step, reading a walk that had already
1850
+ // dropped it, disagreed about the same folder.
1851
+ 'Plugins/HiddenOnly/.hidden.tool': 'name: hidden\n',
1852
+ });
1853
+ await migrate();
1854
+ const dir = await checkout(DEFAULT_BRANCH);
1855
+ expect(await exists(dir, 'Plugins/GTM/plugin.json')).toBe(true);
1856
+ expect(await exists(dir, 'Plugins/TestFolder/plugin.json')).toBe(false);
1857
+ expect(await exists(dir, 'Plugins/Teams/plugin.json')).toBe(false);
1858
+ expect(await exists(dir, 'Plugins/Teams/Agent Made/plugin.json')).toBe(true);
1859
+ expect(await exists(dir, 'Plugins/Vendored/plugin.json')).toBe(true);
1860
+ expect(await exists(dir, 'Plugins/HiddenOnly/plugin.json')).toBe(false);
1861
+ });
1862
+
573
1863
  it('leaves a personal folder a valid plugin', async () => {
574
1864
  await seedUpstream({
575
1865
  'Groups/personal-u-123/access.md': 'write:\n - Ali <ali@x.com>\n',
@@ -581,7 +1871,7 @@ describe('GroupsToPluginsStep — migration edge cases', () => {
581
1871
  });
582
1872
 
583
1873
  describe('the .bevelignore root rule', () => {
584
- it('follows the rename, preserving every other line', async () => {
1874
+ it('retires the rule with the rename — nothing takes its place — preserving every other line', async () => {
585
1875
  await seedUpstream({
586
1876
  'Groups/GTM/access.md': 'write:\n - Admin\n',
587
1877
  '.bevelignore': '# mine\nGroups/\nMy-Own-Rule/\n',
@@ -589,48 +1879,115 @@ describe('GroupsToPluginsStep — migration edge cases', () => {
589
1879
  await migrate();
590
1880
  const dir = await checkout(DEFAULT_BRANCH);
591
1881
  const ignore = norm(await fs.readFile(path.join(dir, '.bevelignore'), 'utf8'));
592
- expect(ignore.split('\n')).toContain('Plugins/');
593
- expect(ignore).not.toContain('Groups/');
594
- expect(ignore).toContain('# mine');
595
- expect(ignore).toContain('My-Own-Rule/');
1882
+ expect(ignore).toBe('# mine\nMy-Own-Rule/\n');
596
1883
  });
597
1884
 
598
- it('rewrites EVERY exact Groups/ line — the first becomes Plugins/, duplicates are dropped', async () => {
1885
+ it('drops EVERY exact Groups/ line, keeping the lines between them', async () => {
599
1886
  await seedUpstream({
600
1887
  'Groups/GTM/access.md': 'write:\n - Admin\n',
601
1888
  '.bevelignore': 'Groups/\n# keep\nGroups/\n',
602
1889
  });
603
1890
  await migrate();
604
1891
  const dir = await checkout(DEFAULT_BRANCH);
605
- const lines = norm(await fs.readFile(path.join(dir, '.bevelignore'), 'utf8')).split('\n');
606
- expect(lines.filter((l) => l.trim() === 'Plugins/')).toHaveLength(1);
607
- expect(lines.filter((l) => l.trim() === 'Groups/')).toHaveLength(0);
608
- expect(lines).toContain('# keep');
1892
+ expect(norm(await fs.readFile(path.join(dir, '.bevelignore'), 'utf8'))).toBe('# keep\n');
609
1893
  });
610
1894
 
611
- it('is left alone when Plugins/ is already listed', async () => {
1895
+ it('retires a Plugins/ line beside it too — both root rules are stale for the same reason', async () => {
612
1896
  await seedUpstream({
613
1897
  'Groups/GTM/access.md': 'write:\n - Admin\n',
614
- '.bevelignore': 'Groups/\nPlugins/\n',
1898
+ '.bevelignore': '# mine\nGroups/\nPlugins/\n!Plugins/\n',
1899
+ });
1900
+ await migrate();
1901
+ const dir = await checkout(DEFAULT_BRANCH);
1902
+ expect(norm(await fs.readFile(path.join(dir, '.bevelignore'), 'utf8'))).toBe('# mine\n!Plugins/\n');
1903
+ });
1904
+
1905
+ it('retires the rules on a branch with neither root — nothing to migrate there, and the rules are as stale', async () => {
1906
+ await seedUpstream({
1907
+ 'marker.txt': 'seeded',
1908
+ '.bevelignore': 'Plugins/\nGroups/\nkeep/\n',
1909
+ });
1910
+ await migrate();
1911
+ const dir = await checkout(DEFAULT_BRANCH);
1912
+ expect(norm(await fs.readFile(path.join(dir, '.bevelignore'), 'utf8'))).toBe('keep/\n');
1913
+ const log = (await git(dir, ['log', '-1', '--format=%B'])).trim();
1914
+ expect(log).toContain('.bevelignore: Groups/, Plugins/ dropped');
1915
+ });
1916
+
1917
+ it('names the commit for what happened: a migrated branch that only lost the rules was not reorganised', async () => {
1918
+ await seedUpstream({
1919
+ 'Plugins/GTM/plugin.json': '{"name":"gtm"}',
1920
+ 'Plugins/GTM/access.md': 'write:\n - Admin\n',
1921
+ '.bevelignore': 'Plugins/\n',
615
1922
  });
616
1923
  await migrate();
617
1924
  const dir = await checkout(DEFAULT_BRANCH);
618
- // The stale line is harmlessly dead; deleting it would be editing the
619
- // operator's file beyond what the rename made stale.
620
- expect(norm(await fs.readFile(path.join(dir, '.bevelignore'), 'utf8'))).toBe('Groups/\nPlugins/\n');
1925
+ const log = (await git(dir, ['log', '-1', '--format=%B'])).trim();
1926
+ expect(log.split('\n')[0]).toBe('Retire the stale Groups/ and Plugins/ ignore rules');
1927
+ expect(log).not.toContain('Reorganise');
621
1928
  });
622
1929
 
623
- it('is not touched by a run that does not rename', async () => {
1930
+ it('retires the rules on a run that does not rename — a branch migrated by an earlier release, a draft included', async () => {
1931
+ // An earlier release renamed `Groups/` to `Plugins/` in the ignore file
1932
+ // of every branch it migrated; the template step retires that line on
1933
+ // the protected branches only, so this step does it wherever it goes.
624
1934
  await seedUpstream({
625
1935
  'Plugins/GTM/access.md': 'write:\n - Admin\n',
626
1936
  'Plugins/GTM/outreach/SKILL.md': '# Outreach\n',
627
- '.bevelignore': 'Groups/\n',
1937
+ '.bevelignore': 'AGENTS.md\nPlugins/\n',
628
1938
  });
629
1939
  await migrate();
630
1940
  const dir = await checkout(DEFAULT_BRANCH);
631
- expect(norm(await fs.readFile(path.join(dir, '.bevelignore'), 'utf8'))).toBe('Groups/\n');
632
- // The run still reorganised the folder — the rule alone was off-limits.
1941
+ expect(norm(await fs.readFile(path.join(dir, '.bevelignore'), 'utf8'))).toBe('AGENTS.md\n');
633
1942
  expect(await exists(dir, 'Plugins/GTM/skills/outreach/SKILL.md')).toBe(true);
1943
+ // Idempotent: a second run declares nothing.
1944
+ await migrate();
1945
+ expect(norm(await fs.readFile(path.join(await checkout(DEFAULT_BRANCH), '.bevelignore'), 'utf8'))).toBe('AGENTS.md\n');
1946
+ });
1947
+
1948
+ it("takes a platform comment above a stale rule with it, and the blank line that opened the block — the template step's own tidy-up, on a branch it never visits", async () => {
1949
+ await seedUpstream({
1950
+ 'Plugins/GTM/access.md': 'write:\n - Admin\n',
1951
+ '.bevelignore':
1952
+ '# mine\n.git/\n\n# Added by the platform: the conventions doc is not node content.\nPlugins/\n# The shared-skills root is rendered by the Skills & Tools app, like Groups/.\nGroups/\n',
1953
+ });
1954
+ await migrate();
1955
+ const dir = await checkout(DEFAULT_BRANCH);
1956
+ expect(norm(await fs.readFile(path.join(dir, '.bevelignore'), 'utf8'))).toBe('# mine\n.git/\n');
634
1957
  });
1958
+
1959
+ it('fails the run when the ignore file cannot be read — a hole is not "no file"', async () => {
1960
+ // A directory where the file should be: readable as neither. Treating
1961
+ // that as absence would leave a possible `Plugins/` rule in place and
1962
+ // report the migration done.
1963
+ await seedUpstream({
1964
+ 'Plugins/GTM/access.md': 'write:\n - Admin\n',
1965
+ '.bevelignore/keep': '',
1966
+ });
1967
+ await expect(migrate()).rejects.toThrow(/EISDIR/);
1968
+ });
1969
+ });
1970
+ });
1971
+
1972
+ /**
1973
+ * The guide's rule in `.bevelignore` is a gitignore PATTERN, and the name rules
1974
+ * admit characters gitignore reads as syntax: written bare, `#Guide.md` is a
1975
+ * comment and hides nothing. Rendered escaped, the rule names exactly the file.
1976
+ */
1977
+ describe('TemplateFilesStep: a guide named with gitignore syntax', () => {
1978
+ it('escapes the name in the ignore rule it writes, and keeps it escaped on the next boot', async () => {
1979
+ configureKbLayout({ ...DEFAULT_KB_LAYOUT, agentsFile: '#Guide[1].md' });
1980
+ await seedUpstream({ 'marker.txt': 'seeded' });
1981
+ await makeRunner([new TemplateFilesStep(new NodeFs())]).runAll();
1982
+ const dir = await checkout(DEFAULT_BRANCH);
1983
+ const ignore = norm(await fs.readFile(path.join(dir, '.bevelignore'), 'utf8')).split('\n').map((l) => l.trim());
1984
+ const escaped = String.raw`\#Guide\[1\].md`;
1985
+ expect(ignore).toContain(escaped);
1986
+ expect(ignore).not.toContain('#Guide[1].md');
1987
+ expect(await exists(dir, '#Guide[1].md')).toBe(true);
1988
+ // A second boot sees the escaped rule as present and adds no second line.
1989
+ await makeRunner([new TemplateFilesStep(new NodeFs())]).runAll();
1990
+ const again = norm(await fs.readFile(path.join(await checkout(DEFAULT_BRANCH), '.bevelignore'), 'utf8'));
1991
+ expect(again.split('\n').filter((l) => l.trim() === escaped)).toHaveLength(1);
635
1992
  });
636
1993
  });