@bevel-software/platform-core-backend 0.13.6 → 0.15.1

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 (456) hide show
  1. package/THIRD-PARTY-NOTICES.md +6 -6
  2. package/dist/core/catalog-cache-invalidation.d.ts +29 -0
  3. package/dist/core/catalog-cache-invalidation.d.ts.map +1 -0
  4. package/dist/core/catalog-cache-invalidation.js +71 -0
  5. package/dist/core/catalog-cache-invalidation.js.map +1 -0
  6. package/dist/core/create-core-server.d.ts.map +1 -1
  7. package/dist/core/create-core-server.js +101 -9
  8. package/dist/core/create-core-server.js.map +1 -1
  9. package/dist/core/create-core-services.d.ts +20 -1
  10. package/dist/core/create-core-services.d.ts.map +1 -1
  11. package/dist/core/create-core-services.js +105 -43
  12. package/dist/core/create-core-services.js.map +1 -1
  13. package/dist/core-config.d.ts.map +1 -1
  14. package/dist/core-config.js +12 -5
  15. package/dist/core-config.js.map +1 -1
  16. package/dist/modules/access/access-control.interface.d.ts +18 -6
  17. package/dist/modules/access/access-control.interface.d.ts.map +1 -1
  18. package/dist/modules/access/access-control.service.d.ts +26 -1
  19. package/dist/modules/access/access-control.service.d.ts.map +1 -1
  20. package/dist/modules/access/access-control.service.js +213 -71
  21. package/dist/modules/access/access-control.service.js.map +1 -1
  22. package/dist/modules/access/access.routes.d.ts.map +1 -1
  23. package/dist/modules/access/access.routes.js +59 -7
  24. package/dist/modules/access/access.routes.js.map +1 -1
  25. package/dist/modules/access-model/access-grammar.d.ts +51 -1
  26. package/dist/modules/access-model/access-grammar.d.ts.map +1 -1
  27. package/dist/modules/access-model/access-grammar.js +95 -1
  28. package/dist/modules/access-model/access-grammar.js.map +1 -1
  29. package/dist/modules/access-model/group-files.d.ts.map +1 -1
  30. package/dist/modules/access-model/group-files.js +4 -1
  31. package/dist/modules/access-model/group-files.js.map +1 -1
  32. package/dist/modules/access-model/kb-read-filter.d.ts +12 -0
  33. package/dist/modules/access-model/kb-read-filter.d.ts.map +1 -1
  34. package/dist/modules/access-model/kb-read-filter.js +15 -0
  35. package/dist/modules/access-model/kb-read-filter.js.map +1 -1
  36. package/dist/modules/access-model/plugin-principals.d.ts +49 -0
  37. package/dist/modules/access-model/plugin-principals.d.ts.map +1 -0
  38. package/dist/modules/access-model/plugin-principals.js +125 -0
  39. package/dist/modules/access-model/plugin-principals.js.map +1 -0
  40. package/dist/modules/connection-probe/connection-probe.contract.d.ts +43 -0
  41. package/dist/modules/connection-probe/connection-probe.contract.d.ts.map +1 -0
  42. package/dist/modules/connection-probe/connection-probe.contract.js +2 -0
  43. package/dist/modules/connection-probe/connection-probe.contract.js.map +1 -0
  44. package/dist/modules/connection-probe/connection-probe.service.d.ts +94 -0
  45. package/dist/modules/connection-probe/connection-probe.service.d.ts.map +1 -0
  46. package/dist/modules/connection-probe/connection-probe.service.js +684 -0
  47. package/dist/modules/connection-probe/connection-probe.service.js.map +1 -0
  48. package/dist/modules/connection-probe/index.d.ts +3 -0
  49. package/dist/modules/connection-probe/index.d.ts.map +1 -0
  50. package/dist/modules/connection-probe/index.js +3 -0
  51. package/dist/modules/connection-probe/index.js.map +1 -0
  52. package/dist/modules/database/core-schema.d.ts +326 -0
  53. package/dist/modules/database/core-schema.d.ts.map +1 -1
  54. package/dist/modules/database/core-schema.js +52 -0
  55. package/dist/modules/database/core-schema.js.map +1 -1
  56. package/dist/modules/diff/diff.routes.d.ts.map +1 -1
  57. package/dist/modules/diff/diff.routes.js +3 -5
  58. package/dist/modules/diff/diff.routes.js.map +1 -1
  59. package/dist/modules/kb-fs/mutex.d.ts +37 -0
  60. package/dist/modules/kb-fs/mutex.d.ts.map +1 -1
  61. package/dist/modules/kb-fs/mutex.js +48 -5
  62. package/dist/modules/kb-fs/mutex.js.map +1 -1
  63. package/dist/modules/kb-sync/index.d.ts +6 -0
  64. package/dist/modules/kb-sync/index.d.ts.map +1 -0
  65. package/dist/modules/kb-sync/index.js +5 -0
  66. package/dist/modules/kb-sync/index.js.map +1 -0
  67. package/dist/modules/kb-sync/kb-sync.interface.d.ts +77 -0
  68. package/dist/modules/kb-sync/kb-sync.interface.d.ts.map +1 -0
  69. package/dist/modules/kb-sync/kb-sync.interface.js +2 -0
  70. package/dist/modules/kb-sync/kb-sync.interface.js.map +1 -0
  71. package/dist/modules/kb-sync/kb-sync.routes.d.ts +70 -0
  72. package/dist/modules/kb-sync/kb-sync.routes.d.ts.map +1 -0
  73. package/dist/modules/kb-sync/kb-sync.routes.js +135 -0
  74. package/dist/modules/kb-sync/kb-sync.routes.js.map +1 -0
  75. package/dist/modules/kb-sync/kb-sync.service.d.ts +45 -0
  76. package/dist/modules/kb-sync/kb-sync.service.d.ts.map +1 -0
  77. package/dist/modules/kb-sync/kb-sync.service.js +183 -0
  78. package/dist/modules/kb-sync/kb-sync.service.js.map +1 -0
  79. package/dist/modules/kb-sync/sync-auth.d.ts +59 -0
  80. package/dist/modules/kb-sync/sync-auth.d.ts.map +1 -0
  81. package/dist/modules/kb-sync/sync-auth.js +62 -0
  82. package/dist/modules/kb-sync/sync-auth.js.map +1 -0
  83. package/dist/modules/kb-sync/sync-payload.d.ts +32 -0
  84. package/dist/modules/kb-sync/sync-payload.d.ts.map +1 -0
  85. package/dist/modules/kb-sync/sync-payload.js +105 -0
  86. package/dist/modules/kb-sync/sync-payload.js.map +1 -0
  87. package/dist/modules/marketplace/git-http.routes.d.ts +42 -0
  88. package/dist/modules/marketplace/git-http.routes.d.ts.map +1 -0
  89. package/dist/modules/marketplace/git-http.routes.js +190 -0
  90. package/dist/modules/marketplace/git-http.routes.js.map +1 -0
  91. package/dist/modules/marketplace/github-facade/github-facade-admin.routes.d.ts +23 -0
  92. package/dist/modules/marketplace/github-facade/github-facade-admin.routes.d.ts.map +1 -0
  93. package/dist/modules/marketplace/github-facade/github-facade-admin.routes.js +59 -0
  94. package/dist/modules/marketplace/github-facade/github-facade-admin.routes.js.map +1 -0
  95. package/dist/modules/marketplace/github-facade/github-facade-codes.store.d.ts +59 -0
  96. package/dist/modules/marketplace/github-facade/github-facade-codes.store.d.ts.map +1 -0
  97. package/dist/modules/marketplace/github-facade/github-facade-codes.store.js +96 -0
  98. package/dist/modules/marketplace/github-facade/github-facade-codes.store.js.map +1 -0
  99. package/dist/modules/marketplace/github-facade/github-facade-credentials.service.d.ts +99 -0
  100. package/dist/modules/marketplace/github-facade/github-facade-credentials.service.d.ts.map +1 -0
  101. package/dist/modules/marketplace/github-facade/github-facade-credentials.service.js +147 -0
  102. package/dist/modules/marketplace/github-facade/github-facade-credentials.service.js.map +1 -0
  103. package/dist/modules/marketplace/github-facade/github-facade.routes.d.ts +47 -0
  104. package/dist/modules/marketplace/github-facade/github-facade.routes.d.ts.map +1 -0
  105. package/dist/modules/marketplace/github-facade/github-facade.routes.js +228 -0
  106. package/dist/modules/marketplace/github-facade/github-facade.routes.js.map +1 -0
  107. package/dist/modules/marketplace/github-facade/github-facade.service.d.ts +109 -0
  108. package/dist/modules/marketplace/github-facade/github-facade.service.d.ts.map +1 -0
  109. package/dist/modules/marketplace/github-facade/github-facade.service.js +172 -0
  110. package/dist/modules/marketplace/github-facade/github-facade.service.js.map +1 -0
  111. package/dist/modules/marketplace/github-facade/index.d.ts +6 -0
  112. package/dist/modules/marketplace/github-facade/index.d.ts.map +1 -0
  113. package/dist/modules/marketplace/github-facade/index.js +6 -0
  114. package/dist/modules/marketplace/github-facade/index.js.map +1 -0
  115. package/dist/modules/marketplace/index.d.ts +4 -0
  116. package/dist/modules/marketplace/index.d.ts.map +1 -0
  117. package/dist/modules/marketplace/index.js +4 -0
  118. package/dist/modules/marketplace/index.js.map +1 -0
  119. package/dist/modules/marketplace/marketplace-repo.service.d.ts +122 -0
  120. package/dist/modules/marketplace/marketplace-repo.service.d.ts.map +1 -0
  121. package/dist/modules/marketplace/marketplace-repo.service.js +300 -0
  122. package/dist/modules/marketplace/marketplace-repo.service.js.map +1 -0
  123. package/dist/modules/mcp/oauth/bevel-oauth-provider.d.ts.map +1 -1
  124. package/dist/modules/mcp/oauth/bevel-oauth-provider.js +4 -0
  125. package/dist/modules/mcp/oauth/bevel-oauth-provider.js.map +1 -1
  126. package/dist/modules/mcp/oauth/oauth-consent.routes.d.ts +14 -8
  127. package/dist/modules/mcp/oauth/oauth-consent.routes.d.ts.map +1 -1
  128. package/dist/modules/mcp/oauth/oauth-consent.routes.js +13 -1
  129. package/dist/modules/mcp/oauth/oauth-consent.routes.js.map +1 -1
  130. package/dist/modules/mcp/oauth/oauth-state.d.ts +13 -2
  131. package/dist/modules/mcp/oauth/oauth-state.d.ts.map +1 -1
  132. package/dist/modules/mcp/oauth/oauth-state.js +3 -1
  133. package/dist/modules/mcp/oauth/oauth-state.js.map +1 -1
  134. package/dist/modules/plugins/compile/compile-marketplace.d.ts +97 -0
  135. package/dist/modules/plugins/compile/compile-marketplace.d.ts.map +1 -0
  136. package/dist/modules/plugins/compile/compile-marketplace.js +240 -0
  137. package/dist/modules/plugins/compile/compile-marketplace.js.map +1 -0
  138. package/dist/modules/plugins/compile/marketplace-compiler.service.d.ts +66 -0
  139. package/dist/modules/plugins/compile/marketplace-compiler.service.d.ts.map +1 -0
  140. package/dist/modules/plugins/compile/marketplace-compiler.service.js +132 -0
  141. package/dist/modules/plugins/compile/marketplace-compiler.service.js.map +1 -0
  142. package/dist/modules/plugins/discovery/bundle-dialect/bundle.source.d.ts +37 -0
  143. package/dist/modules/plugins/discovery/bundle-dialect/bundle.source.d.ts.map +1 -0
  144. package/dist/modules/plugins/discovery/bundle-dialect/bundle.source.js +135 -0
  145. package/dist/modules/plugins/discovery/bundle-dialect/bundle.source.js.map +1 -0
  146. package/dist/modules/plugins/discovery/bundle-dialect/registry.d.ts +45 -0
  147. package/dist/modules/plugins/discovery/bundle-dialect/registry.d.ts.map +1 -0
  148. package/dist/modules/plugins/discovery/bundle-dialect/registry.js +147 -0
  149. package/dist/modules/plugins/discovery/bundle-dialect/registry.js.map +1 -0
  150. package/dist/modules/plugins/discovery/kb-plugin-source.d.ts +37 -0
  151. package/dist/modules/plugins/discovery/kb-plugin-source.d.ts.map +1 -0
  152. package/dist/modules/plugins/discovery/kb-plugin-source.js +107 -0
  153. package/dist/modules/plugins/discovery/kb-plugin-source.js.map +1 -0
  154. package/dist/modules/plugins/discovery/native.source.d.ts +15 -0
  155. package/dist/modules/plugins/discovery/native.source.d.ts.map +1 -0
  156. package/dist/modules/plugins/discovery/native.source.js +102 -0
  157. package/dist/modules/plugins/discovery/native.source.js.map +1 -0
  158. package/dist/modules/plugins/discovery/plugin-source.d.ts +96 -0
  159. package/dist/modules/plugins/discovery/plugin-source.d.ts.map +1 -0
  160. package/dist/modules/plugins/discovery/plugin-source.js +2 -0
  161. package/dist/modules/plugins/discovery/plugin-source.js.map +1 -0
  162. package/dist/modules/plugins/index.d.ts +10 -1
  163. package/dist/modules/plugins/index.d.ts.map +1 -1
  164. package/dist/modules/plugins/index.js +8 -0
  165. package/dist/modules/plugins/index.js.map +1 -1
  166. package/dist/modules/plugins/plugin-links.d.ts +66 -0
  167. package/dist/modules/plugins/plugin-links.d.ts.map +1 -0
  168. package/dist/modules/plugins/plugin-links.js +160 -0
  169. package/dist/modules/plugins/plugin-links.js.map +1 -0
  170. package/dist/modules/plugins/plugin-links.service.d.ts +114 -0
  171. package/dist/modules/plugins/plugin-links.service.d.ts.map +1 -0
  172. package/dist/modules/plugins/plugin-links.service.js +304 -0
  173. package/dist/modules/plugins/plugin-links.service.js.map +1 -0
  174. package/dist/modules/plugins/plugin-provision.service.d.ts +39 -3
  175. package/dist/modules/plugins/plugin-provision.service.d.ts.map +1 -1
  176. package/dist/modules/plugins/plugin-provision.service.js +152 -52
  177. package/dist/modules/plugins/plugin-provision.service.js.map +1 -1
  178. package/dist/modules/plugins/plugin-rename.service.d.ts +69 -0
  179. package/dist/modules/plugins/plugin-rename.service.d.ts.map +1 -0
  180. package/dist/modules/plugins/plugin-rename.service.js +245 -0
  181. package/dist/modules/plugins/plugin-rename.service.js.map +1 -0
  182. package/dist/modules/plugins/plugins.contract.d.ts +27 -1
  183. package/dist/modules/plugins/plugins.contract.d.ts.map +1 -1
  184. package/dist/modules/plugins/plugins.routes.d.ts +7 -1
  185. package/dist/modules/plugins/plugins.routes.d.ts.map +1 -1
  186. package/dist/modules/plugins/plugins.routes.js +120 -12
  187. package/dist/modules/plugins/plugins.routes.js.map +1 -1
  188. package/dist/modules/plugins/plugins.service.d.ts +50 -3
  189. package/dist/modules/plugins/plugins.service.d.ts.map +1 -1
  190. package/dist/modules/plugins/plugins.service.js +108 -47
  191. package/dist/modules/plugins/plugins.service.js.map +1 -1
  192. package/dist/modules/secrets-vault/db-secrets-vault.service.js +1 -1
  193. package/dist/modules/secrets-vault/db-secrets-vault.service.js.map +1 -1
  194. package/dist/modules/secrets-vault/secrets-vault.routes.d.ts +6 -0
  195. package/dist/modules/secrets-vault/secrets-vault.routes.d.ts.map +1 -1
  196. package/dist/modules/secrets-vault/secrets-vault.routes.js +31 -1
  197. package/dist/modules/secrets-vault/secrets-vault.routes.js.map +1 -1
  198. package/dist/modules/settings/deployment-settings.service.d.ts +9 -0
  199. package/dist/modules/settings/deployment-settings.service.d.ts.map +1 -1
  200. package/dist/modules/settings/deployment-settings.service.js +82 -2
  201. package/dist/modules/settings/deployment-settings.service.js.map +1 -1
  202. package/dist/modules/settings/setup.routes.d.ts +26 -0
  203. package/dist/modules/settings/setup.routes.d.ts.map +1 -1
  204. package/dist/modules/settings/setup.routes.js +9 -1
  205. package/dist/modules/settings/setup.routes.js.map +1 -1
  206. package/dist/modules/skills/index.d.ts +1 -0
  207. package/dist/modules/skills/index.d.ts.map +1 -1
  208. package/dist/modules/skills/index.js +1 -0
  209. package/dist/modules/skills/index.js.map +1 -1
  210. package/dist/modules/skills/pending-skills.service.d.ts.map +1 -1
  211. package/dist/modules/skills/pending-skills.service.js +9 -4
  212. package/dist/modules/skills/pending-skills.service.js.map +1 -1
  213. package/dist/modules/skills/skill-access-requests.routes.d.ts +38 -0
  214. package/dist/modules/skills/skill-access-requests.routes.d.ts.map +1 -0
  215. package/dist/modules/skills/skill-access-requests.routes.js +168 -0
  216. package/dist/modules/skills/skill-access-requests.routes.js.map +1 -0
  217. package/dist/modules/skills/skills.contract.d.ts +31 -0
  218. package/dist/modules/skills/skills.contract.d.ts.map +1 -1
  219. package/dist/modules/skills/skills.routes.d.ts +28 -3
  220. package/dist/modules/skills/skills.routes.d.ts.map +1 -1
  221. package/dist/modules/skills/skills.routes.js +42 -3
  222. package/dist/modules/skills/skills.routes.js.map +1 -1
  223. package/dist/modules/skills/skills.service.d.ts +4 -0
  224. package/dist/modules/skills/skills.service.d.ts.map +1 -1
  225. package/dist/modules/skills/skills.service.js +83 -22
  226. package/dist/modules/skills/skills.service.js.map +1 -1
  227. package/dist/modules/tool-auth/external-api-key.interface.d.ts +18 -1
  228. package/dist/modules/tool-auth/external-api-key.interface.d.ts.map +1 -1
  229. package/dist/modules/tool-auth/external-api-key.interface.js +2 -1
  230. package/dist/modules/tool-auth/external-api-key.interface.js.map +1 -1
  231. package/dist/modules/tool-auth/external-api-key.service.d.ts +13 -4
  232. package/dist/modules/tool-auth/external-api-key.service.d.ts.map +1 -1
  233. package/dist/modules/tool-auth/external-api-key.service.js +26 -9
  234. package/dist/modules/tool-auth/external-api-key.service.js.map +1 -1
  235. package/dist/modules/tool-manuals/mcp-json-discovery.d.ts +36 -0
  236. package/dist/modules/tool-manuals/mcp-json-discovery.d.ts.map +1 -1
  237. package/dist/modules/tool-manuals/mcp-json-discovery.js +130 -83
  238. package/dist/modules/tool-manuals/mcp-json-discovery.js.map +1 -1
  239. package/dist/modules/tool-manuals/mcp-server-edit.service.d.ts.map +1 -1
  240. package/dist/modules/tool-manuals/mcp-server-edit.service.js +10 -4
  241. package/dist/modules/tool-manuals/mcp-server-edit.service.js.map +1 -1
  242. package/dist/modules/tool-manuals/tool-manuals.contract.d.ts +83 -0
  243. package/dist/modules/tool-manuals/tool-manuals.contract.d.ts.map +1 -1
  244. package/dist/modules/tool-manuals/tool-manuals.service.d.ts +15 -2
  245. package/dist/modules/tool-manuals/tool-manuals.service.d.ts.map +1 -1
  246. package/dist/modules/tool-manuals/tool-manuals.service.js +199 -38
  247. package/dist/modules/tool-manuals/tool-manuals.service.js.map +1 -1
  248. package/dist/modules/workflow/git/git.service.d.ts +58 -3
  249. package/dist/modules/workflow/git/git.service.d.ts.map +1 -1
  250. package/dist/modules/workflow/git/git.service.js +325 -60
  251. package/dist/modules/workflow/git/git.service.js.map +1 -1
  252. package/dist/modules/workflow/git/pull-request.service.d.ts +6 -0
  253. package/dist/modules/workflow/git/pull-request.service.d.ts.map +1 -1
  254. package/dist/modules/workflow/git/pull-request.service.js +8 -0
  255. package/dist/modules/workflow/git/pull-request.service.js.map +1 -1
  256. package/dist/modules/workflow/workflow.routes.d.ts +2 -1
  257. package/dist/modules/workflow/workflow.routes.d.ts.map +1 -1
  258. package/dist/modules/workflow/workflow.routes.js +98 -9
  259. package/dist/modules/workflow/workflow.routes.js.map +1 -1
  260. package/dist/modules/workflow/workflow.service.d.ts +133 -9
  261. package/dist/modules/workflow/workflow.service.d.ts.map +1 -1
  262. package/dist/modules/workflow/workflow.service.js +462 -52
  263. package/dist/modules/workflow/workflow.service.js.map +1 -1
  264. package/dist/modules/workspace/startup/steps/plugin-manifests.step.d.ts +35 -0
  265. package/dist/modules/workspace/startup/steps/plugin-manifests.step.d.ts.map +1 -0
  266. package/dist/modules/workspace/startup/steps/plugin-manifests.step.js +121 -0
  267. package/dist/modules/workspace/startup/steps/plugin-manifests.step.js.map +1 -0
  268. package/dist/modules/workspace/startup/steps/seed-tree.d.ts.map +1 -1
  269. package/dist/modules/workspace/startup/steps/seed-tree.js +46 -2
  270. package/dist/modules/workspace/startup/steps/seed-tree.js.map +1 -1
  271. package/dist/modules/workspace/startup/steps/template-files.step.d.ts.map +1 -1
  272. package/dist/modules/workspace/startup/steps/template-files.step.js +112 -25
  273. package/dist/modules/workspace/startup/steps/template-files.step.js.map +1 -1
  274. package/dist/modules/workspace/workspace.routes.d.ts.map +1 -1
  275. package/dist/modules/workspace/workspace.routes.js +11 -14
  276. package/dist/modules/workspace/workspace.routes.js.map +1 -1
  277. package/dist/modules/workspace/workspace.service.d.ts +38 -1
  278. package/dist/modules/workspace/workspace.service.d.ts.map +1 -1
  279. package/dist/modules/workspace/workspace.service.js +131 -20
  280. package/dist/modules/workspace/workspace.service.js.map +1 -1
  281. package/dist/shared/domain-errors.d.ts +20 -0
  282. package/dist/shared/domain-errors.d.ts.map +1 -1
  283. package/dist/shared/domain-errors.js +29 -0
  284. package/dist/shared/domain-errors.js.map +1 -1
  285. package/dist/shared/fs-errors.d.ts +8 -0
  286. package/dist/shared/fs-errors.d.ts.map +1 -0
  287. package/dist/shared/fs-errors.js +11 -0
  288. package/dist/shared/fs-errors.js.map +1 -0
  289. package/dist/shared/fs-walk.d.ts +20 -6
  290. package/dist/shared/fs-walk.d.ts.map +1 -1
  291. package/dist/shared/fs-walk.js +35 -27
  292. package/dist/shared/fs-walk.js.map +1 -1
  293. package/dist/shared/kb-walk.d.ts +52 -0
  294. package/dist/shared/kb-walk.d.ts.map +1 -0
  295. package/dist/shared/kb-walk.js +76 -0
  296. package/dist/shared/kb-walk.js.map +1 -0
  297. package/dist/shared/path-order.d.ts +18 -0
  298. package/dist/shared/path-order.d.ts.map +1 -0
  299. package/dist/shared/path-order.js +35 -0
  300. package/dist/shared/path-order.js.map +1 -0
  301. package/dist/shared/token-crypto.d.ts +17 -2
  302. package/dist/shared/token-crypto.d.ts.map +1 -1
  303. package/dist/shared/token-crypto.js +25 -8
  304. package/dist/shared/token-crypto.js.map +1 -1
  305. package/dist/shared/ttl-cache.d.ts +28 -1
  306. package/dist/shared/ttl-cache.d.ts.map +1 -1
  307. package/dist/shared/ttl-cache.js +33 -1
  308. package/dist/shared/ttl-cache.js.map +1 -1
  309. package/kb-template/.bevelignore +1 -1
  310. package/kb-template/AGENTS.md +95 -30
  311. package/kb-template/Skills/.gitkeep +0 -0
  312. package/migrations/0005_github_facade.sql +25 -0
  313. package/migrations/meta/0005_snapshot.json +1717 -0
  314. package/migrations/meta/_journal.json +7 -0
  315. package/package.json +6 -6
  316. package/src/__tests__/core-config.admin.test.ts +21 -0
  317. package/src/__tests__/kb-layout-config.test.ts +110 -0
  318. package/src/core/__tests__/catalog-cache-invalidation.test.ts +146 -0
  319. package/src/core/catalog-cache-invalidation.ts +92 -0
  320. package/src/core/create-core-server.ts +126 -7
  321. package/src/core/create-core-services.ts +173 -41
  322. package/src/core-config.ts +14 -7
  323. package/src/modules/access/__tests__/access-control.invalidate-race.test.ts +86 -0
  324. package/src/modules/access/__tests__/access-control.service.test.ts +19 -3
  325. package/src/modules/access/__tests__/access-plugin-principals.test.ts +368 -0
  326. package/src/modules/access/__tests__/groups-admin.service.test.ts +8 -0
  327. package/src/modules/access/access-control.interface.ts +20 -9
  328. package/src/modules/access/access-control.service.ts +240 -78
  329. package/src/modules/access/access.routes.ts +96 -14
  330. package/src/modules/access-model/__tests__/access-grammar.test.ts +18 -0
  331. package/src/modules/access-model/__tests__/plugin-token-canonical.test.ts +40 -0
  332. package/src/modules/access-model/access-grammar.ts +114 -2
  333. package/src/modules/access-model/group-files.ts +4 -0
  334. package/src/modules/access-model/kb-read-filter.ts +22 -0
  335. package/src/modules/access-model/plugin-principals.ts +137 -0
  336. package/src/modules/connection-probe/__tests__/connection-probe.service.test.ts +685 -0
  337. package/src/modules/connection-probe/connection-probe.contract.ts +44 -0
  338. package/src/modules/connection-probe/connection-probe.service.ts +734 -0
  339. package/src/modules/connection-probe/index.ts +2 -0
  340. package/src/modules/database/core-schema.ts +54 -0
  341. package/src/modules/diff/diff.routes.ts +9 -5
  342. package/src/modules/kb-fs/__tests__/mutex.test.ts +107 -0
  343. package/src/modules/kb-fs/mutex.ts +50 -5
  344. package/src/modules/kb-sync/__tests__/kb-sync.routes.test.ts +297 -0
  345. package/src/modules/kb-sync/__tests__/kb-sync.service.test.ts +295 -0
  346. package/src/modules/kb-sync/__tests__/sync-auth.test.ts +118 -0
  347. package/src/modules/kb-sync/__tests__/sync-payload.test.ts +148 -0
  348. package/src/modules/kb-sync/index.ts +19 -0
  349. package/src/modules/kb-sync/kb-sync.interface.ts +79 -0
  350. package/src/modules/kb-sync/kb-sync.routes.ts +197 -0
  351. package/src/modules/kb-sync/kb-sync.service.ts +201 -0
  352. package/src/modules/kb-sync/sync-auth.ts +118 -0
  353. package/src/modules/kb-sync/sync-payload.ts +143 -0
  354. package/src/modules/marketplace/__tests__/github-facade.test.ts +443 -0
  355. package/src/modules/marketplace/__tests__/marketplace-git.test.ts +179 -0
  356. package/src/modules/marketplace/git-http.routes.ts +193 -0
  357. package/src/modules/marketplace/github-facade/github-facade-admin.routes.ts +78 -0
  358. package/src/modules/marketplace/github-facade/github-facade-codes.store.ts +145 -0
  359. package/src/modules/marketplace/github-facade/github-facade-credentials.service.ts +208 -0
  360. package/src/modules/marketplace/github-facade/github-facade.routes.ts +268 -0
  361. package/src/modules/marketplace/github-facade/github-facade.service.ts +221 -0
  362. package/src/modules/marketplace/github-facade/index.ts +25 -0
  363. package/src/modules/marketplace/index.ts +3 -0
  364. package/src/modules/marketplace/marketplace-repo.service.ts +337 -0
  365. package/src/modules/mcp/oauth/bevel-oauth-provider.ts +3 -0
  366. package/src/modules/mcp/oauth/oauth-consent.routes.ts +24 -2
  367. package/src/modules/mcp/oauth/oauth-state.ts +15 -3
  368. package/src/modules/plugins/__tests__/bundle-dialect.e2e.test.ts +163 -0
  369. package/src/modules/plugins/__tests__/compile-marketplace.test.ts +294 -0
  370. package/src/modules/plugins/__tests__/plugin-index.service.test.ts +56 -12
  371. package/src/modules/plugins/__tests__/plugin-links.service.test.ts +287 -0
  372. package/src/modules/plugins/__tests__/plugin-provision.service.test.ts +205 -5
  373. package/src/modules/plugins/__tests__/plugin-rename.service.test.ts +394 -0
  374. package/src/modules/plugins/__tests__/plugins.routes.test.ts +80 -33
  375. package/src/modules/plugins/compile/compile-marketplace.ts +403 -0
  376. package/src/modules/plugins/compile/marketplace-compiler.service.ts +150 -0
  377. package/src/modules/plugins/discovery/__tests__/kb-plugin-source.test.ts +329 -0
  378. package/src/modules/plugins/discovery/bundle-dialect/bundle.source.ts +140 -0
  379. package/src/modules/plugins/discovery/bundle-dialect/registry.ts +175 -0
  380. package/src/modules/plugins/discovery/kb-plugin-source.ts +114 -0
  381. package/src/modules/plugins/discovery/native.source.ts +122 -0
  382. package/src/modules/plugins/discovery/plugin-source.ts +99 -0
  383. package/src/modules/plugins/index.ts +10 -0
  384. package/src/modules/plugins/plugin-links.service.ts +363 -0
  385. package/src/modules/plugins/plugin-links.ts +184 -0
  386. package/src/modules/plugins/plugin-provision.service.ts +160 -54
  387. package/src/modules/plugins/plugin-rename.service.ts +298 -0
  388. package/src/modules/plugins/plugins.contract.ts +28 -1
  389. package/src/modules/plugins/plugins.routes.ts +128 -11
  390. package/src/modules/plugins/plugins.service.ts +106 -53
  391. package/src/modules/secrets-vault/__tests__/connect-pending.route.test.ts +12 -0
  392. package/src/modules/secrets-vault/__tests__/oauth-return-to.route.test.ts +10 -3
  393. package/src/modules/secrets-vault/__tests__/tool-owner-gate.route.test.ts +26 -0
  394. package/src/modules/secrets-vault/db-secrets-vault.service.ts +1 -1
  395. package/src/modules/secrets-vault/secrets-vault.routes.ts +35 -1
  396. package/src/modules/settings/__tests__/deployment-settings.service.test.ts +57 -0
  397. package/src/modules/settings/__tests__/setup.routes.sync-status.test.ts +77 -0
  398. package/src/modules/settings/deployment-settings.service.ts +95 -2
  399. package/src/modules/settings/setup.routes.ts +24 -0
  400. package/src/modules/skills/__tests__/skill-access-requests.routes.test.ts +128 -0
  401. package/src/modules/skills/__tests__/skills.service.test.ts +85 -0
  402. package/src/modules/skills/index.ts +1 -0
  403. package/src/modules/skills/pending-skills.service.ts +7 -5
  404. package/src/modules/skills/skill-access-requests.routes.ts +199 -0
  405. package/src/modules/skills/skills.contract.ts +32 -0
  406. package/src/modules/skills/skills.routes.ts +59 -3
  407. package/src/modules/skills/skills.service.ts +84 -22
  408. package/src/modules/tool-auth/external-api-key.interface.ts +20 -1
  409. package/src/modules/tool-auth/external-api-key.service.ts +31 -13
  410. package/src/modules/tool-manuals/__tests__/mcp-json-discovery.test.ts +35 -1
  411. package/src/modules/tool-manuals/__tests__/tool-manuals.cli.test.ts +2 -0
  412. package/src/modules/tool-manuals/__tests__/tool-manuals.health-check.test.ts +104 -0
  413. package/src/modules/tool-manuals/__tests__/tool-manuals.service.test.ts +56 -0
  414. package/src/modules/tool-manuals/mcp-json-discovery.ts +154 -76
  415. package/src/modules/tool-manuals/mcp-server-edit.service.ts +13 -4
  416. package/src/modules/tool-manuals/tool-manuals.contract.ts +89 -0
  417. package/src/modules/tool-manuals/tool-manuals.service.ts +210 -37
  418. package/src/modules/workflow/__tests__/preserve-roles-yaml.test.ts +1 -1
  419. package/src/modules/workflow/__tests__/workflow.routes.history-read-gate.test.ts +139 -0
  420. package/src/modules/workflow/__tests__/workflow.service.branch-in-use.test.ts +337 -0
  421. package/src/modules/workflow/__tests__/workflow.service.commitFileWhileLocked.test.ts +1 -0
  422. package/src/modules/workflow/__tests__/workflow.service.deleted-branch-sweep.test.ts +7 -1
  423. package/src/modules/workflow/__tests__/workflow.service.facade.test.ts +97 -10
  424. package/src/modules/workflow/__tests__/workflow.service.releaseLock.test.ts +119 -2
  425. package/src/modules/workflow/__tests__/workflow.service.sync.test.ts +437 -0
  426. package/src/modules/workflow/git/__tests__/git.service.history-guards.test.ts +86 -0
  427. package/src/modules/workflow/git/__tests__/git.service.postMergePull.test.ts +3 -3
  428. package/src/modules/workflow/git/__tests__/git.service.pull.test.ts +47 -4
  429. package/src/modules/workflow/git/__tests__/git.service.syncFromRemote.test.ts +253 -0
  430. package/src/modules/workflow/git/git.service.ts +354 -63
  431. package/src/modules/workflow/git/pull-request.service.ts +9 -0
  432. package/src/modules/workflow/workflow.routes.ts +104 -4
  433. package/src/modules/workflow/workflow.service.ts +505 -64
  434. package/src/modules/workspace/__tests__/workspace.routes.read-gate.test.ts +6 -7
  435. package/src/modules/workspace/__tests__/workspace.service.list-cloned.test.ts +93 -0
  436. package/src/modules/workspace/__tests__/workspace.service.missing-branch.test.ts +28 -0
  437. package/src/modules/workspace/__tests__/workspace.service.read-filter.test.ts +42 -12
  438. package/src/modules/workspace/startup/steps/__tests__/steps.test.ts +219 -4
  439. package/src/modules/workspace/startup/steps/plugin-manifests.step.ts +129 -0
  440. package/src/modules/workspace/startup/steps/seed-tree.ts +44 -2
  441. package/src/modules/workspace/startup/steps/template-files.step.ts +131 -27
  442. package/src/modules/workspace/workspace.routes.ts +17 -21
  443. package/src/modules/workspace/workspace.service.ts +128 -19
  444. package/src/shared/__tests__/filename.test.ts +0 -0
  445. package/src/shared/__tests__/fs-walk.test.ts +22 -1
  446. package/src/shared/__tests__/kb-walk.test.ts +107 -0
  447. package/src/shared/__tests__/path-order.test.ts +24 -0
  448. package/src/shared/__tests__/token-crypto.test.ts +34 -0
  449. package/src/shared/__tests__/ttl-cache.test.ts +55 -0
  450. package/src/shared/domain-errors.ts +29 -0
  451. package/src/shared/fs-errors.ts +10 -0
  452. package/src/shared/fs-walk.ts +38 -23
  453. package/src/shared/kb-walk.ts +95 -0
  454. package/src/shared/path-order.ts +33 -0
  455. package/src/shared/token-crypto.ts +28 -10
  456. package/src/shared/ttl-cache.ts +33 -1
