@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,7 +7,7 @@ import { WorkspaceMutex } from '../../kb-fs/mutex.js';
7
7
  import { assertInsideRepo } from '../../kb-fs/repo-path.js';
8
8
  import { cloneTrackingConfigArgs, SAFE_IMPLICIT_FETCH_ARGS } from '../../kb-fs/clone-config.js';
9
9
  import { assertValidBranchName, assertValidRelativePath, isBranchAuthoredBy, isOwnSuggestionsBranch, isProtectedBranch, PROTECTED_BRANCHES, } from '../../kb-fs/branch-name.js';
10
- import { BranchAuthorshipError, WorkflowValidationError, ProtectedBranchError, PullRebaseConflictError, } from '../../../shared/domain-errors.js';
10
+ import { BranchAuthorshipError, WorkflowValidationError, ProtectedBranchError, PullRebaseConflictError, RemoteBranchGoneError, isMissingRemoteBranchFailure, } from '../../../shared/domain-errors.js';
11
11
  const execFileAsync = promisify(execFile);
12
12
  function redact(msg) {
13
13
  const token = process.env.GITHUB_TOKEN;
@@ -233,6 +233,8 @@ export class GitService {
233
233
  // worst case for branch-switching UX).
234
234
  fetchLocks = new Map();
235
235
  lastImplicitFetchAt = new Map();
236
+ /** Whether the last implicit fetch per workspace SUCCEEDED — see `fetchOriginIfStale`. */
237
+ lastImplicitFetchOk = new Map();
236
238
  static IMPLICIT_FETCH_TTL_MS = 5_000;
237
239
  // Cap on concurrent `aheadBehindForBranch` calls inside `listBranches`. Each
238
240
  // call serializes up to 3 `git rev-list` spawns, so 8 workers ≈ ≤24
@@ -274,6 +276,11 @@ export class GitService {
274
276
  */
275
277
  noteWorkspaceFetched(workspaceId) {
276
278
  this.lastImplicitFetchAt.set(workspaceId, Date.now());
279
+ // A fresh clone IS a successful fetch. Without this, a workspace
280
+ // recreated after some earlier failed fetch inherits the stale `false`
281
+ // under the same id, and a strict listBranches inside the TTL window
282
+ // refuses a list that is in fact provably fresh.
283
+ this.lastImplicitFetchOk.set(workspaceId, true);
277
284
  }
278
285
  /**
279
286
  * Run the registered ADVISORY commit-validation hooks at a commit site.
@@ -351,7 +358,16 @@ export class GitService {
351
358
  // bypasses the TTL — used for user-initiated refreshes (opening the branch
352
359
  // selector) so a draft another workspace just deleted is pruned right away
353
360
  // instead of lingering up to one TTL window.
354
- await this.fetchOriginIfStale(cwd, workspaceId, opts.freshFetch);
361
+ const fresh = await this.fetchOriginIfStale(cwd, workspaceId, opts.freshFetch);
362
+ // `strictFetch` is for callers that USE the list to prove absence — the
363
+ // deleted-branch sweep closes change requests on it. The degrade-to-stale
364
+ // behaviour below is right for a UI listing (stale branches beat a 500),
365
+ // and exactly wrong for an absence proof: a clone that last fetched
366
+ // before a branch was created reports it missing. Such a caller gets a
367
+ // thrown error instead of a list it must not trust.
368
+ if (opts.strictFetch && !fresh) {
369
+ throw new Error('the fetch from origin failed, so the branch list cannot prove absence — refusing to serve a stale list to a strict caller');
370
+ }
355
371
  return this.mutex.run(workspaceId, async () => {
356
372
  // Union local heads with origin's remote-tracking refs: a fresh per-user
357
373
  // clone only has one local head (HEAD's default), so without this we'd hide
@@ -446,15 +462,19 @@ export class GitService {
446
462
  * onto a single in-flight fetch instead of each waiting their turn behind
447
463
  * the network round-trip. Failures (offline, bad auth, no remote) are
448
464
  * logged and swallowed so the branch list still serves stale local data
449
- * instead of returning 500.
465
+ * instead of returning 500 — the RETURN VALUE says whether the refs are
466
+ * provably fresh (`true`: this call fetched, or shared a fetch, that
467
+ * succeeded; also `true` on a TTL skip, whose window a recent success
468
+ * opened). Callers that need proof (strict listing) read it.
450
469
  *
451
470
  * Safe to keep outside the mutex: fetch only writes remote-tracking refs
452
471
  * and the object store, neither of which checkout/status/commit touch.
453
472
  */
454
473
  async fetchOriginIfStale(cwd, workspaceId, force = false) {
455
474
  const last = this.lastImplicitFetchAt.get(workspaceId) ?? 0;
456
- if (!force && Date.now() - last < GitService.IMPLICIT_FETCH_TTL_MS)
457
- return;
475
+ if (!force && Date.now() - last < GitService.IMPLICIT_FETCH_TTL_MS) {
476
+ return this.lastImplicitFetchOk.get(workspaceId) ?? true;
477
+ }
458
478
  const inFlight = this.fetchLocks.get(workspaceId);
459
479
  if (inFlight) {
460
480
  // Another caller is already fetching for this workspace — share its
@@ -462,7 +482,7 @@ export class GitService {
462
482
  // the same promise also means subsequent listBranches calls see the
463
483
  // newly fetched refs.
464
484
  await inFlight;
465
- return;
485
+ return this.lastImplicitFetchOk.get(workspaceId) ?? false;
466
486
  }
467
487
  const runFetch = async () => {
468
488
  try {
@@ -478,6 +498,7 @@ export class GitService {
478
498
  ...SAFE_IMPLICIT_FETCH_ARGS,
479
499
  ]);
480
500
  this.lastImplicitFetchAt.set(workspaceId, Date.now());
501
+ this.lastImplicitFetchOk.set(workspaceId, true);
481
502
  }
482
503
  catch (err) {
483
504
  const msg = err instanceof Error ? err.message : String(err);
@@ -486,12 +507,14 @@ export class GitService {
486
507
  // every subsequent listBranches retry the (still-failing) fetch and
487
508
  // pile up 10s timeouts. Stale local refs are better than spinning.
488
509
  this.lastImplicitFetchAt.set(workspaceId, Date.now());
510
+ this.lastImplicitFetchOk.set(workspaceId, false);
489
511
  }
490
512
  };
491
513
  const p = runFetch();
492
514
  this.fetchLocks.set(workspaceId, p);
493
515
  try {
494
516
  await p;
517
+ return this.lastImplicitFetchOk.get(workspaceId) ?? false;
495
518
  }
496
519
  finally {
497
520
  // Drop the lock once the fetch settles so the next caller past the TTL
@@ -1346,68 +1369,241 @@ export class GitService {
1346
1369
  ]);
1347
1370
  return `refs/remotes/origin/${branch}`;
1348
1371
  }
1349
- async pull(workspaceId) {
1372
+ /** A commit-free tree, for diffing against a HEAD that did not exist yet. */
1373
+ static EMPTY_TREE_SHA = '4b825dc642cb6eb9a060e54bf8d69288fbee4904';
1374
+ /**
1375
+ * The checked-out branch by its symbolic ref, which an UNBORN HEAD (a clone
1376
+ * of an empty upstream) still has — `rev-parse --abbrev-ref HEAD` does not
1377
+ * resolve there, so the tolerant tree reads below would never be reached.
1378
+ */
1379
+ async checkedOutBranch(cwd) {
1380
+ const { stdout } = await this.git(cwd, ['symbolic-ref', '--short', 'HEAD']);
1381
+ return stdout.trim();
1382
+ }
1383
+ /**
1384
+ * Whether origin still has `branch`. `ls-remote --exit-code` answers with
1385
+ * exit status 2 for "no such ref" — the one failure that is a fact about the
1386
+ * branch rather than about reaching the host, which propagates.
1387
+ */
1388
+ async remoteBranchExists(workspaceId, branch) {
1389
+ assertValidBranchName(branch);
1390
+ const cwd = await this.repoDir(workspaceId);
1391
+ try {
1392
+ await this.git(cwd, ['ls-remote', '--exit-code', '--heads', 'origin', `refs/heads/${branch}`]);
1393
+ return true;
1394
+ }
1395
+ catch (err) {
1396
+ if (err.exitCode === 2)
1397
+ return false;
1398
+ throw err;
1399
+ }
1400
+ }
1401
+ /** Whether origin has ANY branch — an empty repository has none. */
1402
+ async remoteHasAnyBranch(cwd) {
1403
+ const { stdout } = await this.git(cwd, ['ls-remote', '--heads', 'origin']);
1404
+ return stdout.trim().length > 0;
1405
+ }
1406
+ /** `git rev-parse --verify` that answers null for a rev that does not resolve (an unborn HEAD). */
1407
+ async revParseOrNull(cwd, rev) {
1408
+ try {
1409
+ const { stdout } = await this.git(cwd, ['rev-parse', '--verify', '--quiet', rev]);
1410
+ return stdout.trim() || null;
1411
+ }
1412
+ catch {
1413
+ return null;
1414
+ }
1415
+ }
1416
+ /**
1417
+ * Refresh `refs/remotes/origin/<branch>` for a sync, naming the one failure
1418
+ * that is not "origin unreachable": the branch no longer exists there.
1419
+ */
1420
+ async refreshRemoteBranchRefForSync(cwd, branch) {
1421
+ try {
1422
+ return await this.refreshRemoteBranchRef(cwd, branch);
1423
+ }
1424
+ catch (err) {
1425
+ const msg = err instanceof Error ? err.message : String(err);
1426
+ if (isMissingRemoteBranchFailure(msg))
1427
+ throw new RemoteBranchGoneError(branch);
1428
+ throw err;
1429
+ }
1430
+ }
1431
+ /**
1432
+ * Rebase the checked-out branch onto `remoteRef`, turning a content
1433
+ * conflict into the typed error the workflow layer queues recovery for.
1434
+ * Shared by `pull` and `syncFromRemote` so the two cannot drift.
1435
+ */
1436
+ async rebaseOntoRemote(cwd, branch, remoteRef) {
1437
+ // An UNBORN HEAD (a clone of an upstream that was empty when cloned) has
1438
+ // no commit to replay and `git rebase` refuses to start from it, so the
1439
+ // branch is moved to the remote commit instead. That is only safe while
1440
+ // the index is EMPTY: files staged in an unborn clone are tracked, and a
1441
+ // hard reset would destroy them where the normal path autostashes — and
1442
+ // `git stash` cannot run without an initial commit. So a non-empty index
1443
+ // is refused as a conflict, naming the staged paths, and takes the same
1444
+ // recovery path a rebase conflict does; nothing on disk is touched.
1445
+ if ((await this.revParseOrNull(cwd, 'HEAD')) === null) {
1446
+ // `-z`: NUL-delimited and unquoted. Line output quotes a path with a
1447
+ // non-ASCII or control character ("Entw\303\274rfe.md"), and that
1448
+ // spelling would name a file that does not exist to whoever recovers.
1449
+ const { stdout: stagedOut } = await this.git(cwd, ['ls-files', '--cached', '-z']);
1450
+ const staged = stagedOut.split('\0').filter(Boolean);
1451
+ if (staged.length > 0) {
1452
+ throw new PullRebaseConflictError(branch, staged, 'The clone has files staged before its first commit; they cannot be replayed onto the remote branch automatically.');
1453
+ }
1454
+ await this.git(cwd, ['reset', '--hard', remoteRef]);
1455
+ return;
1456
+ }
1457
+ try {
1458
+ // `--autostash` is load-bearing for the cooperative push recovery.
1459
+ // Under save=share the working tree is *supposed* to stay clean, but
1460
+ // in practice it can carry modified tracked artifacts (e.g. the
1461
+ // validator regenerates a dashboard HTML on every commit) that the
1462
+ // single-file commit didn't pick up. A rebase without it aborts
1463
+ // outright on those — "cannot rebase: You have unstaged
1464
+ // changes" — which is exactly the failure that strands a diverged
1465
+ // branch in an unrecoverable loop (the retry push stays non-fast-
1466
+ // forward forever). Autostash stashes those modifications, rebases
1467
+ // the local commits onto origin, then reapplies them, so the retry
1468
+ // push can fast-forward. Untracked files don't block rebase and are
1469
+ // left untouched.
1470
+ await this.git(cwd, ['rebase', '--autostash', remoteRef]);
1471
+ }
1472
+ catch (err) {
1473
+ // Capture the contested paths BEFORE the abort wipes the rebase
1474
+ // state — `--diff-filter=U` lists exactly the files whose replay
1475
+ // conflicted. Best-effort: an empty list just means this wasn't a
1476
+ // content conflict (or the probe itself failed) and the raw error
1477
+ // is surfaced unchanged.
1478
+ let conflictedPaths = [];
1479
+ try {
1480
+ const { stdout } = await this.git(cwd, ['diff', '--name-only', '--diff-filter=U']);
1481
+ conflictedPaths = stdout.split('\n').map((s) => s.trim()).filter(Boolean);
1482
+ }
1483
+ catch {
1484
+ // fall through with an empty list
1485
+ }
1486
+ // A failed rebase leaves the repo in a "REBASE_HEAD" / detached-apply state,
1487
+ // which makes every subsequent git command behave unexpectedly. Return the
1488
+ // working tree to a clean state before surfacing the original error.
1489
+ let abortSucceeded = true;
1490
+ await this.git(cwd, ['rebase', '--abort']).catch(() => {
1491
+ abortSucceeded = false;
1492
+ });
1493
+ if (abortSucceeded && conflictedPaths.length > 0) {
1494
+ // Typed so the workflow layer can queue background recovery — this
1495
+ // divergence never resolves on its own (every retry pull hits the
1496
+ // same conflict) and the unpushed local commits are someone's saved
1497
+ // content stuck violating save=share.
1498
+ //
1499
+ // Gated on the abort succeeding: a failed abort means either no
1500
+ // rebase was ever active (the unmerged paths pre-date this pull —
1501
+ // some earlier operation left the index broken) or the clone is in
1502
+ // a state git itself can't unwind. Queueing recovery there would
1503
+ // point the worker's commitFile at a repo mid-conflict, risking a
1504
+ // commit full of conflict markers — surface the raw error instead
1505
+ // and leave diagnosis to a human.
1506
+ throw new PullRebaseConflictError(branch, conflictedPaths, err instanceof Error ? err.message : String(err));
1507
+ }
1508
+ throw err;
1509
+ }
1510
+ // `--autostash` reapplies the stashed edits AFTER the rebase, and when that
1511
+ // reapply conflicts git exits 0: the clone is left with an unmerged index,
1512
+ // the edits kept in `stash@{0}`, and nothing above has thrown. Read the
1513
+ // index rather than the exit status. The rebase itself landed, so the
1514
+ // branch is at the remote commit; resetting to it drops only the
1515
+ // half-applied stash (still in the stash list) and lets the conflict
1516
+ // travel the same typed path a rebase conflict does.
1517
+ const { stdout: unmergedOut } = await this.git(cwd, ['diff', '--name-only', '--diff-filter=U']);
1518
+ const unmerged = unmergedOut.split('\n').map((l) => l.trim()).filter(Boolean);
1519
+ if (unmerged.length > 0) {
1520
+ await this.git(cwd, ['reset', '--hard', 'HEAD']);
1521
+ throw new PullRebaseConflictError(branch, unmerged, 'Reapplying the autostashed working-tree edits conflicted; the edits are kept in git stash.');
1522
+ }
1523
+ }
1524
+ /**
1525
+ * Repo-relative paths whose content differs between two commits. Rename-
1526
+ * aware: a renamed file reports both its old and new path, so a viewer
1527
+ * holding the OLD path learns it is gone. Caller holds the mutex.
1528
+ */
1529
+ async changedPathsBetween(cwd, fromSha, toSha) {
1530
+ const { stdout } = await this.git(cwd, [
1531
+ 'diff', '--name-status', '-M', '--no-color', '-z', fromSha, toSha,
1532
+ ]);
1533
+ const fields = stdout.split('\0');
1534
+ const paths = new Set();
1535
+ for (let i = 0; i < fields.length;) {
1536
+ const status = fields[i];
1537
+ if (!status)
1538
+ break;
1539
+ if (status.startsWith('R') || status.startsWith('C')) {
1540
+ if (fields[i + 1])
1541
+ paths.add(fields[i + 1]);
1542
+ if (fields[i + 2])
1543
+ paths.add(fields[i + 2]);
1544
+ i += 3;
1545
+ }
1546
+ else {
1547
+ if (fields[i + 1])
1548
+ paths.add(fields[i + 1]);
1549
+ i += 2;
1550
+ }
1551
+ }
1552
+ return [...paths];
1553
+ }
1554
+ async syncFromRemote(workspaceId) {
1555
+ // ONE mutex section for everything the sync reports about this clone.
1556
+ // Bracketing `pull` with separate HEAD reads and a diff took the hold
1557
+ // four times, and a save landing between any two of them was announced
1558
+ // back to its author as a "Git sync" change with the wrong sha.
1350
1559
  return this.mutex.run(workspaceId, async () => {
1351
1560
  const cwd = await this.repoDir(workspaceId);
1352
- const branch = await this.currentBranch(cwd);
1561
+ const branch = await this.checkedOutBranch(cwd);
1562
+ const before = await this.revParseOrNull(cwd, 'HEAD');
1563
+ const treeBefore = (await this.revParseOrNull(cwd, 'HEAD^{tree}')) ?? '';
1564
+ let remoteRef;
1353
1565
  try {
1354
- // Refresh WITHOUT `git pull`, and without touching `FETCH_HEAD` — see
1355
- // `refreshRemoteBranchRef` for why both halves are load-bearing.
1356
- //
1357
- // `--autostash` is load-bearing for the cooperative push recovery.
1358
- // Under save=share the working tree is *supposed* to stay clean, but
1359
- // in practice it can carry modified tracked artifacts (e.g. the
1360
- // validator regenerates a dashboard HTML on every commit) that the
1361
- // single-file commit didn't pick up. A rebase without it aborts
1362
- // outright on those — "cannot rebase: You have unstaged
1363
- // changes" — which is exactly the failure that strands a diverged
1364
- // branch in an unrecoverable loop (the retry push stays non-fast-
1365
- // forward forever). Autostash stashes those modifications, rebases
1366
- // the local commits onto origin, then reapplies them, so the retry
1367
- // push can fast-forward. Untracked files don't block rebase and are
1368
- // left untouched.
1369
- const remoteRef = await this.refreshRemoteBranchRef(cwd, branch);
1370
- await this.git(cwd, ['rebase', '--autostash', remoteRef]);
1566
+ remoteRef = await this.refreshRemoteBranchRefForSync(cwd, branch);
1371
1567
  }
1372
1568
  catch (err) {
1373
- // Capture the contested paths BEFORE the abort wipes the rebase
1374
- // state — `--diff-filter=U` lists exactly the files whose replay
1375
- // conflicted. Best-effort: an empty list just means this wasn't a
1376
- // content conflict (or the probe itself failed) and the raw error
1377
- // is surfaced unchanged.
1378
- let conflictedPaths = [];
1379
- try {
1380
- const { stdout } = await this.git(cwd, ['diff', '--name-only', '--diff-filter=U']);
1381
- conflictedPaths = stdout.split('\n').map((s) => s.trim()).filter(Boolean);
1382
- }
1383
- catch {
1384
- // fall through with an empty list
1385
- }
1386
- // A failed rebase leaves the repo in a "REBASE_HEAD" / detached-apply state,
1387
- // which makes every subsequent git command behave unexpectedly. Return the
1388
- // working tree to a clean state before surfacing the original error.
1389
- let abortSucceeded = true;
1390
- await this.git(cwd, ['rebase', '--abort']).catch(() => {
1391
- abortSucceeded = false;
1392
- });
1393
- if (abortSucceeded && conflictedPaths.length > 0) {
1394
- // Typed so the workflow layer can queue background recovery — this
1395
- // divergence never resolves on its own (every retry pull hits the
1396
- // same conflict) and the unpushed local commits are someone's saved
1397
- // content stuck violating save=share.
1398
- //
1399
- // Gated on the abort succeeding: a failed abort means either no
1400
- // rebase was ever active (the unmerged paths pre-date this pull —
1401
- // some earlier operation left the index broken) or the clone is in
1402
- // a state git itself can't unwind. Queueing recovery there would
1403
- // point the worker's commitFile at a repo mid-conflict, risking a
1404
- // commit full of conflict markers — surface the raw error instead
1405
- // and leave diagnosis to a human.
1406
- throw new PullRebaseConflictError(branch, conflictedPaths, err instanceof Error ? err.message : String(err));
1569
+ // An unborn clone whose branch origin does not have either: on a FRESH
1570
+ // deployment (origin has no branch at all) nobody has pushed yet —
1571
+ // nothing to sync, nothing to retire. But an origin that does have
1572
+ // branches and lacks this one has had this branch created and deleted
1573
+ // since the clone was made, and the clone is as stale as any other.
1574
+ if (err instanceof RemoteBranchGoneError && before === null && !(await this.remoteHasAnyBranch(cwd))) {
1575
+ return { before: null, after: null, treeChanged: false, changedPaths: [] };
1407
1576
  }
1408
1577
  throw err;
1409
1578
  }
1579
+ await this.rebaseOntoRemote(cwd, branch, remoteRef);
1580
+ this.accessControl?.invalidate(workspaceId);
1581
+ const after = await this.revParseOrNull(cwd, 'HEAD');
1582
+ const treeAfter = (await this.revParseOrNull(cwd, 'HEAD^{tree}')) ?? '';
1583
+ const treeChanged = treeAfter !== treeBefore;
1584
+ const changedPaths = treeChanged && after
1585
+ ? await this.changedPathsBetween(cwd, before ?? GitService.EMPTY_TREE_SHA, after)
1586
+ : [];
1587
+ return { before, after, treeChanged, changedPaths };
1588
+ });
1589
+ }
1590
+ async pull(workspaceId) {
1591
+ return this.mutex.run(workspaceId, async () => {
1592
+ const cwd = await this.repoDir(workspaceId);
1593
+ const branch = await this.checkedOutBranch(cwd);
1594
+ // For `treeChanged` (see IGitService.pull): TREE ids, not commit ids —
1595
+ // a pull that lands only content-identical commits (an empty commit)
1596
+ // must not read as a content change. Tolerant of an unborn HEAD (a
1597
+ // clone of an empty upstream): "" before + a tree after correctly
1598
+ // reads as changed, "" both sides as not.
1599
+ const treeBefore = (await this.revParseOrNull(cwd, 'HEAD^{tree}')) ?? '';
1600
+ // Refresh WITHOUT `git pull`, and without touching `FETCH_HEAD` — see
1601
+ // `refreshRemoteBranchRef` for why both halves are load-bearing.
1602
+ const remoteRef = await this.refreshRemoteBranchRef(cwd, branch);
1603
+ await this.rebaseOntoRemote(cwd, branch, remoteRef);
1410
1604
  this.accessControl?.invalidate(workspaceId);
1605
+ const treeAfter = (await this.revParseOrNull(cwd, 'HEAD^{tree}')) ?? '';
1606
+ return { treeChanged: treeAfter !== treeBefore };
1411
1607
  });
1412
1608
  }
1413
1609
  /**
@@ -1591,17 +1787,44 @@ export class GitService {
1591
1787
  const repoRelativePath = this.stripRepoPrefix(relativePath);
1592
1788
  return this.mutex.run(workspaceId, async () => {
1593
1789
  const cwd = await this.repoDir(workspaceId);
1790
+ // Same per-file rule as the diff endpoints: `git log -- <dir>` lists
1791
+ // every child's commits, and even metadata (subjects, authors, when)
1792
+ // is history the per-file gate never authorized for the children. A
1793
+ // HEAD check alone is not enough — a directory deleted before HEAD is
1794
+ // absent NOW and still traversed by the log — so the proof comes from
1795
+ // what the log itself TOUCHED: `--name-only` lists the paths behind
1796
+ // each listed commit, and any name that is not exactly the requested
1797
+ // path means the pathspec matched a tree somewhere in the range.
1798
+ await this.assertNotTreeAtRef(cwd, 'HEAD', repoRelativePath, relativePath);
1594
1799
  const { stdout } = await this.git(cwd, [
1595
1800
  'log',
1596
1801
  `--max-count=${max}`,
1597
- '--pretty=format:%H%x00%an%x00%ae%x00%s%x00%aI',
1802
+ '--name-only',
1803
+ '--pretty=format:%x01%H%x00%an%x00%ae%x00%s%x00%aI',
1598
1804
  '--',
1599
1805
  repoRelativePath,
1600
1806
  ]);
1807
+ for (const chunk of stdout.split('\u0001')) {
1808
+ const lines = chunk.split('\n');
1809
+ for (const nameLine of lines.slice(1)) {
1810
+ // No trim: a tracked name CAN begin or end with whitespace, and
1811
+ // trimming it here would reject that valid file as a directory.
1812
+ // Only the genuinely empty separator lines are skipped.
1813
+ const touched = nameLine.endsWith('\r') ? nameLine.slice(0, -1) : nameLine;
1814
+ if (touched && touched !== repoRelativePath) {
1815
+ throw new WorkflowValidationError(`"${relativePath}" is a directory at that point in history — history is served per file`);
1816
+ }
1817
+ }
1818
+ }
1601
1819
  return stdout
1820
+ .split('\u0001')
1821
+ .join('')
1602
1822
  .split('\n')
1603
1823
  .map((line) => line.trim())
1604
1824
  .filter(Boolean)
1825
+ // `--name-only` interleaves the touched path under each commit line;
1826
+ // only the field lines carry the NUL separators.
1827
+ .filter((line) => line.includes('\x00'))
1605
1828
  .map((line) => {
1606
1829
  const [sha, authorName, authorEmail, subject, committedAt] = line.split('\x00');
1607
1830
  return {
@@ -1622,6 +1845,11 @@ export class GitService {
1622
1845
  const repoRelativePath = this.stripRepoPrefix(relativePath);
1623
1846
  return this.mutex.run(workspaceId, async () => {
1624
1847
  const cwd = await this.repoDir(workspaceId);
1848
+ // BOTH sides: at a commit that DELETES a directory the path is absent
1849
+ // at `sha` but a tree at `sha^` — and the diff of that deletion is
1850
+ // every child's content, exactly what the per-file gate never checked.
1851
+ await this.assertNotTreeAtRef(cwd, sha, repoRelativePath, relativePath);
1852
+ await this.assertNotTreeAtRef(cwd, `${sha}^`, repoRelativePath, relativePath);
1625
1853
  try {
1626
1854
  const { stdout } = await this.git(cwd, [
1627
1855
  'show',
@@ -1660,6 +1888,9 @@ export class GitService {
1660
1888
  }
1661
1889
  const repoRelativePath = this.stripRepoPrefix(relativePath);
1662
1890
  return this.mutex.run(workspaceId, async () => {
1891
+ const cwd = await this.repoDir(workspaceId);
1892
+ await this.assertNotTreeAtRef(cwd, sha, repoRelativePath, relativePath);
1893
+ await this.assertNotTreeAtRef(cwd, `${sha}^`, repoRelativePath, relativePath);
1663
1894
  const current = await this.readFileAtRef(workspaceId, sha, repoRelativePath);
1664
1895
  let baseline;
1665
1896
  try {
@@ -1692,6 +1923,8 @@ export class GitService {
1692
1923
  const cwd = await this.repoDir(workspaceId);
1693
1924
  const fromRef = await this.resolveBranchRef(cwd, fromBranch);
1694
1925
  const toRef = await this.resolveBranchRef(cwd, toBranch);
1926
+ await this.assertNotTreeAtRef(cwd, fromRef, repoRelativePath, relativePath);
1927
+ await this.assertNotTreeAtRef(cwd, toRef, repoRelativePath, relativePath);
1695
1928
  // When one side is the currently-checked-out branch, diff against the
1696
1929
  // working tree instead of the branch's HEAD commit. Otherwise saves
1697
1930
  // that haven't been committed yet (which is the normal state in the
@@ -2133,6 +2366,38 @@ export class GitService {
2133
2366
  * `showAtRef`) — the caller distinguishes "absent" from "present but empty".
2134
2367
  * Read-only: `git show <ref>:<path>` never mutates the working tree.
2135
2368
  */
2369
+ /**
2370
+ * Refuse a history pathspec that names a TREE at `ref`. The history
2371
+ * surfaces are per-file, and their read gate upstream checked exactly one
2372
+ * path — but `git show/diff -- <dir>` walks every child, and a directory
2373
+ * whose folder chain grants read can hold children whose own frontmatter
2374
+ * denies it. `git show <ref>:<dir>` even prints the tree listing. Absent
2375
+ * at the ref is fine (absent is not a tree — a deleted file's history is
2376
+ * still its own), and an unresolvable ref is left for the actual read to
2377
+ * report in its own words.
2378
+ */
2379
+ async assertNotTreeAtRef(cwd, ref, repoRelativePath, displayPath) {
2380
+ let kind;
2381
+ try {
2382
+ const { stdout } = await this.git(cwd, ['cat-file', '-t', `${ref}:${repoRelativePath}`]);
2383
+ kind = stdout.trim();
2384
+ }
2385
+ catch (err) {
2386
+ // ONLY proven absence passes: the path missing at the ref, or the ref
2387
+ // itself unresolvable (a root commit's `^`), which the actual read
2388
+ // reports in its own words. Anything else — a locked repo, a corrupt
2389
+ // object, git failing to run — must not be read as "not a tree": a
2390
+ // guard that fails open on its own errors is not a guard.
2391
+ const msg = err instanceof Error ? err.message : String(err);
2392
+ if (/not a valid object name|invalid object name|does not exist in|unknown revision|bad revision|exists on disk, but not in/i.test(msg)) {
2393
+ return;
2394
+ }
2395
+ throw err;
2396
+ }
2397
+ if (kind === 'tree') {
2398
+ throw new WorkflowValidationError(`"${displayPath}" is a directory at that point in history — history is served per file`);
2399
+ }
2400
+ }
2136
2401
  async readFileAtRef(workspaceId, ref, repoRelativePath) {
2137
2402
  const cwd = await this.repoDir(workspaceId);
2138
2403
  try {