@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
@@ -7,6 +7,7 @@ import type {
7
7
  BranchInfo,
8
8
  CommitAttribution,
9
9
  IGitService,
10
+ RemoteSyncPullResult,
10
11
  PrFileStatus,
11
12
  PullRequestFile,
12
13
  ShareChangesRequest,
@@ -32,6 +33,8 @@ import {
32
33
  WorkflowValidationError,
33
34
  ProtectedBranchError,
34
35
  PullRebaseConflictError,
36
+ RemoteBranchGoneError,
37
+ isMissingRemoteBranchFailure,
35
38
  } from '../../../shared/domain-errors.js';
36
39
 
37
40
  const execFileAsync = promisify(execFile);
@@ -271,6 +274,8 @@ export class GitService implements IGitService {
271
274
  // worst case for branch-switching UX).
272
275
  private readonly fetchLocks = new Map<string, Promise<void>>();
273
276
  private readonly lastImplicitFetchAt = new Map<string, number>();
277
+ /** Whether the last implicit fetch per workspace SUCCEEDED — see `fetchOriginIfStale`. */
278
+ private readonly lastImplicitFetchOk = new Map<string, boolean>();
274
279
  private static readonly IMPLICIT_FETCH_TTL_MS = 5_000;
275
280
  // Cap on concurrent `aheadBehindForBranch` calls inside `listBranches`. Each
276
281
  // call serializes up to 3 `git rev-list` spawns, so 8 workers ≈ ≤24
@@ -315,6 +320,11 @@ export class GitService implements IGitService {
315
320
  */
316
321
  noteWorkspaceFetched(workspaceId: string): void {
317
322
  this.lastImplicitFetchAt.set(workspaceId, Date.now());
323
+ // A fresh clone IS a successful fetch. Without this, a workspace
324
+ // recreated after some earlier failed fetch inherits the stale `false`
325
+ // under the same id, and a strict listBranches inside the TTL window
326
+ // refuses a list that is in fact provably fresh.
327
+ this.lastImplicitFetchOk.set(workspaceId, true);
318
328
  }
319
329
 
320
330
  /**
@@ -399,7 +409,7 @@ export class GitService implements IGitService {
399
409
 
400
410
  async listBranches(
401
411
  workspaceId: string,
402
- opts: { freshFetch?: boolean } = {},
412
+ opts: { freshFetch?: boolean; strictFetch?: boolean } = {},
403
413
  ): Promise<BranchInfo[]> {
404
414
  const cwd = await this.repoDir(workspaceId);
405
415
  // Run the fetch BEFORE entering the mutex so a slow origin can't block
@@ -409,7 +419,18 @@ export class GitService implements IGitService {
409
419
  // bypasses the TTL — used for user-initiated refreshes (opening the branch
410
420
  // selector) so a draft another workspace just deleted is pruned right away
411
421
  // instead of lingering up to one TTL window.
412
- await this.fetchOriginIfStale(cwd, workspaceId, opts.freshFetch);
422
+ const fresh = await this.fetchOriginIfStale(cwd, workspaceId, opts.freshFetch);
423
+ // `strictFetch` is for callers that USE the list to prove absence — the
424
+ // deleted-branch sweep closes change requests on it. The degrade-to-stale
425
+ // behaviour below is right for a UI listing (stale branches beat a 500),
426
+ // and exactly wrong for an absence proof: a clone that last fetched
427
+ // before a branch was created reports it missing. Such a caller gets a
428
+ // thrown error instead of a list it must not trust.
429
+ if (opts.strictFetch && !fresh) {
430
+ throw new Error(
431
+ 'the fetch from origin failed, so the branch list cannot prove absence — refusing to serve a stale list to a strict caller',
432
+ );
433
+ }
413
434
  return this.mutex.run(workspaceId, async () => {
414
435
  // Union local heads with origin's remote-tracking refs: a fresh per-user
415
436
  // clone only has one local head (HEAD's default), so without this we'd hide
@@ -509,7 +530,10 @@ export class GitService implements IGitService {
509
530
  * onto a single in-flight fetch instead of each waiting their turn behind
510
531
  * the network round-trip. Failures (offline, bad auth, no remote) are
511
532
  * logged and swallowed so the branch list still serves stale local data
512
- * instead of returning 500.
533
+ * instead of returning 500 — the RETURN VALUE says whether the refs are
534
+ * provably fresh (`true`: this call fetched, or shared a fetch, that
535
+ * succeeded; also `true` on a TTL skip, whose window a recent success
536
+ * opened). Callers that need proof (strict listing) read it.
513
537
  *
514
538
  * Safe to keep outside the mutex: fetch only writes remote-tracking refs
515
539
  * and the object store, neither of which checkout/status/commit touch.
@@ -518,9 +542,11 @@ export class GitService implements IGitService {
518
542
  cwd: string,
519
543
  workspaceId: string,
520
544
  force = false,
521
- ): Promise<void> {
545
+ ): Promise<boolean> {
522
546
  const last = this.lastImplicitFetchAt.get(workspaceId) ?? 0;
523
- if (!force && Date.now() - last < GitService.IMPLICIT_FETCH_TTL_MS) return;
547
+ if (!force && Date.now() - last < GitService.IMPLICIT_FETCH_TTL_MS) {
548
+ return this.lastImplicitFetchOk.get(workspaceId) ?? true;
549
+ }
524
550
 
525
551
  const inFlight = this.fetchLocks.get(workspaceId);
526
552
  if (inFlight) {
@@ -529,7 +555,7 @@ export class GitService implements IGitService {
529
555
  // the same promise also means subsequent listBranches calls see the
530
556
  // newly fetched refs.
531
557
  await inFlight;
532
- return;
558
+ return this.lastImplicitFetchOk.get(workspaceId) ?? false;
533
559
  }
534
560
 
535
561
  const runFetch = async (): Promise<void> => {
@@ -546,6 +572,7 @@ export class GitService implements IGitService {
546
572
  ...SAFE_IMPLICIT_FETCH_ARGS,
547
573
  ]);
548
574
  this.lastImplicitFetchAt.set(workspaceId, Date.now());
575
+ this.lastImplicitFetchOk.set(workspaceId, true);
549
576
  } catch (err) {
550
577
  const msg = err instanceof Error ? err.message : String(err);
551
578
  console.warn(`[git] implicit fetch failed for workspace ${workspaceId}: ${msg}`);
@@ -553,12 +580,14 @@ export class GitService implements IGitService {
553
580
  // every subsequent listBranches retry the (still-failing) fetch and
554
581
  // pile up 10s timeouts. Stale local refs are better than spinning.
555
582
  this.lastImplicitFetchAt.set(workspaceId, Date.now());
583
+ this.lastImplicitFetchOk.set(workspaceId, false);
556
584
  }
557
585
  };
558
586
  const p = runFetch();
559
587
  this.fetchLocks.set(workspaceId, p);
560
588
  try {
561
589
  await p;
590
+ return this.lastImplicitFetchOk.get(workspaceId) ?? false;
562
591
  } finally {
563
592
  // Drop the lock once the fetch settles so the next caller past the TTL
564
593
  // can spawn a fresh one. Compare-and-delete in case a future race
@@ -1520,70 +1549,250 @@ export class GitService implements IGitService {
1520
1549
  return `refs/remotes/origin/${branch}`;
1521
1550
  }
1522
1551
 
1523
- async pull(workspaceId: string): Promise<void> {
1552
+ /** A commit-free tree, for diffing against a HEAD that did not exist yet. */
1553
+ private static readonly EMPTY_TREE_SHA = '4b825dc642cb6eb9a060e54bf8d69288fbee4904';
1554
+
1555
+ /**
1556
+ * The checked-out branch by its symbolic ref, which an UNBORN HEAD (a clone
1557
+ * of an empty upstream) still has — `rev-parse --abbrev-ref HEAD` does not
1558
+ * resolve there, so the tolerant tree reads below would never be reached.
1559
+ */
1560
+ private async checkedOutBranch(cwd: string): Promise<string> {
1561
+ const { stdout } = await this.git(cwd, ['symbolic-ref', '--short', 'HEAD']);
1562
+ return stdout.trim();
1563
+ }
1564
+
1565
+ /**
1566
+ * Whether origin still has `branch`. `ls-remote --exit-code` answers with
1567
+ * exit status 2 for "no such ref" — the one failure that is a fact about the
1568
+ * branch rather than about reaching the host, which propagates.
1569
+ */
1570
+ async remoteBranchExists(workspaceId: string, branch: string): Promise<boolean> {
1571
+ assertValidBranchName(branch);
1572
+ const cwd = await this.repoDir(workspaceId);
1573
+ try {
1574
+ await this.git(cwd, ['ls-remote', '--exit-code', '--heads', 'origin', `refs/heads/${branch}`]);
1575
+ return true;
1576
+ } catch (err) {
1577
+ if ((err as { exitCode?: number }).exitCode === 2) return false;
1578
+ throw err;
1579
+ }
1580
+ }
1581
+
1582
+ /** Whether origin has ANY branch — an empty repository has none. */
1583
+ private async remoteHasAnyBranch(cwd: string): Promise<boolean> {
1584
+ const { stdout } = await this.git(cwd, ['ls-remote', '--heads', 'origin']);
1585
+ return stdout.trim().length > 0;
1586
+ }
1587
+
1588
+ /** `git rev-parse --verify` that answers null for a rev that does not resolve (an unborn HEAD). */
1589
+ private async revParseOrNull(cwd: string, rev: string): Promise<string | null> {
1590
+ try {
1591
+ const { stdout } = await this.git(cwd, ['rev-parse', '--verify', '--quiet', rev]);
1592
+ return stdout.trim() || null;
1593
+ } catch {
1594
+ return null;
1595
+ }
1596
+ }
1597
+
1598
+ /**
1599
+ * Refresh `refs/remotes/origin/<branch>` for a sync, naming the one failure
1600
+ * that is not "origin unreachable": the branch no longer exists there.
1601
+ */
1602
+ private async refreshRemoteBranchRefForSync(cwd: string, branch: string): Promise<string> {
1603
+ try {
1604
+ return await this.refreshRemoteBranchRef(cwd, branch);
1605
+ } catch (err) {
1606
+ const msg = err instanceof Error ? err.message : String(err);
1607
+ if (isMissingRemoteBranchFailure(msg)) throw new RemoteBranchGoneError(branch);
1608
+ throw err;
1609
+ }
1610
+ }
1611
+
1612
+ /**
1613
+ * Rebase the checked-out branch onto `remoteRef`, turning a content
1614
+ * conflict into the typed error the workflow layer queues recovery for.
1615
+ * Shared by `pull` and `syncFromRemote` so the two cannot drift.
1616
+ */
1617
+ private async rebaseOntoRemote(cwd: string, branch: string, remoteRef: string): Promise<void> {
1618
+ // An UNBORN HEAD (a clone of an upstream that was empty when cloned) has
1619
+ // no commit to replay and `git rebase` refuses to start from it, so the
1620
+ // branch is moved to the remote commit instead. That is only safe while
1621
+ // the index is EMPTY: files staged in an unborn clone are tracked, and a
1622
+ // hard reset would destroy them where the normal path autostashes — and
1623
+ // `git stash` cannot run without an initial commit. So a non-empty index
1624
+ // is refused as a conflict, naming the staged paths, and takes the same
1625
+ // recovery path a rebase conflict does; nothing on disk is touched.
1626
+ if ((await this.revParseOrNull(cwd, 'HEAD')) === null) {
1627
+ // `-z`: NUL-delimited and unquoted. Line output quotes a path with a
1628
+ // non-ASCII or control character ("Entw\303\274rfe.md"), and that
1629
+ // spelling would name a file that does not exist to whoever recovers.
1630
+ const { stdout: stagedOut } = await this.git(cwd, ['ls-files', '--cached', '-z']);
1631
+ const staged = stagedOut.split('\0').filter(Boolean);
1632
+ if (staged.length > 0) {
1633
+ throw new PullRebaseConflictError(
1634
+ branch,
1635
+ staged,
1636
+ 'The clone has files staged before its first commit; they cannot be replayed onto the remote branch automatically.',
1637
+ );
1638
+ }
1639
+ await this.git(cwd, ['reset', '--hard', remoteRef]);
1640
+ return;
1641
+ }
1642
+ try {
1643
+ // `--autostash` is load-bearing for the cooperative push recovery.
1644
+ // Under save=share the working tree is *supposed* to stay clean, but
1645
+ // in practice it can carry modified tracked artifacts (e.g. the
1646
+ // validator regenerates a dashboard HTML on every commit) that the
1647
+ // single-file commit didn't pick up. A rebase without it aborts
1648
+ // outright on those — "cannot rebase: You have unstaged
1649
+ // changes" — which is exactly the failure that strands a diverged
1650
+ // branch in an unrecoverable loop (the retry push stays non-fast-
1651
+ // forward forever). Autostash stashes those modifications, rebases
1652
+ // the local commits onto origin, then reapplies them, so the retry
1653
+ // push can fast-forward. Untracked files don't block rebase and are
1654
+ // left untouched.
1655
+ await this.git(cwd, ['rebase', '--autostash', remoteRef]);
1656
+ } catch (err) {
1657
+ // Capture the contested paths BEFORE the abort wipes the rebase
1658
+ // state — `--diff-filter=U` lists exactly the files whose replay
1659
+ // conflicted. Best-effort: an empty list just means this wasn't a
1660
+ // content conflict (or the probe itself failed) and the raw error
1661
+ // is surfaced unchanged.
1662
+ let conflictedPaths: string[] = [];
1663
+ try {
1664
+ const { stdout } = await this.git(cwd, ['diff', '--name-only', '--diff-filter=U']);
1665
+ conflictedPaths = stdout.split('\n').map((s) => s.trim()).filter(Boolean);
1666
+ } catch {
1667
+ // fall through with an empty list
1668
+ }
1669
+ // A failed rebase leaves the repo in a "REBASE_HEAD" / detached-apply state,
1670
+ // which makes every subsequent git command behave unexpectedly. Return the
1671
+ // working tree to a clean state before surfacing the original error.
1672
+ let abortSucceeded = true;
1673
+ await this.git(cwd, ['rebase', '--abort']).catch(() => {
1674
+ abortSucceeded = false;
1675
+ });
1676
+ if (abortSucceeded && conflictedPaths.length > 0) {
1677
+ // Typed so the workflow layer can queue background recovery — this
1678
+ // divergence never resolves on its own (every retry pull hits the
1679
+ // same conflict) and the unpushed local commits are someone's saved
1680
+ // content stuck violating save=share.
1681
+ //
1682
+ // Gated on the abort succeeding: a failed abort means either no
1683
+ // rebase was ever active (the unmerged paths pre-date this pull —
1684
+ // some earlier operation left the index broken) or the clone is in
1685
+ // a state git itself can't unwind. Queueing recovery there would
1686
+ // point the worker's commitFile at a repo mid-conflict, risking a
1687
+ // commit full of conflict markers — surface the raw error instead
1688
+ // and leave diagnosis to a human.
1689
+ throw new PullRebaseConflictError(
1690
+ branch,
1691
+ conflictedPaths,
1692
+ err instanceof Error ? err.message : String(err),
1693
+ );
1694
+ }
1695
+ throw err;
1696
+ }
1697
+ // `--autostash` reapplies the stashed edits AFTER the rebase, and when that
1698
+ // reapply conflicts git exits 0: the clone is left with an unmerged index,
1699
+ // the edits kept in `stash@{0}`, and nothing above has thrown. Read the
1700
+ // index rather than the exit status. The rebase itself landed, so the
1701
+ // branch is at the remote commit; resetting to it drops only the
1702
+ // half-applied stash (still in the stash list) and lets the conflict
1703
+ // travel the same typed path a rebase conflict does.
1704
+ const { stdout: unmergedOut } = await this.git(cwd, ['diff', '--name-only', '--diff-filter=U']);
1705
+ const unmerged = unmergedOut.split('\n').map((l) => l.trim()).filter(Boolean);
1706
+ if (unmerged.length > 0) {
1707
+ await this.git(cwd, ['reset', '--hard', 'HEAD']);
1708
+ throw new PullRebaseConflictError(
1709
+ branch,
1710
+ unmerged,
1711
+ 'Reapplying the autostashed working-tree edits conflicted; the edits are kept in git stash.',
1712
+ );
1713
+ }
1714
+ }
1715
+
1716
+ /**
1717
+ * Repo-relative paths whose content differs between two commits. Rename-
1718
+ * aware: a renamed file reports both its old and new path, so a viewer
1719
+ * holding the OLD path learns it is gone. Caller holds the mutex.
1720
+ */
1721
+ private async changedPathsBetween(cwd: string, fromSha: string, toSha: string): Promise<string[]> {
1722
+ const { stdout } = await this.git(cwd, [
1723
+ 'diff', '--name-status', '-M', '--no-color', '-z', fromSha, toSha,
1724
+ ]);
1725
+ const fields = stdout.split('\0');
1726
+ const paths = new Set<string>();
1727
+ for (let i = 0; i < fields.length; ) {
1728
+ const status = fields[i];
1729
+ if (!status) break;
1730
+ if (status.startsWith('R') || status.startsWith('C')) {
1731
+ if (fields[i + 1]) paths.add(fields[i + 1]);
1732
+ if (fields[i + 2]) paths.add(fields[i + 2]);
1733
+ i += 3;
1734
+ } else {
1735
+ if (fields[i + 1]) paths.add(fields[i + 1]);
1736
+ i += 2;
1737
+ }
1738
+ }
1739
+ return [...paths];
1740
+ }
1741
+
1742
+ async syncFromRemote(workspaceId: string): Promise<RemoteSyncPullResult> {
1743
+ // ONE mutex section for everything the sync reports about this clone.
1744
+ // Bracketing `pull` with separate HEAD reads and a diff took the hold
1745
+ // four times, and a save landing between any two of them was announced
1746
+ // back to its author as a "Git sync" change with the wrong sha.
1524
1747
  return this.mutex.run(workspaceId, async () => {
1525
1748
  const cwd = await this.repoDir(workspaceId);
1526
- const branch = await this.currentBranch(cwd);
1749
+ const branch = await this.checkedOutBranch(cwd);
1750
+ const before = await this.revParseOrNull(cwd, 'HEAD');
1751
+ const treeBefore = (await this.revParseOrNull(cwd, 'HEAD^{tree}')) ?? '';
1752
+ let remoteRef: string;
1527
1753
  try {
1528
- // Refresh WITHOUT `git pull`, and without touching `FETCH_HEAD` — see
1529
- // `refreshRemoteBranchRef` for why both halves are load-bearing.
1530
- //
1531
- // `--autostash` is load-bearing for the cooperative push recovery.
1532
- // Under save=share the working tree is *supposed* to stay clean, but
1533
- // in practice it can carry modified tracked artifacts (e.g. the
1534
- // validator regenerates a dashboard HTML on every commit) that the
1535
- // single-file commit didn't pick up. A rebase without it aborts
1536
- // outright on those — "cannot rebase: You have unstaged
1537
- // changes" — which is exactly the failure that strands a diverged
1538
- // branch in an unrecoverable loop (the retry push stays non-fast-
1539
- // forward forever). Autostash stashes those modifications, rebases
1540
- // the local commits onto origin, then reapplies them, so the retry
1541
- // push can fast-forward. Untracked files don't block rebase and are
1542
- // left untouched.
1543
- const remoteRef = await this.refreshRemoteBranchRef(cwd, branch);
1544
- await this.git(cwd, ['rebase', '--autostash', remoteRef]);
1754
+ remoteRef = await this.refreshRemoteBranchRefForSync(cwd, branch);
1545
1755
  } catch (err) {
1546
- // Capture the contested paths BEFORE the abort wipes the rebase
1547
- // state — `--diff-filter=U` lists exactly the files whose replay
1548
- // conflicted. Best-effort: an empty list just means this wasn't a
1549
- // content conflict (or the probe itself failed) and the raw error
1550
- // is surfaced unchanged.
1551
- let conflictedPaths: string[] = [];
1552
- try {
1553
- const { stdout } = await this.git(cwd, ['diff', '--name-only', '--diff-filter=U']);
1554
- conflictedPaths = stdout.split('\n').map((s) => s.trim()).filter(Boolean);
1555
- } catch {
1556
- // fall through with an empty list
1557
- }
1558
- // A failed rebase leaves the repo in a "REBASE_HEAD" / detached-apply state,
1559
- // which makes every subsequent git command behave unexpectedly. Return the
1560
- // working tree to a clean state before surfacing the original error.
1561
- let abortSucceeded = true;
1562
- await this.git(cwd, ['rebase', '--abort']).catch(() => {
1563
- abortSucceeded = false;
1564
- });
1565
- if (abortSucceeded && conflictedPaths.length > 0) {
1566
- // Typed so the workflow layer can queue background recovery — this
1567
- // divergence never resolves on its own (every retry pull hits the
1568
- // same conflict) and the unpushed local commits are someone's saved
1569
- // content stuck violating save=share.
1570
- //
1571
- // Gated on the abort succeeding: a failed abort means either no
1572
- // rebase was ever active (the unmerged paths pre-date this pull —
1573
- // some earlier operation left the index broken) or the clone is in
1574
- // a state git itself can't unwind. Queueing recovery there would
1575
- // point the worker's commitFile at a repo mid-conflict, risking a
1576
- // commit full of conflict markers — surface the raw error instead
1577
- // and leave diagnosis to a human.
1578
- throw new PullRebaseConflictError(
1579
- branch,
1580
- conflictedPaths,
1581
- err instanceof Error ? err.message : String(err),
1582
- );
1756
+ // An unborn clone whose branch origin does not have either: on a FRESH
1757
+ // deployment (origin has no branch at all) nobody has pushed yet —
1758
+ // nothing to sync, nothing to retire. But an origin that does have
1759
+ // branches and lacks this one has had this branch created and deleted
1760
+ // since the clone was made, and the clone is as stale as any other.
1761
+ if (err instanceof RemoteBranchGoneError && before === null && !(await this.remoteHasAnyBranch(cwd))) {
1762
+ return { before: null, after: null, treeChanged: false, changedPaths: [] };
1583
1763
  }
1584
1764
  throw err;
1585
1765
  }
1766
+ await this.rebaseOntoRemote(cwd, branch, remoteRef);
1586
1767
  this.accessControl?.invalidate(workspaceId);
1768
+ const after = await this.revParseOrNull(cwd, 'HEAD');
1769
+ const treeAfter = (await this.revParseOrNull(cwd, 'HEAD^{tree}')) ?? '';
1770
+ const treeChanged = treeAfter !== treeBefore;
1771
+ const changedPaths =
1772
+ treeChanged && after
1773
+ ? await this.changedPathsBetween(cwd, before ?? GitService.EMPTY_TREE_SHA, after)
1774
+ : [];
1775
+ return { before, after, treeChanged, changedPaths };
1776
+ });
1777
+ }
1778
+
1779
+ async pull(workspaceId: string): Promise<{ treeChanged: boolean }> {
1780
+ return this.mutex.run(workspaceId, async () => {
1781
+ const cwd = await this.repoDir(workspaceId);
1782
+ const branch = await this.checkedOutBranch(cwd);
1783
+ // For `treeChanged` (see IGitService.pull): TREE ids, not commit ids —
1784
+ // a pull that lands only content-identical commits (an empty commit)
1785
+ // must not read as a content change. Tolerant of an unborn HEAD (a
1786
+ // clone of an empty upstream): "" before + a tree after correctly
1787
+ // reads as changed, "" both sides as not.
1788
+ const treeBefore = (await this.revParseOrNull(cwd, 'HEAD^{tree}')) ?? '';
1789
+ // Refresh WITHOUT `git pull`, and without touching `FETCH_HEAD` — see
1790
+ // `refreshRemoteBranchRef` for why both halves are load-bearing.
1791
+ const remoteRef = await this.refreshRemoteBranchRef(cwd, branch);
1792
+ await this.rebaseOntoRemote(cwd, branch, remoteRef);
1793
+ this.accessControl?.invalidate(workspaceId);
1794
+ const treeAfter = (await this.revParseOrNull(cwd, 'HEAD^{tree}')) ?? '';
1795
+ return { treeChanged: treeAfter !== treeBefore };
1587
1796
  });
1588
1797
  }
1589
1798
 
@@ -1781,17 +1990,46 @@ export class GitService implements IGitService {
1781
1990
  const repoRelativePath = this.stripRepoPrefix(relativePath);
1782
1991
  return this.mutex.run(workspaceId, async () => {
1783
1992
  const cwd = await this.repoDir(workspaceId);
1993
+ // Same per-file rule as the diff endpoints: `git log -- <dir>` lists
1994
+ // every child's commits, and even metadata (subjects, authors, when)
1995
+ // is history the per-file gate never authorized for the children. A
1996
+ // HEAD check alone is not enough — a directory deleted before HEAD is
1997
+ // absent NOW and still traversed by the log — so the proof comes from
1998
+ // what the log itself TOUCHED: `--name-only` lists the paths behind
1999
+ // each listed commit, and any name that is not exactly the requested
2000
+ // path means the pathspec matched a tree somewhere in the range.
2001
+ await this.assertNotTreeAtRef(cwd, 'HEAD', repoRelativePath, relativePath);
1784
2002
  const { stdout } = await this.git(cwd, [
1785
2003
  'log',
1786
2004
  `--max-count=${max}`,
1787
- '--pretty=format:%H%x00%an%x00%ae%x00%s%x00%aI',
2005
+ '--name-only',
2006
+ '--pretty=format:%x01%H%x00%an%x00%ae%x00%s%x00%aI',
1788
2007
  '--',
1789
2008
  repoRelativePath,
1790
2009
  ]);
2010
+ for (const chunk of stdout.split('\u0001')) {
2011
+ const lines = chunk.split('\n');
2012
+ for (const nameLine of lines.slice(1)) {
2013
+ // No trim: a tracked name CAN begin or end with whitespace, and
2014
+ // trimming it here would reject that valid file as a directory.
2015
+ // Only the genuinely empty separator lines are skipped.
2016
+ const touched = nameLine.endsWith('\r') ? nameLine.slice(0, -1) : nameLine;
2017
+ if (touched && touched !== repoRelativePath) {
2018
+ throw new WorkflowValidationError(
2019
+ `"${relativePath}" is a directory at that point in history — history is served per file`,
2020
+ );
2021
+ }
2022
+ }
2023
+ }
1791
2024
  return stdout
2025
+ .split('\u0001')
2026
+ .join('')
1792
2027
  .split('\n')
1793
2028
  .map((line) => line.trim())
1794
2029
  .filter(Boolean)
2030
+ // `--name-only` interleaves the touched path under each commit line;
2031
+ // only the field lines carry the NUL separators.
2032
+ .filter((line) => line.includes('\x00'))
1795
2033
  .map((line): CommitAttribution => {
1796
2034
  const [sha, authorName, authorEmail, subject, committedAt] = line.split('\x00');
1797
2035
  return {
@@ -1817,6 +2055,11 @@ export class GitService implements IGitService {
1817
2055
  const repoRelativePath = this.stripRepoPrefix(relativePath);
1818
2056
  return this.mutex.run(workspaceId, async () => {
1819
2057
  const cwd = await this.repoDir(workspaceId);
2058
+ // BOTH sides: at a commit that DELETES a directory the path is absent
2059
+ // at `sha` but a tree at `sha^` — and the diff of that deletion is
2060
+ // every child's content, exactly what the per-file gate never checked.
2061
+ await this.assertNotTreeAtRef(cwd, sha, repoRelativePath, relativePath);
2062
+ await this.assertNotTreeAtRef(cwd, `${sha}^`, repoRelativePath, relativePath);
1820
2063
  try {
1821
2064
  const { stdout } = await this.git(cwd, [
1822
2065
  'show',
@@ -1862,6 +2105,9 @@ export class GitService implements IGitService {
1862
2105
  }
1863
2106
  const repoRelativePath = this.stripRepoPrefix(relativePath);
1864
2107
  return this.mutex.run(workspaceId, async () => {
2108
+ const cwd = await this.repoDir(workspaceId);
2109
+ await this.assertNotTreeAtRef(cwd, sha, repoRelativePath, relativePath);
2110
+ await this.assertNotTreeAtRef(cwd, `${sha}^`, repoRelativePath, relativePath);
1865
2111
  const current = await this.readFileAtRef(workspaceId, sha, repoRelativePath);
1866
2112
  let baseline: string | null;
1867
2113
  try {
@@ -1898,6 +2144,8 @@ export class GitService implements IGitService {
1898
2144
  const cwd = await this.repoDir(workspaceId);
1899
2145
  const fromRef = await this.resolveBranchRef(cwd, fromBranch);
1900
2146
  const toRef = await this.resolveBranchRef(cwd, toBranch);
2147
+ await this.assertNotTreeAtRef(cwd, fromRef, repoRelativePath, relativePath);
2148
+ await this.assertNotTreeAtRef(cwd, toRef, repoRelativePath, relativePath);
1901
2149
  // When one side is the currently-checked-out branch, diff against the
1902
2150
  // working tree instead of the branch's HEAD commit. Otherwise saves
1903
2151
  // that haven't been committed yet (which is the normal state in the
@@ -2394,6 +2642,49 @@ export class GitService implements IGitService {
2394
2642
  * `showAtRef`) — the caller distinguishes "absent" from "present but empty".
2395
2643
  * Read-only: `git show <ref>:<path>` never mutates the working tree.
2396
2644
  */
2645
+ /**
2646
+ * Refuse a history pathspec that names a TREE at `ref`. The history
2647
+ * surfaces are per-file, and their read gate upstream checked exactly one
2648
+ * path — but `git show/diff -- <dir>` walks every child, and a directory
2649
+ * whose folder chain grants read can hold children whose own frontmatter
2650
+ * denies it. `git show <ref>:<dir>` even prints the tree listing. Absent
2651
+ * at the ref is fine (absent is not a tree — a deleted file's history is
2652
+ * still its own), and an unresolvable ref is left for the actual read to
2653
+ * report in its own words.
2654
+ */
2655
+ private async assertNotTreeAtRef(
2656
+ cwd: string,
2657
+ ref: string,
2658
+ repoRelativePath: string,
2659
+ displayPath: string,
2660
+ ): Promise<void> {
2661
+ let kind: string;
2662
+ try {
2663
+ const { stdout } = await this.git(cwd, ['cat-file', '-t', `${ref}:${repoRelativePath}`]);
2664
+ kind = stdout.trim();
2665
+ } catch (err) {
2666
+ // ONLY proven absence passes: the path missing at the ref, or the ref
2667
+ // itself unresolvable (a root commit's `^`), which the actual read
2668
+ // reports in its own words. Anything else — a locked repo, a corrupt
2669
+ // object, git failing to run — must not be read as "not a tree": a
2670
+ // guard that fails open on its own errors is not a guard.
2671
+ const msg = err instanceof Error ? err.message : String(err);
2672
+ if (
2673
+ /not a valid object name|invalid object name|does not exist in|unknown revision|bad revision|exists on disk, but not in/i.test(
2674
+ msg,
2675
+ )
2676
+ ) {
2677
+ return;
2678
+ }
2679
+ throw err;
2680
+ }
2681
+ if (kind === 'tree') {
2682
+ throw new WorkflowValidationError(
2683
+ `"${displayPath}" is a directory at that point in history — history is served per file`,
2684
+ );
2685
+ }
2686
+ }
2687
+
2397
2688
  async readFileAtRef(
2398
2689
  workspaceId: string,
2399
2690
  ref: string,
@@ -438,6 +438,15 @@ export class PullRequestService implements IPullRequestService {
438
438
  this.cachedList.clear();
439
439
  }
440
440
 
441
+ /**
442
+ * Evict the CR-list caches alone. Each list entry carries `touchedNodePaths`
443
+ * resolved against a workspace's tree at the time; a remote sync that moved
444
+ * that tree makes them stale without touching any one request.
445
+ */
446
+ invalidateListCache(): void {
447
+ this.cachedList.clear();
448
+ }
449
+
441
450
  private async findRow(prNumber: number): Promise<ChangeRequestRow | null> {
442
451
  const [row] = await this.db
443
452
  .select()