@@ -0,0 +1,337 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { drizzle } from 'drizzle-orm/pg-proxy';
3
+ import { configureBranchModel } from '@bevel-software/platform-shared';
4
+ import type { GitService } from '../git/git.service.js';
5
+ import type { PullRequestService } from '../git/pull-request.service.js';
6
+ import type { IReviewWorkflowService } from '../review-workflow/review-workflow.interface.js';
7
+ import type { WorkspaceService } from '../../workspace/workspace.service.js';
8
+ import type { IAccessControl } from '../../access/access-control.interface.js';
9
+ import type { FileLockService } from '../file-lock.service.js';
10
+ import type { PendingCommitsService } from '../pending-commits.service.js';
11
+ import type { Database } from '../../database/connection.js';
12
+ import { WorkflowService } from '../workflow.service.js';
13
+
14
+ /**
15
+ * A branch is load-bearing for a change request at EITHER end.
16
+ *
17
+ * The regression: merging `X -> main` retired X while a second open request
18
+ * `Y -> X` still proposed INTO it. Every guard asked only "is X anybody's
19
+ * SOURCE?", so X was deleted and `Y -> X` became permanently unusable — it
20
+ * could not be read (the detail read resolves the target first), declined, or
21
+ * deleted, and the boot sweep looked at sources only, so it survived every
22
+ * restart.
23
+ *
24
+ * These tests run the REAL drizzle query against seeded rows (via the pg-proxy
25
+ * driver, which hands the generated SQL and its parameters to a callback)
26
+ * rather than a stub that answers by call order. That is deliberate: a mock
27
+ * blind to the `where` clause returns the same row whether the guard asks
28
+ * about one branch end or both, so it would pass just as happily against the
29
+ * source-only code this fixes — it could not detect the regression it exists
30
+ * for. Here, reverting `openChangeRequestOn` to a source-only lookup makes the
31
+ * seeded `Y -> X` row stop matching, and these tests fail.
32
+ */
33
+
34
+ interface CrRow {
35
+ number: number;
36
+ source_branch: string;
37
+ target_branch: string;
38
+ state: string;
39
+ }
40
+
41
+ /**
42
+ * A `change_requests` table that answers the query it is actually given.
43
+ *
44
+ * The driver receives real SQL and real parameters, so the predicate is read
45
+ * back off the statement: every `"column" = $n` becomes a (column, value)
46
+ * pair. One value bound to SEVERAL columns is the either-end test and ORs
47
+ * across them (`source_branch = $1 or target_branch = $1`); distinct values
48
+ * AND together (`… and state = $2`). Non-`select` statements report one
49
+ * affected row, which is the guarded update in the sweep succeeding.
50
+ */
51
+ function fakeDb(rows: CrRow[]) {
52
+ const statements: string[] = [];
53
+ const db = drizzle(async (sql: string, params: unknown[]) => {
54
+ statements.push(sql);
55
+ if (!/^\s*select/i.test(sql)) return { rows: [['row-id']] };
56
+
57
+ const projection = (/^\s*select\s+(.+?)\s+from\s/is.exec(sql)?.[1] ?? '')
58
+ .split(',')
59
+ .map((item) => item.trim().replace(/.*"(\w+)"$/, '$1'));
60
+
61
+ const byValue = new Map<unknown, string[]>();
62
+ for (const m of sql.matchAll(/"(\w+)"\s*=\s*\$(\d+)/g)) {
63
+ const value = params[Number(m[2]) - 1];
64
+ byValue.set(value, [...(byValue.get(value) ?? []), m[1]!]);
65
+ }
66
+
67
+ // Reading the predicate off the statement means the reader can go stale
68
+ // against a future drizzle that quotes, aliases or parameterises
69
+ // differently. That is survivable ONLY if it fails loudly: an unparsed
70
+ // WHERE would leave `byValue` empty, `every` over nothing is vacuously
71
+ // true, and every seeded row would match — which is exactly the blind
72
+ // mock this harness replaced, silently restored. So refuse to answer a
73
+ // statement this cannot actually read.
74
+ if (!projection.length || projection.some((c) => !/^\w+$/.test(c))) {
75
+ throw new Error(`fakeDb could not read the projection of: ${sql}`);
76
+ }
77
+ if (/\bwhere\b/i.test(sql) && byValue.size === 0) {
78
+ throw new Error(`fakeDb could not read the predicate of: ${sql}`);
79
+ }
80
+
81
+ const matched = rows.filter((row) =>
82
+ [...byValue].every(([value, columns]) =>
83
+ columns.some((c) => row[c as keyof CrRow] === value),
84
+ ),
85
+ );
86
+ return { rows: matched.map((row) => projection.map((c) => row[c as keyof CrRow])) };
87
+ });
88
+ return { db: db as unknown as Database, statements };
89
+ }
90
+
91
+ function makeService(db: Database, git: Partial<GitService>) {
92
+ return new WorkflowService(
93
+ db,
94
+ git as GitService,
95
+ { invalidateDetailCache: vi.fn() } as unknown as PullRequestService,
96
+ {} as IReviewWorkflowService,
97
+ {
98
+ getOrCreateForBranch: vi.fn(async () => ({ id: 'ws-target' })),
99
+ ensureRemotesFetched: vi.fn(async () => undefined),
100
+ hasBootstrappedWorkspace: vi.fn(async () => false),
101
+ } as unknown as WorkspaceService,
102
+ {} as IAccessControl,
103
+ {} as FileLockService,
104
+ {} as PendingCommitsService,
105
+ 'knowledge-base',
106
+ );
107
+ }
108
+
109
+ const USER = { email: 'juan@example.com', name: 'Juan', id: 'u1' };
110
+
111
+ /** The merged request, and a second one still proposing INTO its source. */
112
+ const MERGED_23: CrRow = {
113
+ number: 23,
114
+ source_branch: 'juan/rfi',
115
+ target_branch: 'target-company-state',
116
+ state: 'merged',
117
+ };
118
+ const OPEN_24_INTO_RFI: CrRow = {
119
+ number: 24,
120
+ source_branch: 'juan/rfi-mock',
121
+ target_branch: 'juan/rfi',
122
+ state: 'open',
123
+ };
124
+
125
+ function retire(svc: WorkflowService, number: number, base: string) {
126
+ return (
127
+ svc as unknown as {
128
+ retireMergedSourceBranch(n: number, base: string, u: unknown): Promise<void>;
129
+ }
130
+ ).retireMergedSourceBranch(number, base, USER);
131
+ }
132
+
133
+ beforeEach(() => {
134
+ configureBranchModel({
135
+ defaultBranch: 'target-company-state',
136
+ protectedBranches: ['current-company-state', 'target-company-state'],
137
+ });
138
+ });
139
+
140
+ describe('post-merge branch retirement', () => {
141
+ it('keeps a branch another open request proposes INTO', async () => {
142
+ const deleted: string[] = [];
143
+ const { db, statements } = fakeDb([MERGED_23, OPEN_24_INTO_RFI]);
144
+ const svc = makeService(db, {
145
+ deleteBranch: vi.fn(async (_ws: string, name: string) => void deleted.push(name)),
146
+ });
147
+
148
+ await retire(svc, 23, 'target-company-state');
149
+
150
+ expect(deleted).toEqual([]);
151
+ // The guard must ask about both ends — a statement naming only
152
+ // `source_branch` is the regression.
153
+ expect(statements.some((q) => /target_branch/.test(q))).toBe(true);
154
+ });
155
+
156
+ it('still retires a branch nothing open needs', async () => {
157
+ const deleted: string[] = [];
158
+ // #24 is gone; only the merged request remains, so nothing needs the branch.
159
+ const { db } = fakeDb([MERGED_23]);
160
+ const svc = makeService(db, {
161
+ deleteBranch: vi.fn(async (_ws: string, name: string) => void deleted.push(name)),
162
+ });
163
+
164
+ await retire(svc, 23, 'target-company-state');
165
+
166
+ expect(deleted).toEqual(['juan/rfi']);
167
+ });
168
+
169
+ it('keeps a branch another open request proposes FROM', async () => {
170
+ const deleted: string[] = [];
171
+ const { db } = fakeDb([
172
+ MERGED_23,
173
+ { number: 25, source_branch: 'juan/rfi', target_branch: 'other', state: 'open' },
174
+ ]);
175
+ const svc = makeService(db, {
176
+ deleteBranch: vi.fn(async (_ws: string, name: string) => void deleted.push(name)),
177
+ });
178
+
179
+ await retire(svc, 23, 'target-company-state');
180
+
181
+ expect(deleted).toEqual([]);
182
+ });
183
+ });
184
+
185
+ describe('deleteBranch', () => {
186
+ it('refuses a branch an open request proposes INTO, naming who can act', async () => {
187
+ const { db } = fakeDb([OPEN_24_INTO_RFI]);
188
+ const svc = makeService(db, { deleteBranch: vi.fn() });
189
+
190
+ // Not "withdraw or decline it": the request belongs to someone else, and
191
+ // the branch owner may be authorized to do neither. The message has to
192
+ // say what the situation is and who can clear it.
193
+ await expect(svc.deleteBranch('ws', 'juan/rfi', USER)).rejects.toThrow(
194
+ /change request #24 proposes changes into "juan\/rfi"/,
195
+ );
196
+ });
197
+
198
+ it('refuses a branch an open request proposes FROM, offering the withdraw verb', async () => {
199
+ const { db } = fakeDb([
200
+ { number: 26, source_branch: 'juan/rfi', target_branch: 'target-company-state', state: 'open' },
201
+ ]);
202
+ const svc = makeService(db, { deleteBranch: vi.fn() });
203
+
204
+ await expect(svc.deleteBranch('ws', 'juan/rfi', USER)).rejects.toThrow(
205
+ /open change request \(#26\)\. Withdraw or decline it/,
206
+ );
207
+ });
208
+
209
+ it('allows a branch no open request names', async () => {
210
+ const deleted: string[] = [];
211
+ const { db } = fakeDb([OPEN_24_INTO_RFI]);
212
+ const svc = makeService(db, {
213
+ deleteBranch: vi.fn(async (_ws: string, name: string) => void deleted.push(name)),
214
+ });
215
+
216
+ await svc.deleteBranch('ws', 'juan/unrelated', USER);
217
+
218
+ expect(deleted).toEqual(['juan/unrelated']);
219
+ });
220
+ });
221
+
222
+ describe('runOnBranchPair', () => {
223
+ function deferred() {
224
+ let resolve!: () => void;
225
+ const promise = new Promise<void>((r) => { resolve = r; });
226
+ return { promise, resolve };
227
+ }
228
+
229
+ type LockInternals = {
230
+ branchLifecycle: { run(key: string, fn: () => Promise<void>): Promise<void> };
231
+ runOnBranchPair(source: string, target: string, fn: () => Promise<void>): Promise<void>;
232
+ };
233
+
234
+ function locks(svc: WorkflowService) {
235
+ return svc as unknown as LockInternals;
236
+ }
237
+
238
+ it('waits on an unprotected target: its key really is reserved', async () => {
239
+ const { db } = fakeDb([]);
240
+ const svc = locks(makeService(db, {}));
241
+ const order: string[] = [];
242
+ const release = deferred();
243
+
244
+ const holder = svc.branchLifecycle.run('branch:juan/rfi', async () => {
245
+ await release.promise;
246
+ order.push('holder');
247
+ });
248
+ await new Promise((r) => setImmediate(r));
249
+
250
+ const pair = svc.runOnBranchPair('juan/other', 'juan/rfi', async () => {
251
+ order.push('pair');
252
+ });
253
+ await new Promise((r) => setImmediate(r));
254
+ expect(order).toEqual([]);
255
+
256
+ release.resolve();
257
+ await Promise.all([holder, pair]);
258
+ expect(order).toEqual(['holder', 'pair']);
259
+ });
260
+
261
+ it('does not reserve a PROTECTED target key', async () => {
262
+ // No path in the system can delete a protected branch, so the key would
263
+ // exclude nothing while serializing every open into the default branch,
264
+ // which is where nearly all requests point. The pair operation must run
265
+ // even while something else holds the protected branch's key.
266
+ const { db } = fakeDb([]);
267
+ const svc = locks(makeService(db, {}));
268
+ const order: string[] = [];
269
+ const release = deferred();
270
+
271
+ const holder = svc.branchLifecycle.run('branch:target-company-state', async () => {
272
+ await release.promise;
273
+ order.push('holder');
274
+ });
275
+ await new Promise((r) => setImmediate(r));
276
+
277
+ await svc.runOnBranchPair('juan/other', 'target-company-state', async () => {
278
+ order.push('pair');
279
+ });
280
+ expect(order).toEqual(['pair']);
281
+
282
+ release.resolve();
283
+ await holder;
284
+ });
285
+
286
+ it('always reserves the source key', async () => {
287
+ const { db } = fakeDb([]);
288
+ const svc = locks(makeService(db, {}));
289
+ const order: string[] = [];
290
+ const release = deferred();
291
+
292
+ const holder = svc.branchLifecycle.run('branch:juan/other', async () => {
293
+ await release.promise;
294
+ order.push('holder');
295
+ });
296
+ await new Promise((r) => setImmediate(r));
297
+
298
+ const pair = svc.runOnBranchPair('juan/other', 'target-company-state', async () => {
299
+ order.push('pair');
300
+ });
301
+ await new Promise((r) => setImmediate(r));
302
+ expect(order).toEqual([]);
303
+
304
+ release.resolve();
305
+ await Promise.all([holder, pair]);
306
+ expect(order).toEqual(['holder', 'pair']);
307
+ });
308
+ });
309
+
310
+ describe('closeChangeRequestsWithDeletedBranches', () => {
311
+ it('closes a request whose TARGET branch is gone', async () => {
312
+ const { db } = fakeDb([OPEN_24_INTO_RFI]);
313
+ const svc = makeService(db, {
314
+ // #24's source lives on; its target `juan/rfi` is gone.
315
+ listBranches: vi.fn(async () => [
316
+ { name: 'target-company-state' },
317
+ { name: 'juan/rfi-mock' },
318
+ ]),
319
+ } as unknown as Partial<GitService>);
320
+
321
+ await expect(svc.closeChangeRequestsWithDeletedBranches()).resolves.toBe(1);
322
+ });
323
+
324
+ it('leaves a request whose branches both exist', async () => {
325
+ const { db } = fakeDb([
326
+ { ...OPEN_24_INTO_RFI, target_branch: 'target-company-state' },
327
+ ]);
328
+ const svc = makeService(db, {
329
+ listBranches: vi.fn(async () => [
330
+ { name: 'target-company-state' },
331
+ { name: 'juan/rfi-mock' },
332
+ ]),
333
+ } as unknown as Partial<GitService>);
334
+
335
+ await expect(svc.closeChangeRequestsWithDeletedBranches()).resolves.toBe(0);
336
+ });
337
+ });
@@ -83,6 +83,7 @@ function makeGit(overrides: Partial<{
83
83
  }),
84
84
  pull: vi.fn().mockImplementation(async () => {
85
85
  if (pullBehavior === 'fail') throw new Error('git pull failed: merge conflict');
86
+ return { treeChanged: true };
86
87
  }),
87
88
  } as unknown as GitService;
88
89
  }
@@ -114,7 +114,13 @@ describe('closeChangeRequestsWithDeletedBranches', () => {
114
114
  { name: 'x' },
115
115
  ]);
116
116
  await svc.closeChangeRequestsWithDeletedBranches();
117
- expect(git.listBranches).toHaveBeenCalledWith('target-company-state', { freshFetch: true });
117
+ expect(git.listBranches).toHaveBeenCalledWith('target-company-state', {
118
+ freshFetch: true,
119
+ // Strict: a fetch that cannot be proven fresh throws instead of serving
120
+ // stale refs, and the sweep's catch closes nothing. Without this, a
121
+ // branch created after the clone's last good fetch reads as "deleted".
122
+ strictFetch: true,
123
+ });
118
124
  });
119
125
 
120
126
  /**
@@ -16,14 +16,61 @@ import { PullRebaseConflictError } from '../../../shared/domain-errors.js';
16
16
  import type { Database } from '../../database/connection.js';
17
17
 
18
18
  // `deleteBranch`'s open-request guard is the only DB touch these tests
19
- // exercise: a chainable select→from→where→limit stub resolving `rows` covers
20
- // it (rows default to "no open request").
21
- function makeDb(rows: unknown[] = []): Database {
19
+ // exercise. The stub ANSWERS FROM THE WHERE-CLAUSE rather than returning the
20
+ // rows unfiltered — a guard that dropped the `state = 'open'` filter or asked
21
+ // about only one branch end used to pass these tests anyway, because any row
22
+ // came back regardless of the predicate.
23
+ //
24
+ // The evaluator walks the drizzle condition tree (duck-typing the
25
+ // SQL/Column/Param/StringChunk shapes) and evaluates it against the row:
26
+ // an `eq(column, value)` leaf compares the row's field, and a node combines
27
+ // its children with whichever connector its literal chunks carry (`and` /
28
+ // `or` — drizzle nests them, so one node never mixes both).
29
+ function rowMatches(cond: unknown, row: Record<string, unknown>): boolean {
30
+ // snake_case column → camelCase row field (the stubs carry row objects in
31
+ // the service's own field names).
32
+ const field = (col: string) => col.replace(/_([a-z])/g, (_, ch: string) => ch.toUpperCase());
33
+ const evalNode = (node: unknown): boolean => {
34
+ const chunks = (node as { queryChunks?: unknown[] }).queryChunks;
35
+ if (!Array.isArray(chunks)) return true;
36
+ const results: boolean[] = [];
37
+ let anyOr = false;
38
+ let pendingColumn: string | null = null;
39
+ for (const c of chunks) {
40
+ if (!c || typeof c !== 'object') continue;
41
+ if ('queryChunks' in c) {
42
+ results.push(evalNode(c));
43
+ } else if ('name' in c && 'keyAsName' in c) {
44
+ pendingColumn = String((c as { name: unknown }).name);
45
+ } else if ('encoder' in c && 'value' in c) {
46
+ if (pendingColumn !== null) {
47
+ results.push(row[field(pendingColumn)] === (c as { value: unknown }).value);
48
+ pendingColumn = null;
49
+ }
50
+ } else if ('value' in c) {
51
+ if (String((c as { value: unknown }).value).includes(' or ')) anyOr = true;
52
+ }
53
+ }
54
+ // A node this walker cannot decompose (`inArray`, `isNull`, a future
55
+ // rewrite of the query) must NOT read as a match — `true` here would be
56
+ // the permissive direction, returning rows the real query excludes and
57
+ // masking exactly the guard regressions these tests exist to catch.
58
+ if (results.length === 0) return false;
59
+ return anyOr ? results.some(Boolean) : results.every(Boolean);
60
+ };
61
+ return evalNode(cond);
62
+ }
63
+
64
+ function makeDb(rows: Record<string, unknown>[] = []): Database {
65
+ let lastWhere: unknown;
22
66
  const chain = {
23
67
  select: vi.fn(() => chain),
24
68
  from: vi.fn(() => chain),
25
- where: vi.fn(() => chain),
26
- limit: vi.fn(async () => rows),
69
+ where: vi.fn((cond: unknown) => {
70
+ lastWhere = cond;
71
+ return chain;
72
+ }),
73
+ limit: vi.fn(async () => rows.filter((r) => rowMatches(lastWhere, r))),
27
74
  };
28
75
  return chain as unknown as Database;
29
76
  }
@@ -135,7 +182,7 @@ function makeGit(): GitService {
135
182
  commit: vi.fn(),
136
183
  push: vi.fn(),
137
184
  fetch: vi.fn(),
138
- pull: vi.fn(),
185
+ pull: vi.fn().mockResolvedValue({ treeChanged: true }),
139
186
  diffStat: vi.fn(),
140
187
  pendingChanges: vi.fn(),
141
188
  resolveForkBase: vi.fn(),
@@ -435,11 +482,27 @@ describe('WorkflowService — file lock delegation', () => {
435
482
  it('deleteBranch refuses while the branch carries an open change request', async () => {
436
483
  const git = makeGit();
437
484
  // One open request rides the branch — deleting it would strand the request.
438
- const svc = new WorkflowService(makeDb([{ number: 7 }]), git, makePrs(), makeReviewWorkflow(), makeWorkspaceService(), makeAccessControl(), makeFileLockService(), makePendingCommits(), 'knowledge-base');
485
+ // `sourceBranch` matters to the guard now: it decides whether the refusal
486
+ // says "withdraw yours" (source) or names the other request's actors (target).
487
+ const svc = new WorkflowService(makeDb([{ number: 7, sourceBranch: 'feat/x', targetBranch: 'dev', state: 'open' }]), git, makePrs(), makeReviewWorkflow(), makeWorkspaceService(), makeAccessControl(), makeFileLockService(), makePendingCommits(), 'knowledge-base');
439
488
  await expect(svc.deleteBranch('w1', 'feat/x', makeUser())).rejects.toThrow(/open change request \(#7\)/);
440
489
  expect(git.deleteBranch).not.toHaveBeenCalled();
441
490
  });
442
491
 
492
+ it('deleteBranch refuses when an open request proposes INTO the branch (target end)', async () => {
493
+ const git = makeGit();
494
+ const svc = new WorkflowService(makeDb([{ number: 9, sourceBranch: 'other/y', targetBranch: 'feat/x', state: 'open' }]), git, makePrs(), makeReviewWorkflow(), makeWorkspaceService(), makeAccessControl(), makeFileLockService(), makePendingCommits(), 'knowledge-base');
495
+ await expect(svc.deleteBranch('w1', 'feat/x', makeUser())).rejects.toThrow(/proposes changes into/);
496
+ expect(git.deleteBranch).not.toHaveBeenCalled();
497
+ });
498
+
499
+ it('deleteBranch proceeds past a CLOSED request — the guard filters on state, not mere mention', async () => {
500
+ const git = makeGit();
501
+ const svc = new WorkflowService(makeDb([{ number: 7, sourceBranch: 'feat/x', targetBranch: 'dev', state: 'closed' }]), git, makePrs(), makeReviewWorkflow(), makeWorkspaceService(), makeAccessControl(), makeFileLockService(), makePendingCommits(), 'knowledge-base');
502
+ await expect(svc.deleteBranch('w1', 'feat/x', makeUser())).resolves.toBeUndefined();
503
+ expect(git.deleteBranch).toHaveBeenCalled();
504
+ });
505
+
443
506
  it('deleteBranch deletes when the branch has no open change request', async () => {
444
507
  const git = makeGit();
445
508
  const svc = new WorkflowService(makeDb(), git, makePrs(), makeReviewWorkflow(), makeWorkspaceService(), makeAccessControl(), makeFileLockService(), makePendingCommits(), 'knowledge-base');
@@ -496,7 +559,7 @@ describe('WorkflowService — revertChangeRequestFile / closeEmptyChangeRequest'
496
559
 
497
560
  function makeRevertGit(overrides: Record<string, unknown> = {}): GitService {
498
561
  return Object.assign(makeGit(), {
499
- pull: vi.fn().mockResolvedValue(undefined),
562
+ pull: vi.fn().mockResolvedValue({ treeChanged: true }),
500
563
  changedPathsForPr: vi.fn().mockResolvedValue(['Docs/a.md', 'Docs/b.md']),
501
564
  mergeBaseForPr: vi.fn().mockResolvedValue('mb-sha'),
502
565
  restorePathFromRef: vi.fn().mockResolvedValue(undefined),
@@ -656,8 +719,9 @@ describe('WorkflowService — deleteChangeRequest (admin moderation verb)', () =
656
719
  set: vi.fn(() => chain),
657
720
  returning: vi.fn(async () => [{ id: 1 }]),
658
721
  });
659
- const svc = new WorkflowService(chain as unknown as Database, git, prs, makeReviewWorkflow(), makeWorkspaceService(), access, makeFileLockService(), makePendingCommits(), 'knowledge-base');
660
- return { svc, prs, db: chain, access };
722
+ const workspaces = makeWorkspaceService();
723
+ const svc = new WorkflowService(chain as unknown as Database, git, prs, makeReviewWorkflow(), workspaces, access, makeFileLockService(), makePendingCommits(), 'knowledge-base');
724
+ return { svc, prs, db: chain, access, workspaces };
661
725
  }
662
726
 
663
727
  it('refuses a non-admin with 403 and touches nothing', async () => {
@@ -678,4 +742,27 @@ describe('WorkflowService — deleteChangeRequest (admin moderation verb)', () =
678
742
  await expect(svc.deleteChangeRequest(9, makeUser())).rejects.toMatchObject({ status: 422 });
679
743
  expect(db.update).not.toHaveBeenCalled();
680
744
  });
745
+
746
+ it('kicks the deleted-branch sweep when the base workspace cannot be resolved', async () => {
747
+ const { svc, db, workspaces } = makeDeleteHarness({ isAdmin: true });
748
+ (workspaces.getOrCreateForBranch as ReturnType<typeof vi.fn>).mockRejectedValue(
749
+ new Error('Remote branch main not found in upstream origin'),
750
+ );
751
+ const sweep = vi.spyOn(svc, 'closeChangeRequestsWithDeletedBranches').mockResolvedValue(0);
752
+
753
+ // The failure still surfaces (it is not proven absence, so the verb must
754
+ // not pretend to succeed), but the sweep is kicked so a genuinely
755
+ // stranded request is closed without waiting for a restart.
756
+ await expect(svc.deleteChangeRequest(9, makeUser())).rejects.toThrow(/not found/);
757
+ expect(sweep).toHaveBeenCalledTimes(1);
758
+ expect(db.update).not.toHaveBeenCalled();
759
+ });
760
+
761
+ it('does not kick the sweep on a plain authorization refusal', async () => {
762
+ const { svc, db } = makeDeleteHarness({ isAdmin: false });
763
+ const sweep = vi.spyOn(svc, 'closeChangeRequestsWithDeletedBranches').mockResolvedValue(0);
764
+ await expect(svc.deleteChangeRequest(9, makeUser())).rejects.toMatchObject({ status: 403 });
765
+ expect(sweep).not.toHaveBeenCalled();
766
+ expect(db.update).not.toHaveBeenCalled();
767
+ });
681
768
  });
@@ -8,9 +8,10 @@ import type { IAccessControl } from '../../access/access-control.interface.js';
8
8
  import { FileLockService } from '../file-lock.service.js';
9
9
  import { PendingCommitsService } from '../pending-commits.service.js';
10
10
  import { WorkflowEventBus } from '../event-bus.js';
11
- import { WorkflowService } from '../workflow.service.js';
11
+ import { WorkflowService, syncConflictMessage } from '../workflow.service.js';
12
12
  import type { Database } from '../../database/connection.js';
13
13
  import {
14
+ PullRebaseConflictError,
14
15
  PullRebaseConflictError,
15
16
  PushNeedsAgentResolutionError,
16
17
  WorkflowValidationError,
@@ -87,7 +88,7 @@ function makePending(): PendingCommitsService {
87
88
  function makeGit(overrides: Partial<{
88
89
  commitFile: Change | null;
89
90
  pushBehavior: 'ok' | 'nff' | 'auth-fail';
90
- pullBehavior: 'ok' | 'fail';
91
+ pullBehavior: 'ok' | 'fail' | 'conflict';
91
92
  pushAfterPullBehavior: 'ok' | 'fail';
92
93
  hasUnpushedCommits: boolean;
93
94
  }> = {}): GitService {
@@ -120,6 +121,8 @@ function makeGit(overrides: Partial<{
120
121
  }),
121
122
  pull: vi.fn().mockImplementation(async () => {
122
123
  if (pullBehavior === 'fail') throw new Error('git pull failed: merge conflict');
124
+ if (pullBehavior === 'conflict') throw new PullRebaseConflictError('feat/x', ['foo.md'], 'CONFLICT (content)');
125
+ return { treeChanged: true };
123
126
  }),
124
127
  } as unknown as GitService;
125
128
  }
@@ -460,6 +463,29 @@ describe('WorkflowService.runPendingCommit — worker entry point', () => {
460
463
  ]);
461
464
  });
462
465
 
466
+ it('a rebase CONFLICT on the recovery path raises the banner WITH its files, like the remote sync does', async () => {
467
+ // The banner keeps the latest failure per branch. The remote sync raises
468
+ // the conflict variant (files as links); if this path then raised the
469
+ // generic variant for the same conflict, the retry would take the links
470
+ // away — seen on staging. Every path names the files.
471
+ const git = makeGit({
472
+ commitFile: null,
473
+ hasUnpushedCommits: true,
474
+ pushBehavior: 'nff',
475
+ pullBehavior: 'conflict',
476
+ });
477
+ const svc = makeFacade(git, makeFileLocks(USER.id), makePending(), events);
478
+ await expect(svc.runPendingCommit('ws-1', 'feat/x', 'foo.md', USER)).rejects.toBeInstanceOf(
479
+ PushNeedsAgentResolutionError,
480
+ );
481
+ const failed = emitSpy.mock.calls.map((c) => c[0] as Record<string, unknown>).find((e) => e.kind === 'git-sync-failed');
482
+ expect(failed).toMatchObject({
483
+ branch: 'feat/x',
484
+ conflictedPaths: ['foo.md'],
485
+ reason: syncConflictMessage('feat/x', ['foo.md']),
486
+ });
487
+ });
488
+
463
489
  it('recovers from non-fast-forward push via pull --rebase + retry, emits file-changed exactly once', async () => {
464
490
  // Teammate pushed to the same branch between our commit and our
465
491
  // push. The cooperative path pulls + retries — this is the case
@@ -750,3 +776,94 @@ describe('WorkflowService.updateFromRemote — pull-conflict recovery dispatch',
750
776
  await expect(svc.updateFromRemote('main', USER)).rejects.toBe(CONFLICT);
751
777
  });
752
778
  });
779
+
780
+ describe('WorkflowService.updateFromRemote — the tree-change announcement', () => {
781
+ /**
782
+ * The announcement under test is what keeps the DEFAULT branch's catalog
783
+ * caches (skills, tool manuals, plugin index) honest: a pull is the one way
784
+ * a tree changes without passing the write routes, so `pullWorkspace` owes
785
+ * the process an `fs-tree-changed` — and owes it ONLY when the pull moved
786
+ * HEAD, on the default branch, after the pull resolved. Asserted through
787
+ * `updateFromRemote`, the public verb that wraps `pullWorkspace`.
788
+ */
789
+ const DEFAULT_WS = 'target-company-state'; // == the test env's DEFAULT_BRANCH
790
+
791
+ function makePullingGit(result: { treeChanged: boolean } | Error): {
792
+ git: GitService;
793
+ pullSettled: () => boolean;
794
+ } {
795
+ let settled = false;
796
+ const git = {
797
+ pull: vi.fn().mockImplementation(async () => {
798
+ // A microtask gap, so an emit issued before the await would be
799
+ // observably premature rather than coincidentally ordered.
800
+ await Promise.resolve();
801
+ settled = true;
802
+ if (result instanceof Error) throw result;
803
+ return result;
804
+ }),
805
+ } as unknown as GitService;
806
+ return { git, pullSettled: () => settled };
807
+ }
808
+
809
+ function treeChangedEvents(emitSpy: MockInstance): unknown[] {
810
+ return emitSpy.mock.calls
811
+ .map((c) => c[0] as { kind: string })
812
+ .filter((e) => e.kind === 'fs-tree-changed');
813
+ }
814
+
815
+ it('emits fs-tree-changed for the default workspace once the pull has changed the tree', async () => {
816
+ const events = new WorkflowEventBus();
817
+ const emitSpy = vi.spyOn(events, 'emit');
818
+ const { git, pullSettled } = makePullingGit({ treeChanged: true });
819
+ let settledAtEmit: boolean | undefined;
820
+ emitSpy.mockImplementation(() => {
821
+ settledAtEmit = pullSettled();
822
+ });
823
+ const svc = makeFacade(git, makeFileLocks(USER.id), makePending(), events);
824
+
825
+ await svc.updateFromRemote(DEFAULT_WS, USER);
826
+
827
+ expect(treeChangedEvents(emitSpy)).toEqual([
828
+ { kind: 'fs-tree-changed', workspaceId: DEFAULT_WS, branch: DEFAULT_WS },
829
+ ]);
830
+ // After the pull RESOLVED — an emit for a pull that later failed would
831
+ // make other clients refetch a tree that never changed.
832
+ expect(settledAtEmit).toBe(true);
833
+ });
834
+
835
+ it('stays silent when the pull found nothing new (treeChanged: false)', async () => {
836
+ const events = new WorkflowEventBus();
837
+ const emitSpy = vi.spyOn(events, 'emit');
838
+ const { git } = makePullingGit({ treeChanged: false });
839
+ const svc = makeFacade(git, makeFileLocks(USER.id), makePending(), events);
840
+
841
+ await svc.updateFromRemote(DEFAULT_WS, USER);
842
+
843
+ // An "already up to date" sync must not drop the catalogs or make every
844
+ // attached browser refetch its file tree.
845
+ expect(treeChangedEvents(emitSpy)).toEqual([]);
846
+ });
847
+
848
+ it('stays silent for a non-default workspace even when the tree changed', async () => {
849
+ const events = new WorkflowEventBus();
850
+ const emitSpy = vi.spyOn(events, 'emit');
851
+ const { git } = makePullingGit({ treeChanged: true });
852
+ const svc = makeFacade(git, makeFileLocks(USER.id), makePending(), events);
853
+
854
+ await svc.updateFromRemote(encodeURIComponent('alice/draft'), USER);
855
+
856
+ expect(treeChangedEvents(emitSpy)).toEqual([]);
857
+ });
858
+
859
+ it('stays silent when the pull throws', async () => {
860
+ const events = new WorkflowEventBus();
861
+ const emitSpy = vi.spyOn(events, 'emit');
862
+ const { git } = makePullingGit(new Error('git fetch failed: could not resolve host'));
863
+ const svc = makeFacade(git, makeFileLocks(USER.id), makePending(), events);
864
+
865
+ await expect(svc.updateFromRemote(DEFAULT_WS, USER)).rejects.toThrow();
866
+
867
+ expect(treeChangedEvents(emitSpy)).toEqual([]);
868
+ });
869
+ });