@bevel-software/platform-core-backend 0.14.0 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (1307) hide show
  1. package/THIRD-PARTY-NOTICES.md +8 -8
  2. package/dist/core/catalog-cache-invalidation.d.ts +45 -0
  3. package/dist/core/catalog-cache-invalidation.d.ts.map +1 -0
  4. package/dist/core/catalog-cache-invalidation.js +76 -0
  5. package/dist/core/catalog-cache-invalidation.js.map +1 -0
  6. package/dist/core/catalog-revision.d.ts +39 -0
  7. package/dist/core/catalog-revision.d.ts.map +1 -0
  8. package/dist/core/catalog-revision.js +94 -0
  9. package/dist/core/catalog-revision.js.map +1 -0
  10. package/dist/core/create-core-server.d.ts +7 -0
  11. package/dist/core/create-core-server.d.ts.map +1 -1
  12. package/dist/core/create-core-server.js +279 -64
  13. package/dist/core/create-core-server.js.map +1 -1
  14. package/dist/core/create-core-services.d.ts +66 -2
  15. package/dist/core/create-core-services.d.ts.map +1 -1
  16. package/dist/core/create-core-services.js +290 -111
  17. package/dist/core/create-core-services.js.map +1 -1
  18. package/dist/core/lifecycle.d.ts +138 -0
  19. package/dist/core/lifecycle.d.ts.map +1 -0
  20. package/dist/core/lifecycle.js +218 -0
  21. package/dist/core/lifecycle.js.map +1 -0
  22. package/dist/core/public-config.d.ts +82 -0
  23. package/dist/core/public-config.d.ts.map +1 -0
  24. package/dist/core/public-config.js +78 -0
  25. package/dist/core/public-config.js.map +1 -0
  26. package/dist/core/readiness.d.ts +86 -0
  27. package/dist/core/readiness.d.ts.map +1 -0
  28. package/dist/core/readiness.js +56 -0
  29. package/dist/core/readiness.js.map +1 -0
  30. package/dist/core-config.d.ts +30 -0
  31. package/dist/core-config.d.ts.map +1 -1
  32. package/dist/core-config.js +64 -2
  33. package/dist/core-config.js.map +1 -1
  34. package/dist/index.d.ts +4 -1
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +6 -0
  37. package/dist/index.js.map +1 -1
  38. package/dist/modules/access/access-control.interface.d.ts +231 -15
  39. package/dist/modules/access/access-control.interface.d.ts.map +1 -1
  40. package/dist/modules/access/access-control.service.d.ts +83 -12
  41. package/dist/modules/access/access-control.service.d.ts.map +1 -1
  42. package/dist/modules/access/access-control.service.js +815 -212
  43. package/dist/modules/access/access-control.service.js.map +1 -1
  44. package/dist/modules/access/access-mutation.service.d.ts +71 -5
  45. package/dist/modules/access/access-mutation.service.d.ts.map +1 -1
  46. package/dist/modules/access/access-mutation.service.js +161 -13
  47. package/dist/modules/access/access-mutation.service.js.map +1 -1
  48. package/dist/modules/access/access-view.d.ts +147 -0
  49. package/dist/modules/access/access-view.d.ts.map +1 -0
  50. package/dist/modules/access/access-view.js +211 -0
  51. package/dist/modules/access/access-view.js.map +1 -0
  52. package/dist/modules/access/access.routes.d.ts.map +1 -1
  53. package/dist/modules/access/access.routes.js +312 -84
  54. package/dist/modules/access/access.routes.js.map +1 -1
  55. package/dist/modules/access/admin-locked-commit.d.ts +2 -3
  56. package/dist/modules/access/admin-locked-commit.d.ts.map +1 -1
  57. package/dist/modules/access/admin-locked-commit.js +9 -8
  58. package/dist/modules/access/admin-locked-commit.js.map +1 -1
  59. package/dist/modules/access/admin-route-helpers.d.ts.map +1 -1
  60. package/dist/modules/access/admin-route-helpers.js +4 -4
  61. package/dist/modules/access/admin-route-helpers.js.map +1 -1
  62. package/dist/modules/access/change-read-gate.d.ts +53 -0
  63. package/dist/modules/access/change-read-gate.d.ts.map +1 -0
  64. package/dist/modules/access/change-read-gate.js +134 -0
  65. package/dist/modules/access/change-read-gate.js.map +1 -0
  66. package/dist/modules/access/creator-access.d.ts +33 -39
  67. package/dist/modules/access/creator-access.d.ts.map +1 -1
  68. package/dist/modules/access/creator-access.js +92 -119
  69. package/dist/modules/access/creator-access.js.map +1 -1
  70. package/dist/modules/access/directory-sync-bot.d.ts.map +1 -1
  71. package/dist/modules/access/directory-sync-bot.js +3 -1
  72. package/dist/modules/access/directory-sync-bot.js.map +1 -1
  73. package/dist/modules/access/groups-admin.service.d.ts +3 -3
  74. package/dist/modules/access/groups-admin.service.js +2 -2
  75. package/dist/modules/access/reference-scan.d.ts +1 -1
  76. package/dist/modules/access/reference-scan.d.ts.map +1 -1
  77. package/dist/modules/access/reference-scan.js +5 -4
  78. package/dist/modules/access/reference-scan.js.map +1 -1
  79. package/dist/modules/access/roles-admin.service.d.ts +72 -13
  80. package/dist/modules/access/roles-admin.service.d.ts.map +1 -1
  81. package/dist/modules/access/roles-admin.service.js +71 -13
  82. package/dist/modules/access/roles-admin.service.js.map +1 -1
  83. package/dist/modules/access/synced-groups-committer.d.ts.map +1 -1
  84. package/dist/modules/access/synced-groups-committer.js +8 -6
  85. package/dist/modules/access/synced-groups-committer.js.map +1 -1
  86. package/dist/modules/access/synced-groups-writer.d.ts.map +1 -1
  87. package/dist/modules/access/synced-groups-writer.js +3 -8
  88. package/dist/modules/access/synced-groups-writer.js.map +1 -1
  89. package/dist/modules/access/user-access-removal.service.d.ts +209 -0
  90. package/dist/modules/access/user-access-removal.service.d.ts.map +1 -0
  91. package/dist/modules/access/user-access-removal.service.js +662 -0
  92. package/dist/modules/access/user-access-removal.service.js.map +1 -0
  93. package/dist/modules/access-model/access-errors.d.ts +32 -1
  94. package/dist/modules/access-model/access-errors.d.ts.map +1 -1
  95. package/dist/modules/access-model/access-errors.js +38 -11
  96. package/dist/modules/access-model/access-errors.js.map +1 -1
  97. package/dist/modules/access-model/access-grammar.d.ts +100 -25
  98. package/dist/modules/access-model/access-grammar.d.ts.map +1 -1
  99. package/dist/modules/access-model/access-grammar.js +162 -48
  100. package/dist/modules/access-model/access-grammar.js.map +1 -1
  101. package/dist/modules/access-model/access-splice.d.ts.map +1 -1
  102. package/dist/modules/access-model/access-splice.js +18 -18
  103. package/dist/modules/access-model/access-splice.js.map +1 -1
  104. package/dist/modules/access-model/access-template.d.ts +22 -0
  105. package/dist/modules/access-model/access-template.d.ts.map +1 -0
  106. package/dist/modules/access-model/access-template.js +42 -0
  107. package/dist/modules/access-model/access-template.js.map +1 -0
  108. package/dist/modules/access-model/change-gate.d.ts +65 -0
  109. package/dist/modules/access-model/change-gate.d.ts.map +1 -0
  110. package/dist/modules/access-model/change-gate.js +25 -0
  111. package/dist/modules/access-model/change-gate.js.map +1 -0
  112. package/dist/modules/access-model/creator.d.ts +13 -16
  113. package/dist/modules/access-model/creator.d.ts.map +1 -1
  114. package/dist/modules/access-model/creator.js.map +1 -1
  115. package/dist/modules/access-model/frontmatter-lines.d.ts +27 -0
  116. package/dist/modules/access-model/frontmatter-lines.d.ts.map +1 -0
  117. package/dist/modules/access-model/frontmatter-lines.js +74 -0
  118. package/dist/modules/access-model/frontmatter-lines.js.map +1 -0
  119. package/dist/modules/access-model/group-files.d.ts.map +1 -1
  120. package/dist/modules/access-model/group-files.js +4 -1
  121. package/dist/modules/access-model/group-files.js.map +1 -1
  122. package/dist/modules/access-model/plugin-principals.d.ts +51 -0
  123. package/dist/modules/access-model/plugin-principals.d.ts.map +1 -0
  124. package/dist/modules/access-model/plugin-principals.js +119 -0
  125. package/dist/modules/access-model/plugin-principals.js.map +1 -0
  126. package/dist/modules/access-model/roles-yaml-guard.d.ts +73 -2
  127. package/dist/modules/access-model/roles-yaml-guard.d.ts.map +1 -1
  128. package/dist/modules/access-model/roles-yaml-guard.js +153 -3
  129. package/dist/modules/access-model/roles-yaml-guard.js.map +1 -1
  130. package/dist/modules/admin/admin-access.service.d.ts.map +1 -1
  131. package/dist/modules/admin/admin-access.service.js +5 -2
  132. package/dist/modules/admin/admin-access.service.js.map +1 -1
  133. package/dist/modules/agent-instructions/agent-instructions.routes.d.ts +21 -0
  134. package/dist/modules/agent-instructions/agent-instructions.routes.d.ts.map +1 -0
  135. package/dist/modules/agent-instructions/agent-instructions.routes.js +38 -0
  136. package/dist/modules/agent-instructions/agent-instructions.routes.js.map +1 -0
  137. package/dist/modules/agent-instructions/compose.d.ts +71 -0
  138. package/dist/modules/agent-instructions/compose.d.ts.map +1 -0
  139. package/dist/modules/agent-instructions/compose.js +234 -0
  140. package/dist/modules/agent-instructions/compose.js.map +1 -0
  141. package/dist/modules/agent-instructions/index.d.ts +4 -0
  142. package/dist/modules/agent-instructions/index.d.ts.map +1 -0
  143. package/dist/modules/agent-instructions/index.js +4 -0
  144. package/dist/modules/agent-instructions/index.js.map +1 -0
  145. package/dist/modules/agent-instructions/read-preamble.d.ts +37 -0
  146. package/dist/modules/agent-instructions/read-preamble.d.ts.map +1 -0
  147. package/dist/modules/agent-instructions/read-preamble.js +81 -0
  148. package/dist/modules/agent-instructions/read-preamble.js.map +1 -0
  149. package/dist/modules/auth/account-erasure.service.d.ts +30 -4
  150. package/dist/modules/auth/account-erasure.service.d.ts.map +1 -1
  151. package/dist/modules/auth/account-erasure.service.js +96 -10
  152. package/dist/modules/auth/account-erasure.service.js.map +1 -1
  153. package/dist/modules/auth/account.routes.d.ts +6 -3
  154. package/dist/modules/auth/account.routes.d.ts.map +1 -1
  155. package/dist/modules/auth/account.routes.js +109 -10
  156. package/dist/modules/auth/account.routes.js.map +1 -1
  157. package/dist/modules/auth/auth.routes.d.ts +10 -3
  158. package/dist/modules/auth/auth.routes.d.ts.map +1 -1
  159. package/dist/modules/auth/auth.routes.js +9 -4
  160. package/dist/modules/auth/auth.routes.js.map +1 -1
  161. package/dist/modules/auth/auth.service.d.ts +89 -5
  162. package/dist/modules/auth/auth.service.d.ts.map +1 -1
  163. package/dist/modules/auth/auth.service.js +160 -25
  164. package/dist/modules/auth/auth.service.js.map +1 -1
  165. package/dist/modules/auth/oidc-auth-provider.d.ts +46 -3
  166. package/dist/modules/auth/oidc-auth-provider.d.ts.map +1 -1
  167. package/dist/modules/auth/oidc-auth-provider.js +99 -21
  168. package/dist/modules/auth/oidc-auth-provider.js.map +1 -1
  169. package/dist/modules/code-mode/code-mode.tool.d.ts.map +1 -1
  170. package/dist/modules/code-mode/code-mode.tool.js +14 -2
  171. package/dist/modules/code-mode/code-mode.tool.js.map +1 -1
  172. package/dist/modules/connection-probe/connection-probe.service.d.ts.map +1 -1
  173. package/dist/modules/connection-probe/connection-probe.service.js +56 -7
  174. package/dist/modules/connection-probe/connection-probe.service.js.map +1 -1
  175. package/dist/modules/database/advisory-lock.d.ts +103 -0
  176. package/dist/modules/database/advisory-lock.d.ts.map +1 -0
  177. package/dist/modules/database/advisory-lock.js +320 -0
  178. package/dist/modules/database/advisory-lock.js.map +1 -0
  179. package/dist/modules/database/connection.d.ts.map +1 -1
  180. package/dist/modules/database/connection.js +9 -1
  181. package/dist/modules/database/connection.js.map +1 -1
  182. package/dist/modules/database/core-schema.d.ts +672 -0
  183. package/dist/modules/database/core-schema.d.ts.map +1 -1
  184. package/dist/modules/database/core-schema.js +171 -0
  185. package/dist/modules/database/core-schema.js.map +1 -1
  186. package/dist/modules/database/migrate.d.ts.map +1 -1
  187. package/dist/modules/database/migrate.js +26 -6
  188. package/dist/modules/database/migrate.js.map +1 -1
  189. package/dist/modules/declared-variables/declared-variables.routes.d.ts.map +1 -1
  190. package/dist/modules/declared-variables/declared-variables.routes.js +4 -2
  191. package/dist/modules/declared-variables/declared-variables.routes.js.map +1 -1
  192. package/dist/modules/diff/diff.routes.d.ts.map +1 -1
  193. package/dist/modules/diff/diff.routes.js +2 -4
  194. package/dist/modules/diff/diff.routes.js.map +1 -1
  195. package/dist/modules/diff/diff.service.d.ts +4 -1
  196. package/dist/modules/diff/diff.service.d.ts.map +1 -1
  197. package/dist/modules/diff/diff.service.js +79 -81
  198. package/dist/modules/diff/diff.service.js.map +1 -1
  199. package/dist/modules/kb-fs/bevel-ignore.d.ts +35 -0
  200. package/dist/modules/kb-fs/bevel-ignore.d.ts.map +1 -0
  201. package/dist/modules/kb-fs/bevel-ignore.js +71 -0
  202. package/dist/modules/kb-fs/bevel-ignore.js.map +1 -0
  203. package/dist/modules/kb-fs/file-change-notifier.d.ts.map +1 -1
  204. package/dist/modules/kb-fs/file-change-notifier.js +3 -1
  205. package/dist/modules/kb-fs/file-change-notifier.js.map +1 -1
  206. package/dist/modules/kb-fs/git-guarded-filesystem.d.ts +37 -0
  207. package/dist/modules/kb-fs/git-guarded-filesystem.d.ts.map +1 -0
  208. package/dist/modules/kb-fs/git-guarded-filesystem.js +104 -0
  209. package/dist/modules/kb-fs/git-guarded-filesystem.js.map +1 -0
  210. package/dist/modules/kb-fs/locking-filesystem.d.ts +113 -18
  211. package/dist/modules/kb-fs/locking-filesystem.d.ts.map +1 -1
  212. package/dist/modules/kb-fs/locking-filesystem.js +353 -76
  213. package/dist/modules/kb-fs/locking-filesystem.js.map +1 -1
  214. package/dist/modules/kb-fs/node-fs.d.ts +23 -0
  215. package/dist/modules/kb-fs/node-fs.d.ts.map +1 -0
  216. package/dist/modules/kb-fs/node-fs.js +158 -0
  217. package/dist/modules/kb-fs/node-fs.js.map +1 -0
  218. package/dist/modules/kb-fs/read-only-filesystem.d.ts +10 -9
  219. package/dist/modules/kb-fs/read-only-filesystem.d.ts.map +1 -1
  220. package/dist/modules/kb-fs/read-only-filesystem.js +18 -9
  221. package/dist/modules/kb-fs/read-only-filesystem.js.map +1 -1
  222. package/dist/modules/kb-fs/repo-path.d.ts +100 -3
  223. package/dist/modules/kb-fs/repo-path.d.ts.map +1 -1
  224. package/dist/modules/kb-fs/repo-path.js +195 -3
  225. package/dist/modules/kb-fs/repo-path.js.map +1 -1
  226. package/dist/modules/kb-sync/index.d.ts +6 -0
  227. package/dist/modules/kb-sync/index.d.ts.map +1 -0
  228. package/dist/modules/kb-sync/index.js +5 -0
  229. package/dist/modules/kb-sync/index.js.map +1 -0
  230. package/dist/modules/kb-sync/kb-sync.interface.d.ts +77 -0
  231. package/dist/modules/kb-sync/kb-sync.interface.d.ts.map +1 -0
  232. package/dist/modules/kb-sync/kb-sync.interface.js +2 -0
  233. package/dist/modules/kb-sync/kb-sync.interface.js.map +1 -0
  234. package/dist/modules/kb-sync/kb-sync.routes.d.ts +70 -0
  235. package/dist/modules/kb-sync/kb-sync.routes.d.ts.map +1 -0
  236. package/dist/modules/kb-sync/kb-sync.routes.js +137 -0
  237. package/dist/modules/kb-sync/kb-sync.routes.js.map +1 -0
  238. package/dist/modules/kb-sync/kb-sync.service.d.ts +45 -0
  239. package/dist/modules/kb-sync/kb-sync.service.d.ts.map +1 -0
  240. package/dist/modules/kb-sync/kb-sync.service.js +185 -0
  241. package/dist/modules/kb-sync/kb-sync.service.js.map +1 -0
  242. package/dist/modules/kb-sync/sync-auth.d.ts +59 -0
  243. package/dist/modules/kb-sync/sync-auth.d.ts.map +1 -0
  244. package/dist/modules/kb-sync/sync-auth.js +62 -0
  245. package/dist/modules/kb-sync/sync-auth.js.map +1 -0
  246. package/dist/modules/kb-sync/sync-payload.d.ts +32 -0
  247. package/dist/modules/kb-sync/sync-payload.d.ts.map +1 -0
  248. package/dist/modules/kb-sync/sync-payload.js +105 -0
  249. package/dist/modules/kb-sync/sync-payload.js.map +1 -0
  250. package/dist/modules/marketplace/git-http.routes.d.ts +42 -0
  251. package/dist/modules/marketplace/git-http.routes.d.ts.map +1 -0
  252. package/dist/modules/marketplace/git-http.routes.js +192 -0
  253. package/dist/modules/marketplace/git-http.routes.js.map +1 -0
  254. package/dist/modules/marketplace/github-facade/github-facade-admin.routes.d.ts +33 -0
  255. package/dist/modules/marketplace/github-facade/github-facade-admin.routes.d.ts.map +1 -0
  256. package/dist/modules/marketplace/github-facade/github-facade-admin.routes.js +88 -0
  257. package/dist/modules/marketplace/github-facade/github-facade-admin.routes.js.map +1 -0
  258. package/dist/modules/marketplace/github-facade/github-facade-codes.store.d.ts +59 -0
  259. package/dist/modules/marketplace/github-facade/github-facade-codes.store.d.ts.map +1 -0
  260. package/dist/modules/marketplace/github-facade/github-facade-codes.store.js +96 -0
  261. package/dist/modules/marketplace/github-facade/github-facade-codes.store.js.map +1 -0
  262. package/dist/modules/marketplace/github-facade/github-facade-credentials.service.d.ts +126 -0
  263. package/dist/modules/marketplace/github-facade/github-facade-credentials.service.d.ts.map +1 -0
  264. package/dist/modules/marketplace/github-facade/github-facade-credentials.service.js +192 -0
  265. package/dist/modules/marketplace/github-facade/github-facade-credentials.service.js.map +1 -0
  266. package/dist/modules/marketplace/github-facade/github-facade.routes.d.ts +47 -0
  267. package/dist/modules/marketplace/github-facade/github-facade.routes.d.ts.map +1 -0
  268. package/dist/modules/marketplace/github-facade/github-facade.routes.js +252 -0
  269. package/dist/modules/marketplace/github-facade/github-facade.routes.js.map +1 -0
  270. package/dist/modules/marketplace/github-facade/github-facade.service.d.ts +121 -0
  271. package/dist/modules/marketplace/github-facade/github-facade.service.d.ts.map +1 -0
  272. package/dist/modules/marketplace/github-facade/github-facade.service.js +213 -0
  273. package/dist/modules/marketplace/github-facade/github-facade.service.js.map +1 -0
  274. package/dist/modules/marketplace/github-facade/index.d.ts +6 -0
  275. package/dist/modules/marketplace/github-facade/index.d.ts.map +1 -0
  276. package/dist/modules/marketplace/github-facade/index.js +6 -0
  277. package/dist/modules/marketplace/github-facade/index.js.map +1 -0
  278. package/dist/modules/marketplace/index.d.ts +4 -0
  279. package/dist/modules/marketplace/index.d.ts.map +1 -0
  280. package/dist/modules/marketplace/index.js +4 -0
  281. package/dist/modules/marketplace/index.js.map +1 -0
  282. package/dist/modules/marketplace/marketplace-repo.service.d.ts +135 -0
  283. package/dist/modules/marketplace/marketplace-repo.service.d.ts.map +1 -0
  284. package/dist/modules/marketplace/marketplace-repo.service.js +318 -0
  285. package/dist/modules/marketplace/marketplace-repo.service.js.map +1 -0
  286. package/dist/modules/mcp/downstream-pool.d.ts +107 -0
  287. package/dist/modules/mcp/downstream-pool.d.ts.map +1 -0
  288. package/dist/modules/mcp/downstream-pool.js +221 -0
  289. package/dist/modules/mcp/downstream-pool.js.map +1 -0
  290. package/dist/modules/mcp/downstream-token-refresh.d.ts +77 -0
  291. package/dist/modules/mcp/downstream-token-refresh.d.ts.map +1 -0
  292. package/dist/modules/mcp/downstream-token-refresh.js +166 -0
  293. package/dist/modules/mcp/downstream-token-refresh.js.map +1 -0
  294. package/dist/modules/mcp/first-call-probe.cli-args.d.ts +78 -0
  295. package/dist/modules/mcp/first-call-probe.cli-args.d.ts.map +1 -0
  296. package/dist/modules/mcp/first-call-probe.cli-args.js +151 -0
  297. package/dist/modules/mcp/first-call-probe.cli-args.js.map +1 -0
  298. package/dist/modules/mcp/first-call-probe.cli.d.ts +2 -0
  299. package/dist/modules/mcp/first-call-probe.cli.d.ts.map +1 -0
  300. package/dist/modules/mcp/first-call-probe.cli.js +62 -0
  301. package/dist/modules/mcp/first-call-probe.cli.js.map +1 -0
  302. package/dist/modules/mcp/first-call-probe.d.ts +165 -0
  303. package/dist/modules/mcp/first-call-probe.d.ts.map +1 -0
  304. package/dist/modules/mcp/first-call-probe.js +298 -0
  305. package/dist/modules/mcp/first-call-probe.js.map +1 -0
  306. package/dist/modules/mcp/manual-failure-memo.d.ts +42 -17
  307. package/dist/modules/mcp/manual-failure-memo.d.ts.map +1 -1
  308. package/dist/modules/mcp/manual-failure-memo.js +79 -23
  309. package/dist/modules/mcp/manual-failure-memo.js.map +1 -1
  310. package/dist/modules/mcp/mcp-auth.middleware.d.ts +9 -5
  311. package/dist/modules/mcp/mcp-auth.middleware.d.ts.map +1 -1
  312. package/dist/modules/mcp/mcp-auth.middleware.js +24 -16
  313. package/dist/modules/mcp/mcp-auth.middleware.js.map +1 -1
  314. package/dist/modules/mcp/mcp.routes.d.ts +4 -4
  315. package/dist/modules/mcp/mcp.routes.d.ts.map +1 -1
  316. package/dist/modules/mcp/mcp.routes.js +83 -179
  317. package/dist/modules/mcp/mcp.routes.js.map +1 -1
  318. package/dist/modules/mcp/mcp.service.d.ts +218 -61
  319. package/dist/modules/mcp/mcp.service.d.ts.map +1 -1
  320. package/dist/modules/mcp/mcp.service.js +713 -159
  321. package/dist/modules/mcp/mcp.service.js.map +1 -1
  322. package/dist/modules/mcp/oauth/bevel-oauth-provider.d.ts.map +1 -1
  323. package/dist/modules/mcp/oauth/bevel-oauth-provider.js +8 -2
  324. package/dist/modules/mcp/oauth/bevel-oauth-provider.js.map +1 -1
  325. package/dist/modules/mcp/oauth/oauth-consent.routes.d.ts +14 -8
  326. package/dist/modules/mcp/oauth/oauth-consent.routes.d.ts.map +1 -1
  327. package/dist/modules/mcp/oauth/oauth-consent.routes.js +16 -2
  328. package/dist/modules/mcp/oauth/oauth-consent.routes.js.map +1 -1
  329. package/dist/modules/mcp/oauth/oauth-state.d.ts +13 -2
  330. package/dist/modules/mcp/oauth/oauth-state.d.ts.map +1 -1
  331. package/dist/modules/mcp/oauth/oauth-state.js +3 -1
  332. package/dist/modules/mcp/oauth/oauth-state.js.map +1 -1
  333. package/dist/modules/mcp/surface-log-throttle.d.ts +59 -0
  334. package/dist/modules/mcp/surface-log-throttle.d.ts.map +1 -0
  335. package/dist/modules/mcp/surface-log-throttle.js +94 -0
  336. package/dist/modules/mcp/surface-log-throttle.js.map +1 -0
  337. package/dist/modules/plugins/compile/compile-marketplace.d.ts +104 -0
  338. package/dist/modules/plugins/compile/compile-marketplace.d.ts.map +1 -0
  339. package/dist/modules/plugins/compile/compile-marketplace.js +323 -0
  340. package/dist/modules/plugins/compile/compile-marketplace.js.map +1 -0
  341. package/dist/modules/plugins/compile/marketplace-compiler.service.d.ts +73 -0
  342. package/dist/modules/plugins/compile/marketplace-compiler.service.d.ts.map +1 -0
  343. package/dist/modules/plugins/compile/marketplace-compiler.service.js +135 -0
  344. package/dist/modules/plugins/compile/marketplace-compiler.service.js.map +1 -0
  345. package/dist/modules/plugins/discovery/bundle-dialect/bundle.source.d.ts +37 -0
  346. package/dist/modules/plugins/discovery/bundle-dialect/bundle.source.d.ts.map +1 -0
  347. package/dist/modules/plugins/discovery/bundle-dialect/bundle.source.js +158 -0
  348. package/dist/modules/plugins/discovery/bundle-dialect/bundle.source.js.map +1 -0
  349. package/dist/modules/plugins/discovery/bundle-dialect/registry.d.ts +65 -0
  350. package/dist/modules/plugins/discovery/bundle-dialect/registry.d.ts.map +1 -0
  351. package/dist/modules/plugins/discovery/bundle-dialect/registry.js +204 -0
  352. package/dist/modules/plugins/discovery/bundle-dialect/registry.js.map +1 -0
  353. package/dist/modules/plugins/discovery/kb-plugin-source.d.ts +39 -0
  354. package/dist/modules/plugins/discovery/kb-plugin-source.d.ts.map +1 -0
  355. package/dist/modules/plugins/discovery/kb-plugin-source.js +110 -0
  356. package/dist/modules/plugins/discovery/kb-plugin-source.js.map +1 -0
  357. package/dist/modules/plugins/discovery/native.source.d.ts +16 -0
  358. package/dist/modules/plugins/discovery/native.source.d.ts.map +1 -0
  359. package/dist/modules/plugins/discovery/native.source.js +120 -0
  360. package/dist/modules/plugins/discovery/native.source.js.map +1 -0
  361. package/dist/modules/plugins/discovery/plugin-source.d.ts +110 -0
  362. package/dist/modules/plugins/discovery/plugin-source.d.ts.map +1 -0
  363. package/dist/modules/plugins/discovery/plugin-source.js +2 -0
  364. package/dist/modules/plugins/discovery/plugin-source.js.map +1 -0
  365. package/dist/modules/plugins/index.d.ts +15 -3
  366. package/dist/modules/plugins/index.d.ts.map +1 -1
  367. package/dist/modules/plugins/index.js +13 -2
  368. package/dist/modules/plugins/index.js.map +1 -1
  369. package/dist/modules/plugins/join-request-jobs.service.d.ts +250 -0
  370. package/dist/modules/plugins/join-request-jobs.service.d.ts.map +1 -0
  371. package/dist/modules/plugins/join-request-jobs.service.js +602 -0
  372. package/dist/modules/plugins/join-request-jobs.service.js.map +1 -0
  373. package/dist/modules/plugins/join-request-records.store.d.ts +161 -0
  374. package/dist/modules/plugins/join-request-records.store.d.ts.map +1 -0
  375. package/dist/modules/plugins/join-request-records.store.js +179 -0
  376. package/dist/modules/plugins/join-request-records.store.js.map +1 -0
  377. package/dist/modules/plugins/join-requests.service.d.ts.map +1 -1
  378. package/dist/modules/plugins/join-requests.service.js +4 -2
  379. package/dist/modules/plugins/join-requests.service.js.map +1 -1
  380. package/dist/modules/plugins/plugin-links.d.ts +69 -0
  381. package/dist/modules/plugins/plugin-links.d.ts.map +1 -0
  382. package/dist/modules/plugins/plugin-links.js +163 -0
  383. package/dist/modules/plugins/plugin-links.js.map +1 -0
  384. package/dist/modules/plugins/plugin-links.service.d.ts +111 -0
  385. package/dist/modules/plugins/plugin-links.service.d.ts.map +1 -0
  386. package/dist/modules/plugins/plugin-links.service.js +295 -0
  387. package/dist/modules/plugins/plugin-links.service.js.map +1 -0
  388. package/dist/modules/plugins/plugin-provision.service.d.ts +129 -21
  389. package/dist/modules/plugins/plugin-provision.service.d.ts.map +1 -1
  390. package/dist/modules/plugins/plugin-provision.service.js +320 -75
  391. package/dist/modules/plugins/plugin-provision.service.js.map +1 -1
  392. package/dist/modules/plugins/plugin-rename.service.d.ts +71 -0
  393. package/dist/modules/plugins/plugin-rename.service.d.ts.map +1 -0
  394. package/dist/modules/plugins/plugin-rename.service.js +254 -0
  395. package/dist/modules/plugins/plugin-rename.service.js.map +1 -0
  396. package/dist/modules/plugins/plugins.contract.d.ts +78 -4
  397. package/dist/modules/plugins/plugins.contract.d.ts.map +1 -1
  398. package/dist/modules/plugins/plugins.routes.d.ts +44 -5
  399. package/dist/modules/plugins/plugins.routes.d.ts.map +1 -1
  400. package/dist/modules/plugins/plugins.routes.js +297 -115
  401. package/dist/modules/plugins/plugins.routes.js.map +1 -1
  402. package/dist/modules/plugins/plugins.service.d.ts +82 -3
  403. package/dist/modules/plugins/plugins.service.d.ts.map +1 -1
  404. package/dist/modules/plugins/plugins.service.js +178 -48
  405. package/dist/modules/plugins/plugins.service.js.map +1 -1
  406. package/dist/modules/plugins/plugins.tools.d.ts +25 -0
  407. package/dist/modules/plugins/plugins.tools.d.ts.map +1 -0
  408. package/dist/modules/plugins/plugins.tools.js +82 -0
  409. package/dist/modules/plugins/plugins.tools.js.map +1 -0
  410. package/dist/modules/plugins/teams.routes.d.ts +42 -0
  411. package/dist/modules/plugins/teams.routes.d.ts.map +1 -0
  412. package/dist/modules/plugins/teams.routes.js +79 -0
  413. package/dist/modules/plugins/teams.routes.js.map +1 -0
  414. package/dist/modules/secrets-vault/db-secrets-vault.service.d.ts +40 -1
  415. package/dist/modules/secrets-vault/db-secrets-vault.service.d.ts.map +1 -1
  416. package/dist/modules/secrets-vault/db-secrets-vault.service.js +202 -37
  417. package/dist/modules/secrets-vault/db-secrets-vault.service.js.map +1 -1
  418. package/dist/modules/secrets-vault/index.d.ts +1 -1
  419. package/dist/modules/secrets-vault/index.d.ts.map +1 -1
  420. package/dist/modules/secrets-vault/index.js.map +1 -1
  421. package/dist/modules/secrets-vault/mcp-oauth-discovery.service.d.ts.map +1 -1
  422. package/dist/modules/secrets-vault/mcp-oauth-discovery.service.js +5 -3
  423. package/dist/modules/secrets-vault/mcp-oauth-discovery.service.js.map +1 -1
  424. package/dist/modules/secrets-vault/secrets-variable-loader.d.ts.map +1 -1
  425. package/dist/modules/secrets-vault/secrets-variable-loader.js +3 -1
  426. package/dist/modules/secrets-vault/secrets-variable-loader.js.map +1 -1
  427. package/dist/modules/secrets-vault/secrets-vault.contract.d.ts +78 -0
  428. package/dist/modules/secrets-vault/secrets-vault.contract.d.ts.map +1 -1
  429. package/dist/modules/secrets-vault/secrets-vault.contract.js +40 -0
  430. package/dist/modules/secrets-vault/secrets-vault.contract.js.map +1 -1
  431. package/dist/modules/secrets-vault/secrets-vault.routes.d.ts.map +1 -1
  432. package/dist/modules/secrets-vault/secrets-vault.routes.js +106 -52
  433. package/dist/modules/secrets-vault/secrets-vault.routes.js.map +1 -1
  434. package/dist/modules/settings/connection-check.d.ts +118 -0
  435. package/dist/modules/settings/connection-check.d.ts.map +1 -0
  436. package/dist/modules/settings/connection-check.js +287 -0
  437. package/dist/modules/settings/connection-check.js.map +1 -0
  438. package/dist/modules/settings/deployment-settings.service.d.ts +157 -2
  439. package/dist/modules/settings/deployment-settings.service.d.ts.map +1 -1
  440. package/dist/modules/settings/deployment-settings.service.js +412 -35
  441. package/dist/modules/settings/deployment-settings.service.js.map +1 -1
  442. package/dist/modules/settings/git-root-folders.d.ts +54 -0
  443. package/dist/modules/settings/git-root-folders.d.ts.map +1 -0
  444. package/dist/modules/settings/git-root-folders.js +130 -0
  445. package/dist/modules/settings/git-root-folders.js.map +1 -0
  446. package/dist/modules/settings/oidc-check.d.ts +85 -0
  447. package/dist/modules/settings/oidc-check.d.ts.map +1 -0
  448. package/dist/modules/settings/oidc-check.js +172 -0
  449. package/dist/modules/settings/oidc-check.js.map +1 -0
  450. package/dist/modules/settings/setup.routes.d.ts +57 -1
  451. package/dist/modules/settings/setup.routes.d.ts.map +1 -1
  452. package/dist/modules/settings/setup.routes.js +408 -108
  453. package/dist/modules/settings/setup.routes.js.map +1 -1
  454. package/dist/modules/skills/allowed-tools-check.d.ts +132 -0
  455. package/dist/modules/skills/allowed-tools-check.d.ts.map +1 -0
  456. package/dist/modules/skills/allowed-tools-check.js +260 -0
  457. package/dist/modules/skills/allowed-tools-check.js.map +1 -0
  458. package/dist/modules/skills/index.d.ts +4 -1
  459. package/dist/modules/skills/index.d.ts.map +1 -1
  460. package/dist/modules/skills/index.js +2 -0
  461. package/dist/modules/skills/index.js.map +1 -1
  462. package/dist/modules/skills/pending-skills.service.d.ts +6 -11
  463. package/dist/modules/skills/pending-skills.service.d.ts.map +1 -1
  464. package/dist/modules/skills/pending-skills.service.js +24 -71
  465. package/dist/modules/skills/pending-skills.service.js.map +1 -1
  466. package/dist/modules/skills/skill-access-requests.routes.d.ts +38 -0
  467. package/dist/modules/skills/skill-access-requests.routes.d.ts.map +1 -0
  468. package/dist/modules/skills/skill-access-requests.routes.js +171 -0
  469. package/dist/modules/skills/skill-access-requests.routes.js.map +1 -0
  470. package/dist/modules/skills/skills.contract.d.ts +67 -2
  471. package/dist/modules/skills/skills.contract.d.ts.map +1 -1
  472. package/dist/modules/skills/skills.routes.d.ts +35 -3
  473. package/dist/modules/skills/skills.routes.d.ts.map +1 -1
  474. package/dist/modules/skills/skills.routes.js +57 -4
  475. package/dist/modules/skills/skills.routes.js.map +1 -1
  476. package/dist/modules/skills/skills.service.d.ts +31 -3
  477. package/dist/modules/skills/skills.service.d.ts.map +1 -1
  478. package/dist/modules/skills/skills.service.js +236 -65
  479. package/dist/modules/skills/skills.service.js.map +1 -1
  480. package/dist/modules/skills/skills.tools.d.ts +4 -1
  481. package/dist/modules/skills/skills.tools.d.ts.map +1 -1
  482. package/dist/modules/skills/skills.tools.js +41 -6
  483. package/dist/modules/skills/skills.tools.js.map +1 -1
  484. package/dist/modules/tool-auth/connection-key-rejection.d.ts +18 -0
  485. package/dist/modules/tool-auth/connection-key-rejection.d.ts.map +1 -0
  486. package/dist/modules/tool-auth/connection-key-rejection.js +20 -0
  487. package/dist/modules/tool-auth/connection-key-rejection.js.map +1 -0
  488. package/dist/modules/tool-auth/connection-keys-admin.routes.d.ts +14 -0
  489. package/dist/modules/tool-auth/connection-keys-admin.routes.d.ts.map +1 -0
  490. package/dist/modules/tool-auth/connection-keys-admin.routes.js +67 -0
  491. package/dist/modules/tool-auth/connection-keys-admin.routes.js.map +1 -0
  492. package/dist/modules/tool-auth/external-api-key.interface.d.ts +66 -1
  493. package/dist/modules/tool-auth/external-api-key.interface.d.ts.map +1 -1
  494. package/dist/modules/tool-auth/external-api-key.interface.js +2 -1
  495. package/dist/modules/tool-auth/external-api-key.interface.js.map +1 -1
  496. package/dist/modules/tool-auth/external-api-key.service.d.ts +23 -4
  497. package/dist/modules/tool-auth/external-api-key.service.d.ts.map +1 -1
  498. package/dist/modules/tool-auth/external-api-key.service.js +79 -19
  499. package/dist/modules/tool-auth/external-api-key.service.js.map +1 -1
  500. package/dist/modules/tool-auth/internal-token.service.d.ts +5 -2
  501. package/dist/modules/tool-auth/internal-token.service.d.ts.map +1 -1
  502. package/dist/modules/tool-auth/internal-token.service.js +5 -2
  503. package/dist/modules/tool-auth/internal-token.service.js.map +1 -1
  504. package/dist/modules/tool-auth/key-or-session.middleware.d.ts +23 -0
  505. package/dist/modules/tool-auth/key-or-session.middleware.d.ts.map +1 -0
  506. package/dist/modules/tool-auth/key-or-session.middleware.js +39 -0
  507. package/dist/modules/tool-auth/key-or-session.middleware.js.map +1 -0
  508. package/dist/modules/tool-auth/tool-auth.middleware.d.ts +7 -1
  509. package/dist/modules/tool-auth/tool-auth.middleware.d.ts.map +1 -1
  510. package/dist/modules/tool-auth/tool-auth.middleware.js +8 -5
  511. package/dist/modules/tool-auth/tool-auth.middleware.js.map +1 -1
  512. package/dist/modules/tool-helpers/tool-context.d.ts +14 -0
  513. package/dist/modules/tool-helpers/tool-context.d.ts.map +1 -1
  514. package/dist/modules/tool-helpers/tool-context.js +28 -6
  515. package/dist/modules/tool-helpers/tool-context.js.map +1 -1
  516. package/dist/modules/tool-helpers/tool-handler.d.ts.map +1 -1
  517. package/dist/modules/tool-helpers/tool-handler.js +22 -5
  518. package/dist/modules/tool-helpers/tool-handler.js.map +1 -1
  519. package/dist/modules/tool-helpers/tool.contract.d.ts +6 -1
  520. package/dist/modules/tool-helpers/tool.contract.d.ts.map +1 -1
  521. package/dist/modules/tool-helpers/tool.contract.js +7 -1
  522. package/dist/modules/tool-helpers/tool.contract.js.map +1 -1
  523. package/dist/modules/tool-manuals/index.d.ts +2 -1
  524. package/dist/modules/tool-manuals/index.d.ts.map +1 -1
  525. package/dist/modules/tool-manuals/index.js +1 -0
  526. package/dist/modules/tool-manuals/index.js.map +1 -1
  527. package/dist/modules/tool-manuals/mcp-json-discovery.d.ts +36 -0
  528. package/dist/modules/tool-manuals/mcp-json-discovery.d.ts.map +1 -1
  529. package/dist/modules/tool-manuals/mcp-json-discovery.js +136 -96
  530. package/dist/modules/tool-manuals/mcp-json-discovery.js.map +1 -1
  531. package/dist/modules/tool-manuals/mcp-server-edit.service.d.ts +10 -2
  532. package/dist/modules/tool-manuals/mcp-server-edit.service.d.ts.map +1 -1
  533. package/dist/modules/tool-manuals/mcp-server-edit.service.js +12 -15
  534. package/dist/modules/tool-manuals/mcp-server-edit.service.js.map +1 -1
  535. package/dist/modules/tool-manuals/pending-tools.service.d.ts +50 -0
  536. package/dist/modules/tool-manuals/pending-tools.service.d.ts.map +1 -0
  537. package/dist/modules/tool-manuals/pending-tools.service.js +149 -0
  538. package/dist/modules/tool-manuals/pending-tools.service.js.map +1 -0
  539. package/dist/modules/tool-manuals/tool-delete.service.d.ts +135 -0
  540. package/dist/modules/tool-manuals/tool-delete.service.d.ts.map +1 -0
  541. package/dist/modules/tool-manuals/tool-delete.service.js +303 -0
  542. package/dist/modules/tool-manuals/tool-delete.service.js.map +1 -0
  543. package/dist/modules/tool-manuals/tool-manuals.contract.d.ts +161 -0
  544. package/dist/modules/tool-manuals/tool-manuals.contract.d.ts.map +1 -1
  545. package/dist/modules/tool-manuals/tool-manuals.contract.js +9 -0
  546. package/dist/modules/tool-manuals/tool-manuals.contract.js.map +1 -1
  547. package/dist/modules/tool-manuals/tool-manuals.routes.d.ts +19 -2
  548. package/dist/modules/tool-manuals/tool-manuals.routes.d.ts.map +1 -1
  549. package/dist/modules/tool-manuals/tool-manuals.routes.js +127 -57
  550. package/dist/modules/tool-manuals/tool-manuals.routes.js.map +1 -1
  551. package/dist/modules/tool-manuals/tool-manuals.service.d.ts +121 -2
  552. package/dist/modules/tool-manuals/tool-manuals.service.d.ts.map +1 -1
  553. package/dist/modules/tool-manuals/tool-manuals.service.js +415 -92
  554. package/dist/modules/tool-manuals/tool-manuals.service.js.map +1 -1
  555. package/dist/modules/tool-manuals/tool-manuals.tools.d.ts.map +1 -1
  556. package/dist/modules/tool-manuals/tool-manuals.tools.js +78 -6
  557. package/dist/modules/tool-manuals/tool-manuals.tools.js.map +1 -1
  558. package/dist/modules/workflow/agent-tools/workflow.tools.d.ts.map +1 -1
  559. package/dist/modules/workflow/agent-tools/workflow.tools.js +88 -54
  560. package/dist/modules/workflow/agent-tools/workflow.tools.js.map +1 -1
  561. package/dist/modules/workflow/event-bus.d.ts +10 -4
  562. package/dist/modules/workflow/event-bus.d.ts.map +1 -1
  563. package/dist/modules/workflow/event-bus.js +14 -11
  564. package/dist/modules/workflow/event-bus.js.map +1 -1
  565. package/dist/modules/workflow/events.routes.d.ts +17 -2
  566. package/dist/modules/workflow/events.routes.d.ts.map +1 -1
  567. package/dist/modules/workflow/events.routes.js +31 -12
  568. package/dist/modules/workflow/events.routes.js.map +1 -1
  569. package/dist/modules/workflow/file-lock.service.d.ts +22 -4
  570. package/dist/modules/workflow/file-lock.service.d.ts.map +1 -1
  571. package/dist/modules/workflow/file-lock.service.js +27 -4
  572. package/dist/modules/workflow/file-lock.service.js.map +1 -1
  573. package/dist/modules/workflow/git/change-request-link.d.ts +20 -0
  574. package/dist/modules/workflow/git/change-request-link.d.ts.map +1 -0
  575. package/dist/modules/workflow/git/change-request-link.js +34 -0
  576. package/dist/modules/workflow/git/change-request-link.js.map +1 -0
  577. package/dist/modules/workflow/git/git-version.d.ts.map +1 -1
  578. package/dist/modules/workflow/git/git-version.js +3 -1
  579. package/dist/modules/workflow/git/git-version.js.map +1 -1
  580. package/dist/modules/workflow/git/git.service.d.ts +303 -6
  581. package/dist/modules/workflow/git/git.service.d.ts.map +1 -1
  582. package/dist/modules/workflow/git/git.service.js +753 -170
  583. package/dist/modules/workflow/git/git.service.js.map +1 -1
  584. package/dist/modules/workflow/git/node-git-runner.d.ts +36 -0
  585. package/dist/modules/workflow/git/node-git-runner.d.ts.map +1 -0
  586. package/dist/modules/workflow/git/node-git-runner.js +273 -0
  587. package/dist/modules/workflow/git/node-git-runner.js.map +1 -0
  588. package/dist/modules/workflow/git/pull-request.service.d.ts +57 -2
  589. package/dist/modules/workflow/git/pull-request.service.d.ts.map +1 -1
  590. package/dist/modules/workflow/git/pull-request.service.js +165 -25
  591. package/dist/modules/workflow/git/pull-request.service.js.map +1 -1
  592. package/dist/modules/workflow/pending-commits.service.d.ts +11 -0
  593. package/dist/modules/workflow/pending-commits.service.d.ts.map +1 -1
  594. package/dist/modules/workflow/pending-commits.service.js +24 -4
  595. package/dist/modules/workflow/pending-commits.service.js.map +1 -1
  596. package/dist/modules/workflow/pending-commits.worker.d.ts +15 -8
  597. package/dist/modules/workflow/pending-commits.worker.d.ts.map +1 -1
  598. package/dist/modules/workflow/pending-commits.worker.js +107 -58
  599. package/dist/modules/workflow/pending-commits.worker.js.map +1 -1
  600. package/dist/modules/workflow/recovery-bot.d.ts.map +1 -1
  601. package/dist/modules/workflow/recovery-bot.js +3 -1
  602. package/dist/modules/workflow/recovery-bot.js.map +1 -1
  603. package/dist/modules/workflow/review-workflow/approval-lock.d.ts +60 -0
  604. package/dist/modules/workflow/review-workflow/approval-lock.d.ts.map +1 -0
  605. package/dist/modules/workflow/review-workflow/approval-lock.js +116 -0
  606. package/dist/modules/workflow/review-workflow/approval-lock.js.map +1 -0
  607. package/dist/modules/workflow/review-workflow/review-workflow.interface.d.ts +54 -7
  608. package/dist/modules/workflow/review-workflow/review-workflow.interface.d.ts.map +1 -1
  609. package/dist/modules/workflow/review-workflow/review-workflow.service.d.ts +64 -0
  610. package/dist/modules/workflow/review-workflow/review-workflow.service.d.ts.map +1 -1
  611. package/dist/modules/workflow/review-workflow/review-workflow.service.js +269 -90
  612. package/dist/modules/workflow/review-workflow/review-workflow.service.js.map +1 -1
  613. package/dist/modules/workflow/sanitize-error.d.ts +5 -2
  614. package/dist/modules/workflow/sanitize-error.d.ts.map +1 -1
  615. package/dist/modules/workflow/sanitize-error.js +8 -4
  616. package/dist/modules/workflow/sanitize-error.js.map +1 -1
  617. package/dist/modules/workflow/workflow.routes.d.ts +2 -0
  618. package/dist/modules/workflow/workflow.routes.d.ts.map +1 -1
  619. package/dist/modules/workflow/workflow.routes.js +259 -29
  620. package/dist/modules/workflow/workflow.routes.js.map +1 -1
  621. package/dist/modules/workflow/workflow.service.d.ts +217 -14
  622. package/dist/modules/workflow/workflow.service.d.ts.map +1 -1
  623. package/dist/modules/workflow/workflow.service.js +1142 -109
  624. package/dist/modules/workflow/workflow.service.js.map +1 -1
  625. package/dist/modules/workspace/empty-dirs.d.ts +15 -0
  626. package/dist/modules/workspace/empty-dirs.d.ts.map +1 -0
  627. package/dist/modules/workspace/empty-dirs.js +90 -0
  628. package/dist/modules/workspace/empty-dirs.js.map +1 -0
  629. package/dist/modules/workspace/file-readers/content-mode.d.ts +50 -0
  630. package/dist/modules/workspace/file-readers/content-mode.d.ts.map +1 -0
  631. package/dist/modules/workspace/file-readers/content-mode.js +55 -0
  632. package/dist/modules/workspace/file-readers/content-mode.js.map +1 -0
  633. package/dist/modules/workspace/file-readers/doc-extract.service.d.ts +4 -1
  634. package/dist/modules/workspace/file-readers/doc-extract.service.d.ts.map +1 -1
  635. package/dist/modules/workspace/file-readers/doc-extract.service.js +4 -1
  636. package/dist/modules/workspace/file-readers/doc-extract.service.js.map +1 -1
  637. package/dist/modules/workspace/file-readers/doc-extract.types.d.ts +5 -2
  638. package/dist/modules/workspace/file-readers/doc-extract.types.d.ts.map +1 -1
  639. package/dist/modules/workspace/file-readers/doc-extract.types.js +7 -4
  640. package/dist/modules/workspace/file-readers/doc-extract.types.js.map +1 -1
  641. package/dist/modules/workspace/file-readers/document-reader.d.ts +2 -0
  642. package/dist/modules/workspace/file-readers/document-reader.d.ts.map +1 -1
  643. package/dist/modules/workspace/file-readers/document-reader.js +5 -0
  644. package/dist/modules/workspace/file-readers/document-reader.js.map +1 -1
  645. package/dist/modules/workspace/file-readers/extract-docx.d.ts +6 -0
  646. package/dist/modules/workspace/file-readers/extract-docx.d.ts.map +1 -1
  647. package/dist/modules/workspace/file-readers/extract-docx.js +6 -0
  648. package/dist/modules/workspace/file-readers/extract-docx.js.map +1 -1
  649. package/dist/modules/workspace/file-readers/file-reader.d.ts +24 -5
  650. package/dist/modules/workspace/file-readers/file-reader.d.ts.map +1 -1
  651. package/dist/modules/workspace/file-readers/file-reader.js +11 -6
  652. package/dist/modules/workspace/file-readers/file-reader.js.map +1 -1
  653. package/dist/modules/workspace/file-readers/file-reader.registry.d.ts.map +1 -1
  654. package/dist/modules/workspace/file-readers/file-reader.registry.js +6 -1
  655. package/dist/modules/workspace/file-readers/file-reader.registry.js.map +1 -1
  656. package/dist/modules/workspace/file-readers/image-reader.d.ts +7 -5
  657. package/dist/modules/workspace/file-readers/image-reader.d.ts.map +1 -1
  658. package/dist/modules/workspace/file-readers/image-reader.js +9 -5
  659. package/dist/modules/workspace/file-readers/image-reader.js.map +1 -1
  660. package/dist/modules/workspace/file-readers/ooxml-text.d.ts +3 -0
  661. package/dist/modules/workspace/file-readers/ooxml-text.d.ts.map +1 -1
  662. package/dist/modules/workspace/file-readers/ooxml-text.js +39 -2
  663. package/dist/modules/workspace/file-readers/ooxml-text.js.map +1 -1
  664. package/dist/modules/workspace/file-readers/text-reader.d.ts +29 -1
  665. package/dist/modules/workspace/file-readers/text-reader.d.ts.map +1 -1
  666. package/dist/modules/workspace/file-readers/text-reader.js +57 -3
  667. package/dist/modules/workspace/file-readers/text-reader.js.map +1 -1
  668. package/dist/modules/workspace/git-internals.middleware.d.ts +28 -0
  669. package/dist/modules/workspace/git-internals.middleware.d.ts.map +1 -0
  670. package/dist/modules/workspace/git-internals.middleware.js +93 -0
  671. package/dist/modules/workspace/git-internals.middleware.js.map +1 -0
  672. package/dist/modules/workspace/not-found.d.ts +80 -0
  673. package/dist/modules/workspace/not-found.d.ts.map +1 -0
  674. package/dist/modules/workspace/not-found.js +112 -0
  675. package/dist/modules/workspace/not-found.js.map +1 -0
  676. package/dist/modules/workspace/session-ontology.gate.d.ts +1 -1
  677. package/dist/modules/workspace/session-ontology.gate.d.ts.map +1 -1
  678. package/dist/modules/workspace/session-ontology.gate.js +1 -1
  679. package/dist/modules/workspace/session-ontology.gate.js.map +1 -1
  680. package/dist/modules/workspace/startup/beside-checkout.d.ts +42 -0
  681. package/dist/modules/workspace/startup/beside-checkout.d.ts.map +1 -0
  682. package/dist/modules/workspace/startup/beside-checkout.js +110 -0
  683. package/dist/modules/workspace/startup/beside-checkout.js.map +1 -0
  684. package/dist/modules/workspace/startup/kb-git.d.ts +7 -12
  685. package/dist/modules/workspace/startup/kb-git.d.ts.map +1 -1
  686. package/dist/modules/workspace/startup/kb-git.js +34 -35
  687. package/dist/modules/workspace/startup/kb-git.js.map +1 -1
  688. package/dist/modules/workspace/startup/kb-startup-runner.d.ts +95 -0
  689. package/dist/modules/workspace/startup/kb-startup-runner.d.ts.map +1 -1
  690. package/dist/modules/workspace/startup/kb-startup-runner.js +257 -55
  691. package/dist/modules/workspace/startup/kb-startup-runner.js.map +1 -1
  692. package/dist/modules/workspace/startup/steps/groups-to-plugins.step.d.ts +45 -0
  693. package/dist/modules/workspace/startup/steps/groups-to-plugins.step.d.ts.map +1 -1
  694. package/dist/modules/workspace/startup/steps/groups-to-plugins.step.js +417 -385
  695. package/dist/modules/workspace/startup/steps/groups-to-plugins.step.js.map +1 -1
  696. package/dist/modules/workspace/startup/steps/personal-spaces.step.d.ts +48 -0
  697. package/dist/modules/workspace/startup/steps/personal-spaces.step.d.ts.map +1 -0
  698. package/dist/modules/workspace/startup/steps/personal-spaces.step.js +183 -0
  699. package/dist/modules/workspace/startup/steps/personal-spaces.step.js.map +1 -0
  700. package/dist/modules/workspace/startup/steps/plugin-display-names.step.d.ts +51 -0
  701. package/dist/modules/workspace/startup/steps/plugin-display-names.step.d.ts.map +1 -0
  702. package/dist/modules/workspace/startup/steps/plugin-display-names.step.js +158 -0
  703. package/dist/modules/workspace/startup/steps/plugin-display-names.step.js.map +1 -0
  704. package/dist/modules/workspace/startup/steps/plugin-layout.d.ts +64 -0
  705. package/dist/modules/workspace/startup/steps/plugin-layout.d.ts.map +1 -0
  706. package/dist/modules/workspace/startup/steps/plugin-layout.js +97 -0
  707. package/dist/modules/workspace/startup/steps/plugin-layout.js.map +1 -0
  708. package/dist/modules/workspace/startup/steps/plugin-manifests.step.d.ts +41 -0
  709. package/dist/modules/workspace/startup/steps/plugin-manifests.step.d.ts.map +1 -0
  710. package/dist/modules/workspace/startup/steps/plugin-manifests.step.js +85 -0
  711. package/dist/modules/workspace/startup/steps/plugin-manifests.step.js.map +1 -0
  712. package/dist/modules/workspace/startup/steps/roles-yaml.step.d.ts +3 -1
  713. package/dist/modules/workspace/startup/steps/roles-yaml.step.d.ts.map +1 -1
  714. package/dist/modules/workspace/startup/steps/roles-yaml.step.js +4 -12
  715. package/dist/modules/workspace/startup/steps/roles-yaml.step.js.map +1 -1
  716. package/dist/modules/workspace/startup/steps/seed-tree.d.ts +6 -1
  717. package/dist/modules/workspace/startup/steps/seed-tree.d.ts.map +1 -1
  718. package/dist/modules/workspace/startup/steps/seed-tree.js +174 -54
  719. package/dist/modules/workspace/startup/steps/seed-tree.js.map +1 -1
  720. package/dist/modules/workspace/startup/steps/template-files.step.d.ts +96 -24
  721. package/dist/modules/workspace/startup/steps/template-files.step.d.ts.map +1 -1
  722. package/dist/modules/workspace/startup/steps/template-files.step.js +521 -126
  723. package/dist/modules/workspace/startup/steps/template-files.step.js.map +1 -1
  724. package/dist/modules/workspace/startup/steps/template-source.d.ts +94 -0
  725. package/dist/modules/workspace/startup/steps/template-source.d.ts.map +1 -0
  726. package/dist/modules/workspace/startup/steps/template-source.js +160 -0
  727. package/dist/modules/workspace/startup/steps/template-source.js.map +1 -0
  728. package/dist/modules/workspace/workspace.routes.d.ts +14 -2
  729. package/dist/modules/workspace/workspace.routes.d.ts.map +1 -1
  730. package/dist/modules/workspace/workspace.routes.js +657 -246
  731. package/dist/modules/workspace/workspace.routes.js.map +1 -1
  732. package/dist/modules/workspace/workspace.service.d.ts +323 -12
  733. package/dist/modules/workspace/workspace.service.d.ts.map +1 -1
  734. package/dist/modules/workspace/workspace.service.js +1006 -257
  735. package/dist/modules/workspace/workspace.service.js.map +1 -1
  736. package/dist/modules/workspace/workspace.tools.d.ts +42 -1
  737. package/dist/modules/workspace/workspace.tools.d.ts.map +1 -1
  738. package/dist/modules/workspace/workspace.tools.js +1590 -146
  739. package/dist/modules/workspace/workspace.tools.js.map +1 -1
  740. package/dist/modules/workspace/write-denial.d.ts +76 -0
  741. package/dist/modules/workspace/write-denial.d.ts.map +1 -0
  742. package/dist/modules/workspace/write-denial.js +138 -0
  743. package/dist/modules/workspace/write-denial.js.map +1 -0
  744. package/dist/shared/canonical-file-identity.d.ts +64 -0
  745. package/dist/shared/canonical-file-identity.d.ts.map +1 -0
  746. package/dist/shared/canonical-file-identity.js +92 -0
  747. package/dist/shared/canonical-file-identity.js.map +1 -0
  748. package/dist/shared/domain-errors.d.ts +128 -0
  749. package/dist/shared/domain-errors.d.ts.map +1 -1
  750. package/dist/shared/domain-errors.js +166 -0
  751. package/dist/shared/domain-errors.js.map +1 -1
  752. package/dist/shared/email-identity.d.ts +32 -0
  753. package/dist/shared/email-identity.d.ts.map +1 -0
  754. package/dist/shared/email-identity.js +37 -0
  755. package/dist/shared/email-identity.js.map +1 -0
  756. package/dist/shared/fs-errors.d.ts +8 -0
  757. package/dist/shared/fs-errors.d.ts.map +1 -0
  758. package/dist/shared/fs-errors.js +11 -0
  759. package/dist/shared/fs-errors.js.map +1 -0
  760. package/dist/shared/fs-walk.d.ts +20 -6
  761. package/dist/shared/fs-walk.d.ts.map +1 -1
  762. package/dist/shared/fs-walk.js +35 -27
  763. package/dist/shared/fs-walk.js.map +1 -1
  764. package/dist/shared/fs.contract.d.ts +206 -0
  765. package/dist/shared/fs.contract.d.ts.map +1 -0
  766. package/dist/shared/fs.contract.js +79 -0
  767. package/dist/shared/fs.contract.js.map +1 -0
  768. package/dist/shared/git-failure.d.ts +80 -0
  769. package/dist/shared/git-failure.d.ts.map +1 -0
  770. package/dist/shared/git-failure.js +139 -0
  771. package/dist/shared/git-failure.js.map +1 -0
  772. package/dist/shared/git-internals.d.ts +17 -0
  773. package/dist/shared/git-internals.d.ts.map +1 -0
  774. package/dist/shared/git-internals.js +123 -0
  775. package/dist/shared/git-internals.js.map +1 -0
  776. package/dist/shared/git.contract.d.ts +157 -0
  777. package/dist/shared/git.contract.d.ts.map +1 -0
  778. package/dist/shared/git.contract.js +103 -0
  779. package/dist/shared/git.contract.js.map +1 -0
  780. package/dist/shared/http-errors.d.ts +33 -0
  781. package/dist/shared/http-errors.d.ts.map +1 -0
  782. package/dist/shared/http-errors.js +35 -0
  783. package/dist/shared/http-errors.js.map +1 -0
  784. package/dist/shared/kb-walk.d.ts +52 -0
  785. package/dist/shared/kb-walk.d.ts.map +1 -0
  786. package/dist/shared/kb-walk.js +76 -0
  787. package/dist/shared/kb-walk.js.map +1 -0
  788. package/dist/shared/logger.contract.d.ts +63 -0
  789. package/dist/shared/logger.contract.d.ts.map +1 -0
  790. package/dist/shared/logger.contract.js +126 -0
  791. package/dist/shared/logger.contract.js.map +1 -0
  792. package/dist/shared/logging.d.ts +31 -0
  793. package/dist/shared/logging.d.ts.map +1 -0
  794. package/dist/shared/logging.js +92 -0
  795. package/dist/shared/logging.js.map +1 -0
  796. package/dist/shared/path-containment.d.ts +21 -0
  797. package/dist/shared/path-containment.d.ts.map +1 -0
  798. package/dist/shared/path-containment.js +34 -0
  799. package/dist/shared/path-containment.js.map +1 -0
  800. package/dist/shared/path-order.d.ts +18 -0
  801. package/dist/shared/path-order.d.ts.map +1 -0
  802. package/dist/shared/path-order.js +35 -0
  803. package/dist/shared/path-order.js.map +1 -0
  804. package/dist/shared/pending-proposals.d.ts +71 -0
  805. package/dist/shared/pending-proposals.d.ts.map +1 -0
  806. package/dist/shared/pending-proposals.js +98 -0
  807. package/dist/shared/pending-proposals.js.map +1 -0
  808. package/dist/shared/printable.d.ts +22 -0
  809. package/dist/shared/printable.d.ts.map +1 -0
  810. package/dist/shared/printable.js +65 -0
  811. package/dist/shared/printable.js.map +1 -0
  812. package/dist/shared/redact-secret.d.ts +30 -0
  813. package/dist/shared/redact-secret.d.ts.map +1 -0
  814. package/dist/shared/redact-secret.js +114 -0
  815. package/dist/shared/redact-secret.js.map +1 -0
  816. package/dist/shared/rename-no-replace.d.ts +119 -0
  817. package/dist/shared/rename-no-replace.d.ts.map +1 -0
  818. package/dist/shared/rename-no-replace.js +323 -0
  819. package/dist/shared/rename-no-replace.js.map +1 -0
  820. package/dist/shared/ssrf.d.ts.map +1 -1
  821. package/dist/shared/ssrf.js +49 -18
  822. package/dist/shared/ssrf.js.map +1 -1
  823. package/dist/shared/ttl-cache.d.ts +28 -1
  824. package/dist/shared/ttl-cache.d.ts.map +1 -1
  825. package/dist/shared/ttl-cache.js +33 -1
  826. package/dist/shared/ttl-cache.js.map +1 -1
  827. package/dist/version.d.ts +10 -6
  828. package/dist/version.d.ts.map +1 -1
  829. package/dist/version.js +10 -6
  830. package/dist/version.js.map +1 -1
  831. package/kb-template/.bevelignore +4 -3
  832. package/kb-template/AGENTS.md +364 -54
  833. package/kb-template/KnowledgeBase/How to get started.md +1 -1
  834. package/kb-template/Skills/.gitkeep +0 -0
  835. package/kb-template/access.md +5 -3
  836. package/kb-template/mcp-description.md +27 -0
  837. package/migrations/0005_github_facade.sql +25 -0
  838. package/migrations/0006_api_tokens_revoked_by.sql +1 -0
  839. package/migrations/0007_github_facade_registered.sql +1 -0
  840. package/migrations/0008_change_request_apply_failure.sql +4 -0
  841. package/migrations/0009_change_request_apply_failure_kind.sql +13 -0
  842. package/migrations/0010_plugin_join_requests.sql +15 -0
  843. package/migrations/0011_plugin_join_request_claim.sql +11 -0
  844. package/migrations/meta/0005_snapshot.json +1717 -0
  845. package/migrations/meta/0006_snapshot.json +1723 -0
  846. package/migrations/meta/0007_snapshot.json +1729 -0
  847. package/migrations/meta/0008_snapshot.json +1753 -0
  848. package/migrations/meta/0009_snapshot.json +1759 -0
  849. package/migrations/meta/0010_snapshot.json +1872 -0
  850. package/migrations/meta/0011_snapshot.json +1884 -0
  851. package/migrations/meta/_journal.json +49 -0
  852. package/package.json +8 -7
  853. package/src/__tests__/core-config.domain.test.ts +67 -2
  854. package/src/__tests__/kb-layout-config.test.ts +295 -0
  855. package/src/__tests__/open-change-gate.ts +15 -0
  856. package/src/core/__tests__/catalog-cache-invalidation.test.ts +226 -0
  857. package/src/core/__tests__/catalog-revision.test.ts +190 -0
  858. package/src/core/__tests__/lifecycle.test.ts +369 -0
  859. package/src/core/__tests__/public-config.test.ts +30 -0
  860. package/src/core/__tests__/readiness.test.ts +109 -0
  861. package/src/core/catalog-cache-invalidation.ts +111 -0
  862. package/src/core/catalog-revision.ts +110 -0
  863. package/src/core/create-core-server.ts +358 -63
  864. package/src/core/create-core-services.ts +449 -107
  865. package/src/core/lifecycle.ts +312 -0
  866. package/src/core/public-config.ts +86 -0
  867. package/src/core/readiness.ts +123 -0
  868. package/src/core-config.ts +70 -4
  869. package/src/index.ts +15 -0
  870. package/src/modules/access/__tests__/access-control.admin-root-floor.test.ts +362 -0
  871. package/src/modules/access/__tests__/access-control.atref-batch.test.ts +2 -1
  872. package/src/modules/access/__tests__/access-control.atref-cache.test.ts +46 -22
  873. package/src/modules/access/__tests__/access-control.deny-dependents.test.ts +215 -0
  874. package/src/modules/access/__tests__/access-control.download-read.test.ts +268 -0
  875. package/src/modules/access/__tests__/access-control.everyone-read.test.ts +145 -0
  876. package/src/modules/access/__tests__/access-control.group-read.test.ts +163 -0
  877. package/src/modules/access/__tests__/access-control.invalidate-race.test.ts +88 -0
  878. package/src/modules/access/__tests__/access-control.platform-restore.test.ts +152 -0
  879. package/src/modules/access/__tests__/access-control.prospective.test.ts +189 -0
  880. package/src/modules/access/__tests__/access-control.read-batch.test.ts +2 -1
  881. package/src/modules/access/__tests__/access-control.service.test.ts +142 -81
  882. package/src/modules/access/__tests__/access-groups.test.ts +47 -14
  883. package/src/modules/access/__tests__/access-md-format.test.ts +2 -1
  884. package/src/modules/access/__tests__/access-mutation.service.test.ts +52 -4
  885. package/src/modules/access/__tests__/access-own-read-grant.test.ts +2 -0
  886. package/src/modules/access/__tests__/access-personal-plugin.test.ts +153 -0
  887. package/src/modules/access/__tests__/access-plugin-principals.test.ts +385 -0
  888. package/src/modules/access/__tests__/access-restrict-here.test.ts +450 -0
  889. package/src/modules/access/__tests__/access.routes.batch.test.ts +139 -0
  890. package/src/modules/access/__tests__/access.routes.folder-governs.test.ts +394 -0
  891. package/src/modules/access/__tests__/access.routes.group-grant.test.ts +6 -2
  892. package/src/modules/access/__tests__/access.routes.overrides.test.ts +3 -2
  893. package/src/modules/access/__tests__/access.routes.proposal-branch.test.ts +234 -0
  894. package/src/modules/access/__tests__/access.routes.prospective.test.ts +216 -0
  895. package/src/modules/access/__tests__/access.routes.revoke.test.ts +7 -2
  896. package/src/modules/access/__tests__/access.routes.unknown-email.test.ts +360 -0
  897. package/src/modules/access/__tests__/change-read-gate.test.ts +272 -0
  898. package/src/modules/access/__tests__/creator-access.test.ts +81 -83
  899. package/src/modules/access/__tests__/grant-sources.test.ts +3 -2
  900. package/src/modules/access/__tests__/groups-admin.service.test.ts +14 -5
  901. package/src/modules/access/__tests__/roles-admin.security.test.ts +196 -0
  902. package/src/modules/access/__tests__/roles-admin.service.test.ts +127 -2
  903. package/src/modules/access/__tests__/roles-capabilities.test.ts +5 -4
  904. package/src/modules/access/__tests__/roles.routes.test.ts +5 -2
  905. package/src/modules/access/__tests__/user-access-removal.service.test.ts +402 -0
  906. package/src/modules/access/__tests__/users-db-double.ts +63 -0
  907. package/src/modules/access/access-control.interface.ts +269 -18
  908. package/src/modules/access/access-control.service.ts +943 -226
  909. package/src/modules/access/access-mutation.service.ts +182 -13
  910. package/src/modules/access/access-view.ts +316 -0
  911. package/src/modules/access/access.routes.ts +385 -105
  912. package/src/modules/access/admin-locked-commit.ts +12 -14
  913. package/src/modules/access/admin-route-helpers.ts +4 -4
  914. package/src/modules/access/change-read-gate.ts +148 -0
  915. package/src/modules/access/creator-access.ts +89 -124
  916. package/src/modules/access/directory-sync-bot.ts +4 -1
  917. package/src/modules/access/groups-admin.service.ts +3 -3
  918. package/src/modules/access/reference-scan.ts +5 -5
  919. package/src/modules/access/roles-admin.service.ts +94 -15
  920. package/src/modules/access/synced-groups-committer.ts +10 -15
  921. package/src/modules/access/synced-groups-writer.ts +3 -12
  922. package/src/modules/access/user-access-removal.service.ts +711 -0
  923. package/src/modules/access-model/__tests__/access-grammar.test.ts +18 -0
  924. package/src/modules/access-model/__tests__/access-splice.test.ts +63 -1
  925. package/src/modules/access-model/__tests__/frontmatter-fences.test.ts +71 -0
  926. package/src/modules/access-model/__tests__/plugin-token-canonical.test.ts +40 -0
  927. package/src/modules/access-model/__tests__/roles-yaml-guard.test.ts +131 -0
  928. package/src/modules/access-model/access-errors.ts +56 -15
  929. package/src/modules/access-model/access-grammar.ts +198 -49
  930. package/src/modules/access-model/access-splice.ts +18 -18
  931. package/src/modules/access-model/access-template.ts +44 -0
  932. package/src/modules/access-model/change-gate.ts +80 -0
  933. package/src/modules/access-model/creator.ts +18 -21
  934. package/src/modules/access-model/frontmatter-lines.ts +98 -0
  935. package/src/modules/access-model/group-files.ts +4 -0
  936. package/src/modules/access-model/plugin-principals.ts +130 -0
  937. package/src/modules/access-model/roles-yaml-guard.ts +182 -4
  938. package/src/modules/admin/admin-access.service.ts +6 -2
  939. package/src/modules/agent-instructions/__tests__/agent-instructions.route.test.ts +100 -0
  940. package/src/modules/agent-instructions/__tests__/compose.test.ts +256 -0
  941. package/src/modules/agent-instructions/__tests__/read-preamble.test.ts +135 -0
  942. package/src/modules/agent-instructions/agent-instructions.routes.ts +41 -0
  943. package/src/modules/agent-instructions/compose.ts +271 -0
  944. package/src/modules/agent-instructions/index.ts +14 -0
  945. package/src/modules/agent-instructions/read-preamble.ts +88 -0
  946. package/src/modules/auth/__tests__/account-erasure.approval-gate.test.ts +209 -0
  947. package/src/modules/auth/__tests__/account.routes.test.ts +178 -2
  948. package/src/modules/auth/__tests__/auth.middleware.test.ts +78 -0
  949. package/src/modules/auth/__tests__/auth.service.test.ts +338 -5
  950. package/src/modules/auth/__tests__/oidc-auth-provider.test.ts +352 -12
  951. package/src/modules/auth/__tests__/onboarding-done.route.test.ts +3 -1
  952. package/src/modules/auth/account-erasure.service.ts +117 -10
  953. package/src/modules/auth/account.routes.ts +115 -14
  954. package/src/modules/auth/auth.routes.ts +20 -7
  955. package/src/modules/auth/auth.service.ts +187 -28
  956. package/src/modules/auth/oidc-auth-provider.ts +120 -22
  957. package/src/modules/code-mode/__tests__/code-mode.tool.test.ts +109 -1
  958. package/src/modules/code-mode/code-mode.tool.ts +13 -2
  959. package/src/modules/connection-probe/__tests__/connection-probe.service.test.ts +78 -0
  960. package/src/modules/connection-probe/connection-probe.service.ts +59 -7
  961. package/src/modules/database/__tests__/advisory-lease.test.ts +152 -0
  962. package/src/modules/database/__tests__/advisory-lock.test.ts +147 -0
  963. package/src/modules/database/advisory-lock.ts +346 -0
  964. package/src/modules/database/connection.ts +9 -1
  965. package/src/modules/database/core-schema.ts +177 -0
  966. package/src/modules/database/migrate.ts +27 -6
  967. package/src/modules/declared-variables/declared-variables.routes.ts +6 -3
  968. package/src/modules/diff/__tests__/diff.service.seed-atomicity.test.ts +3 -0
  969. package/src/modules/diff/__tests__/diff.service.test.ts +3 -0
  970. package/src/modules/diff/diff.routes.ts +2 -4
  971. package/src/modules/diff/diff.service.ts +75 -79
  972. package/src/modules/kb-fs/__tests__/locking-filesystem.test.ts +472 -9
  973. package/src/modules/kb-fs/__tests__/node-fs.test.ts +409 -0
  974. package/src/modules/kb-fs/__tests__/repo-path.test.ts +210 -1
  975. package/src/modules/{workspace → kb-fs}/bevel-ignore.ts +22 -15
  976. package/src/modules/kb-fs/file-change-notifier.ts +4 -1
  977. package/src/modules/kb-fs/git-guarded-filesystem.ts +123 -0
  978. package/src/modules/kb-fs/locking-filesystem.ts +399 -109
  979. package/src/modules/kb-fs/node-fs.ts +165 -0
  980. package/src/modules/kb-fs/read-only-filesystem.ts +18 -9
  981. package/src/modules/kb-fs/repo-path.ts +213 -3
  982. package/src/modules/kb-sync/__tests__/kb-sync.routes.test.ts +297 -0
  983. package/src/modules/kb-sync/__tests__/kb-sync.service.test.ts +295 -0
  984. package/src/modules/kb-sync/__tests__/sync-auth.test.ts +118 -0
  985. package/src/modules/kb-sync/__tests__/sync-payload.test.ts +148 -0
  986. package/src/modules/kb-sync/index.ts +19 -0
  987. package/src/modules/kb-sync/kb-sync.interface.ts +79 -0
  988. package/src/modules/kb-sync/kb-sync.routes.ts +200 -0
  989. package/src/modules/kb-sync/kb-sync.service.ts +204 -0
  990. package/src/modules/kb-sync/sync-auth.ts +118 -0
  991. package/src/modules/kb-sync/sync-payload.ts +143 -0
  992. package/src/modules/marketplace/__tests__/github-facade.test.ts +575 -0
  993. package/src/modules/marketplace/__tests__/marketplace-git.test.ts +180 -0
  994. package/src/modules/marketplace/__tests__/marketplace-repo.head.test.ts +47 -0
  995. package/src/modules/marketplace/git-http.routes.ts +196 -0
  996. package/src/modules/marketplace/github-facade/github-facade-admin.routes.ts +110 -0
  997. package/src/modules/marketplace/github-facade/github-facade-codes.store.ts +145 -0
  998. package/src/modules/marketplace/github-facade/github-facade-credentials.service.ts +263 -0
  999. package/src/modules/marketplace/github-facade/github-facade.routes.ts +294 -0
  1000. package/src/modules/marketplace/github-facade/github-facade.service.ts +282 -0
  1001. package/src/modules/marketplace/github-facade/index.ts +26 -0
  1002. package/src/modules/marketplace/index.ts +3 -0
  1003. package/src/modules/marketplace/marketplace-repo.service.ts +352 -0
  1004. package/src/modules/mcp/__tests__/downstream-pool.test.ts +278 -0
  1005. package/src/modules/mcp/__tests__/downstream-token-refresh.test.ts +178 -0
  1006. package/src/modules/mcp/__tests__/fake-downstream-mcp-server.ts +156 -0
  1007. package/src/modules/mcp/__tests__/first-call-probe.cli-args.test.ts +178 -0
  1008. package/src/modules/mcp/__tests__/first-call-probe.test.ts +414 -0
  1009. package/src/modules/mcp/__tests__/manual-failure-memo.test.ts +98 -6
  1010. package/src/modules/mcp/__tests__/mcp-auth.middleware.test.ts +8 -5
  1011. package/src/modules/mcp/__tests__/mcp-routes-harness.ts +1 -3
  1012. package/src/modules/mcp/__tests__/mcp.e2e.test.ts +1055 -59
  1013. package/src/modules/mcp/__tests__/mcp.routes.stateless.test.ts +182 -0
  1014. package/src/modules/mcp/__tests__/mcp.service.test.ts +274 -6
  1015. package/src/modules/mcp/__tests__/surface-log-throttle.test.ts +169 -0
  1016. package/src/modules/mcp/downstream-pool.ts +256 -0
  1017. package/src/modules/mcp/downstream-token-refresh.ts +170 -0
  1018. package/src/modules/mcp/first-call-probe.cli-args.ts +159 -0
  1019. package/src/modules/mcp/first-call-probe.cli.ts +60 -0
  1020. package/src/modules/mcp/first-call-probe.ts +418 -0
  1021. package/src/modules/mcp/manual-failure-memo.ts +81 -23
  1022. package/src/modules/mcp/mcp-auth.middleware.ts +25 -16
  1023. package/src/modules/mcp/mcp.routes.ts +376 -482
  1024. package/src/modules/mcp/mcp.service.ts +846 -176
  1025. package/src/modules/mcp/oauth/bevel-oauth-provider.ts +8 -2
  1026. package/src/modules/mcp/oauth/oauth-consent.routes.ts +28 -6
  1027. package/src/modules/mcp/oauth/oauth-state.ts +15 -3
  1028. package/src/modules/mcp/surface-log-throttle.ts +120 -0
  1029. package/src/modules/plugins/__tests__/bundle-dialect.e2e.test.ts +166 -0
  1030. package/src/modules/plugins/__tests__/bundle-dialect.test.ts +165 -0
  1031. package/src/modules/plugins/__tests__/compile-marketplace.test.ts +423 -0
  1032. package/src/modules/plugins/__tests__/fake-join-request-store.ts +189 -0
  1033. package/src/modules/plugins/__tests__/join-request-jobs.service.test.ts +608 -0
  1034. package/src/modules/plugins/__tests__/plugin-creation-display-name.test.ts +241 -0
  1035. package/src/modules/plugins/__tests__/plugin-index.service.test.ts +201 -16
  1036. package/src/modules/plugins/__tests__/plugin-links.service.test.ts +284 -0
  1037. package/src/modules/plugins/__tests__/plugin-names.test.ts +77 -0
  1038. package/src/modules/plugins/__tests__/plugin-provision.service.test.ts +330 -20
  1039. package/src/modules/plugins/__tests__/plugin-rename.service.test.ts +456 -0
  1040. package/src/modules/plugins/__tests__/plugins.routes.test.ts +441 -43
  1041. package/src/modules/plugins/__tests__/plugins.tools.test.ts +131 -0
  1042. package/src/modules/plugins/__tests__/teams.routes.test.ts +248 -0
  1043. package/src/modules/plugins/compile/compile-marketplace.ts +500 -0
  1044. package/src/modules/plugins/compile/marketplace-compiler.service.ts +151 -0
  1045. package/src/modules/plugins/discovery/__tests__/kb-plugin-source.test.ts +365 -0
  1046. package/src/modules/plugins/discovery/bundle-dialect/bundle.source.ts +161 -0
  1047. package/src/modules/plugins/discovery/bundle-dialect/registry.ts +250 -0
  1048. package/src/modules/plugins/discovery/kb-plugin-source.ts +117 -0
  1049. package/src/modules/plugins/discovery/native.source.ts +140 -0
  1050. package/src/modules/plugins/discovery/plugin-source.ts +113 -0
  1051. package/src/modules/plugins/index.ts +25 -2
  1052. package/src/modules/plugins/join-request-jobs.service.ts +675 -0
  1053. package/src/modules/plugins/join-request-records.store.ts +353 -0
  1054. package/src/modules/plugins/join-requests.service.ts +7 -4
  1055. package/src/modules/plugins/plugin-links.service.ts +350 -0
  1056. package/src/modules/plugins/plugin-links.ts +187 -0
  1057. package/src/modules/plugins/plugin-provision.service.ts +356 -78
  1058. package/src/modules/plugins/plugin-rename.service.ts +310 -0
  1059. package/src/modules/plugins/plugins.contract.ts +79 -4
  1060. package/src/modules/plugins/plugins.routes.ts +320 -123
  1061. package/src/modules/plugins/plugins.service.ts +184 -57
  1062. package/src/modules/plugins/plugins.tools.ts +88 -0
  1063. package/src/modules/plugins/teams.routes.ts +129 -0
  1064. package/src/modules/secrets-vault/__tests__/connect-pending.route.test.ts +359 -15
  1065. package/src/modules/secrets-vault/__tests__/db-secrets-vault.oauth.test.ts +248 -0
  1066. package/src/modules/secrets-vault/__tests__/namespace-secrets.test.ts +202 -0
  1067. package/src/modules/secrets-vault/db-secrets-vault.service.ts +217 -35
  1068. package/src/modules/secrets-vault/index.ts +1 -0
  1069. package/src/modules/secrets-vault/mcp-oauth-discovery.service.ts +6 -3
  1070. package/src/modules/secrets-vault/secrets-variable-loader.ts +4 -4
  1071. package/src/modules/secrets-vault/secrets-vault.contract.ts +93 -0
  1072. package/src/modules/secrets-vault/secrets-vault.routes.ts +99 -38
  1073. package/src/modules/settings/__tests__/connection-check.test.ts +233 -0
  1074. package/src/modules/settings/__tests__/deployment-settings.service.test.ts +240 -1
  1075. package/src/modules/settings/__tests__/git-root-folders.test.ts +247 -0
  1076. package/src/modules/settings/__tests__/oidc-check.test.ts +185 -0
  1077. package/src/modules/settings/__tests__/setup.routes.boot-failure.test.ts +119 -0
  1078. package/src/modules/settings/__tests__/setup.routes.layout-phase.test.ts +170 -0
  1079. package/src/modules/settings/__tests__/setup.routes.oidc.test.ts +555 -0
  1080. package/src/modules/settings/__tests__/setup.routes.sync-status.test.ts +77 -0
  1081. package/src/modules/settings/__tests__/setup.routes.test.ts +667 -16
  1082. package/src/modules/settings/connection-check.ts +351 -0
  1083. package/src/modules/settings/deployment-settings.service.ts +478 -39
  1084. package/src/modules/settings/git-root-folders.ts +171 -0
  1085. package/src/modules/settings/oidc-check.ts +229 -0
  1086. package/src/modules/settings/setup.routes.ts +480 -110
  1087. package/src/modules/skills/__tests__/allowed-tools-check.test.ts +224 -0
  1088. package/src/modules/skills/__tests__/allowed-tools-warn.tools.test.ts +210 -0
  1089. package/src/modules/skills/__tests__/pending-skills.service.test.ts +3 -3
  1090. package/src/modules/skills/__tests__/skill-access-requests.routes.test.ts +128 -0
  1091. package/src/modules/skills/__tests__/skills.routes.warnings.test.ts +91 -0
  1092. package/src/modules/skills/__tests__/skills.service.test.ts +308 -2
  1093. package/src/modules/skills/allowed-tools-check.ts +352 -0
  1094. package/src/modules/skills/index.ts +5 -0
  1095. package/src/modules/skills/pending-skills.service.ts +26 -85
  1096. package/src/modules/skills/skill-access-requests.routes.ts +203 -0
  1097. package/src/modules/skills/skills.contract.ts +67 -3
  1098. package/src/modules/skills/skills.routes.ts +75 -4
  1099. package/src/modules/skills/skills.service.ts +258 -66
  1100. package/src/modules/skills/skills.tools.ts +43 -5
  1101. package/src/modules/tool-auth/__tests__/connection-key-rejection.test.ts +192 -0
  1102. package/src/modules/tool-auth/__tests__/connection-keys-admin.routes.test.ts +116 -0
  1103. package/src/modules/tool-auth/__tests__/external-api-key.service.test.ts +124 -0
  1104. package/src/modules/tool-auth/connection-key-rejection.ts +25 -0
  1105. package/src/modules/tool-auth/connection-keys-admin.routes.ts +76 -0
  1106. package/src/modules/tool-auth/external-api-key.interface.ts +73 -1
  1107. package/src/modules/tool-auth/external-api-key.service.ts +95 -29
  1108. package/src/modules/tool-auth/internal-token.service.ts +5 -2
  1109. package/src/modules/tool-auth/key-or-session.middleware.ts +45 -0
  1110. package/src/modules/tool-auth/tool-auth.middleware.ts +16 -7
  1111. package/src/modules/tool-helpers/__tests__/agent-roles-write.test.ts +228 -0
  1112. package/src/modules/tool-helpers/__tests__/phase4-tools.test.ts +33 -4
  1113. package/src/modules/tool-helpers/tool-context.ts +46 -7
  1114. package/src/modules/tool-helpers/tool-handler.ts +23 -5
  1115. package/src/modules/tool-helpers/tool.contract.ts +5 -0
  1116. package/src/modules/tool-manuals/__tests__/mcp-json-discovery.test.ts +24 -1
  1117. package/src/modules/tool-manuals/__tests__/mcp-server-edit.service.test.ts +2 -1
  1118. package/src/modules/tool-manuals/__tests__/pending-tools.service.test.ts +411 -0
  1119. package/src/modules/tool-manuals/__tests__/tool-delete.route.test.ts +133 -0
  1120. package/src/modules/tool-manuals/__tests__/tool-delete.service.test.ts +469 -0
  1121. package/src/modules/tool-manuals/__tests__/tool-manuals.archive.route.test.ts +21 -2
  1122. package/src/modules/tool-manuals/__tests__/tool-manuals.cli.test.ts +11 -4
  1123. package/src/modules/tool-manuals/__tests__/tool-manuals.detail.route.test.ts +6 -1
  1124. package/src/modules/tool-manuals/__tests__/tool-manuals.invalid.routes.test.ts +254 -0
  1125. package/src/modules/tool-manuals/__tests__/tool-manuals.invalid.test.ts +577 -0
  1126. package/src/modules/tool-manuals/__tests__/tool-manuals.mcp-oauth.test.ts +6 -1
  1127. package/src/modules/tool-manuals/__tests__/tool-manuals.pending.route.test.ts +102 -0
  1128. package/src/modules/tool-manuals/__tests__/tool-manuals.restart.test.ts +285 -0
  1129. package/src/modules/tool-manuals/__tests__/tool-manuals.service.test.ts +178 -2
  1130. package/src/modules/tool-manuals/__tests__/tool-manuals.tools.test.ts +114 -2
  1131. package/src/modules/tool-manuals/index.ts +3 -0
  1132. package/src/modules/tool-manuals/mcp-json-discovery.ts +164 -95
  1133. package/src/modules/tool-manuals/mcp-server-edit.service.ts +11 -14
  1134. package/src/modules/tool-manuals/pending-tools.service.ts +179 -0
  1135. package/src/modules/tool-manuals/tool-delete.service.ts +401 -0
  1136. package/src/modules/tool-manuals/tool-manuals.contract.ts +165 -0
  1137. package/src/modules/tool-manuals/tool-manuals.routes.ts +138 -54
  1138. package/src/modules/tool-manuals/tool-manuals.service.ts +440 -104
  1139. package/src/modules/tool-manuals/tool-manuals.tools.ts +81 -6
  1140. package/src/modules/workflow/__tests__/apply-failure.test.ts +1038 -0
  1141. package/src/modules/workflow/__tests__/event-bus.test.ts +61 -9
  1142. package/src/modules/workflow/__tests__/fake-file-lock-db.ts +199 -0
  1143. package/src/modules/workflow/__tests__/file-lock.service.path-identity.test.ts +237 -0
  1144. package/src/modules/workflow/__tests__/format-affected-owners.test.ts +18 -0
  1145. package/src/modules/workflow/__tests__/merge-announces-tree.test.ts +220 -0
  1146. package/src/modules/workflow/__tests__/pending-commits.worker.concurrency.test.ts +187 -0
  1147. package/src/modules/workflow/__tests__/preserve-roles-yaml.test.ts +3 -1
  1148. package/src/modules/workflow/__tests__/sanitize-error.test.ts +10 -0
  1149. package/src/modules/workflow/__tests__/workflow.routes.folder-proposals.test.ts +128 -0
  1150. package/src/modules/workflow/__tests__/workflow.routes.fork-point-file.test.ts +149 -0
  1151. package/src/modules/workflow/__tests__/workflow.routes.lock-path.test.ts +305 -0
  1152. package/src/modules/workflow/__tests__/workflow.service.branch-in-use.test.ts +2 -0
  1153. package/src/modules/workflow/__tests__/workflow.service.commitFileWhileLocked.test.ts +3 -0
  1154. package/src/modules/workflow/__tests__/workflow.service.deleted-branch-sweep.test.ts +2 -0
  1155. package/src/modules/workflow/__tests__/workflow.service.facade.test.ts +296 -27
  1156. package/src/modules/workflow/__tests__/workflow.service.folder-proposals.test.ts +564 -0
  1157. package/src/modules/workflow/__tests__/workflow.service.platform-restore.test.ts +135 -0
  1158. package/src/modules/workflow/__tests__/workflow.service.read-gate.test.ts +172 -0
  1159. package/src/modules/workflow/__tests__/workflow.service.releaseLock.test.ts +133 -2
  1160. package/src/modules/workflow/__tests__/workflow.service.sync.test.ts +442 -0
  1161. package/src/modules/workflow/__tests__/workflow.service.update-from-target.test.ts +281 -0
  1162. package/src/modules/workflow/agent-tools/__tests__/workflow.tools.test.ts +158 -7
  1163. package/src/modules/workflow/agent-tools/workflow.tools.ts +93 -64
  1164. package/src/modules/workflow/event-bus.ts +26 -28
  1165. package/src/modules/workflow/events.routes.ts +56 -21
  1166. package/src/modules/workflow/file-lock.service.ts +27 -4
  1167. package/src/modules/workflow/git/__tests__/change-request-link.test.ts +43 -0
  1168. package/src/modules/workflow/git/__tests__/git.service.accessGating.test.ts +1 -0
  1169. package/src/modules/workflow/git/__tests__/git.service.changedFilesForPr.test.ts +238 -1
  1170. package/src/modules/workflow/git/__tests__/git.service.forkPoint.test.ts +245 -0
  1171. package/src/modules/workflow/git/__tests__/git.service.list-branches.test.ts +35 -0
  1172. package/src/modules/workflow/git/__tests__/git.service.mergeChangeRequest.test.ts +149 -0
  1173. package/src/modules/workflow/git/__tests__/git.service.postMergePull.test.ts +3 -3
  1174. package/src/modules/workflow/git/__tests__/git.service.pull.test.ts +47 -4
  1175. package/src/modules/workflow/git/__tests__/git.service.revertPathsAndPush.test.ts +177 -0
  1176. package/src/modules/workflow/git/__tests__/git.service.syncFromRemote.test.ts +253 -0
  1177. package/src/modules/workflow/git/__tests__/node-git-runner.test.ts +114 -0
  1178. package/src/modules/workflow/git/__tests__/pull-request.service.getPrDetail.test.ts +69 -2
  1179. package/src/modules/workflow/git/__tests__/pull-request.service.list-fetch.test.ts +121 -0
  1180. package/src/modules/workflow/git/__tests__/pull-request.service.placeholder.test.ts +79 -0
  1181. package/src/modules/workflow/git/__tests__/pull-request.service.test.ts +1 -1
  1182. package/src/modules/workflow/git/__tests__/pull-request.service.viewer-can-cancel.test.ts +1 -1
  1183. package/src/modules/workflow/git/__tests__/pull-request.service.viewer-can-update.test.ts +111 -0
  1184. package/src/modules/workflow/git/change-request-link.ts +35 -0
  1185. package/src/modules/workflow/git/git-version.ts +5 -2
  1186. package/src/modules/workflow/git/git.service.ts +922 -199
  1187. package/src/modules/workflow/git/node-git-runner.ts +315 -0
  1188. package/src/modules/workflow/git/pull-request.service.ts +188 -27
  1189. package/src/modules/workflow/pending-commits.service.ts +26 -7
  1190. package/src/modules/workflow/pending-commits.worker.ts +114 -69
  1191. package/src/modules/workflow/recovery-bot.ts +4 -1
  1192. package/src/modules/workflow/review-workflow/__tests__/approval-states.test.ts +180 -1
  1193. package/src/modules/workflow/review-workflow/__tests__/cancel-pr.test.ts +1 -1
  1194. package/src/modules/workflow/review-workflow/__tests__/carry-approvals-forward.test.ts +520 -0
  1195. package/src/modules/workflow/review-workflow/__tests__/erase-approver.test.ts +84 -0
  1196. package/src/modules/workflow/review-workflow/__tests__/merge-gate.test.ts +89 -77
  1197. package/src/modules/workflow/review-workflow/approval-lock.ts +122 -0
  1198. package/src/modules/workflow/review-workflow/review-workflow.interface.ts +59 -7
  1199. package/src/modules/workflow/review-workflow/review-workflow.service.ts +309 -105
  1200. package/src/modules/workflow/sanitize-error.ts +9 -4
  1201. package/src/modules/workflow/workflow.routes.ts +273 -29
  1202. package/src/modules/workflow/workflow.service.ts +1341 -174
  1203. package/src/modules/workspace/__tests__/empty-dirs.test.ts +44 -0
  1204. package/src/modules/workspace/__tests__/file-stat-access.test.ts +301 -0
  1205. package/src/modules/workspace/__tests__/git-internals.security.test.ts +792 -0
  1206. package/src/modules/workspace/__tests__/workspace-paths-inside-the-repo.test.ts +464 -0
  1207. package/src/modules/workspace/__tests__/workspace.routes.branch-errors.test.ts +139 -0
  1208. package/src/modules/workspace/__tests__/workspace.routes.create-grant.test.ts +189 -37
  1209. package/src/modules/workspace/__tests__/workspace.routes.delete.test.ts +51 -11
  1210. package/src/modules/workspace/__tests__/workspace.routes.download-implies-read.test.ts +161 -0
  1211. package/src/modules/workspace/__tests__/workspace.routes.download.test.ts +109 -10
  1212. package/src/modules/workspace/__tests__/workspace.routes.file-writes.test.ts +624 -0
  1213. package/src/modules/workspace/__tests__/workspace.routes.folders.test.ts +409 -0
  1214. package/src/modules/workspace/__tests__/workspace.routes.move-platform-files.test.ts +327 -0
  1215. package/src/modules/workspace/__tests__/workspace.routes.read-gate.test.ts +31 -13
  1216. package/src/modules/workspace/__tests__/workspace.service.ensure-remotes-fetched.test.ts +109 -0
  1217. package/src/modules/workspace/__tests__/workspace.service.list-cloned.test.ts +94 -0
  1218. package/src/modules/workspace/__tests__/workspace.service.missing-branch.test.ts +28 -0
  1219. package/src/modules/workspace/__tests__/workspace.service.move-never-overwrites.test.ts +218 -0
  1220. package/src/modules/workspace/__tests__/workspace.service.read-filter.test.ts +98 -13
  1221. package/src/modules/workspace/__tests__/workspace.service.test.ts +811 -42
  1222. package/src/modules/workspace/__tests__/workspace.service.unknown-branch.test.ts +197 -0
  1223. package/src/modules/workspace/__tests__/workspace.tools.agents-file.test.ts +128 -0
  1224. package/src/modules/workspace/__tests__/workspace.tools.branch-errors.test.ts +212 -0
  1225. package/src/modules/workspace/__tests__/workspace.tools.test.ts +2632 -110
  1226. package/src/modules/workspace/__tests__/write-denial.test.ts +75 -0
  1227. package/src/modules/workspace/empty-dirs.ts +79 -0
  1228. package/src/modules/workspace/file-readers/README.md +32 -0
  1229. package/src/modules/workspace/file-readers/__tests__/doc-extract.test.ts +59 -2
  1230. package/src/modules/workspace/file-readers/__tests__/file-reader.registry.test.ts +133 -4
  1231. package/src/modules/workspace/file-readers/__tests__/fixtures/soft-break.docx +0 -0
  1232. package/src/modules/workspace/file-readers/__tests__/fixtures/tab.docx +0 -0
  1233. package/src/modules/workspace/file-readers/content-mode.ts +87 -0
  1234. package/src/modules/workspace/file-readers/doc-extract.service.ts +4 -1
  1235. package/src/modules/workspace/file-readers/doc-extract.types.ts +8 -4
  1236. package/src/modules/workspace/file-readers/document-reader.ts +6 -0
  1237. package/src/modules/workspace/file-readers/extract-docx.ts +6 -0
  1238. package/src/modules/workspace/file-readers/file-reader.registry.ts +9 -1
  1239. package/src/modules/workspace/file-readers/file-reader.ts +27 -6
  1240. package/src/modules/workspace/file-readers/image-reader.ts +10 -5
  1241. package/src/modules/workspace/file-readers/ooxml-text.ts +38 -2
  1242. package/src/modules/workspace/file-readers/text-reader.ts +61 -4
  1243. package/src/modules/workspace/git-internals.middleware.ts +86 -0
  1244. package/src/modules/workspace/not-found.ts +119 -0
  1245. package/src/modules/workspace/session-ontology.gate.ts +1 -1
  1246. package/src/modules/workspace/startup/__tests__/beside-checkout.test.ts +181 -0
  1247. package/src/modules/workspace/startup/__tests__/kb-startup-runner.degraded.test.ts +361 -0
  1248. package/src/modules/workspace/startup/__tests__/kb-startup-runner.test.ts +53 -16
  1249. package/src/modules/workspace/startup/beside-checkout.ts +112 -0
  1250. package/src/modules/workspace/startup/kb-git.ts +43 -36
  1251. package/src/modules/workspace/startup/kb-startup-runner.ts +303 -63
  1252. package/src/modules/workspace/startup/steps/__tests__/steps.test.ts +1405 -48
  1253. package/src/modules/workspace/startup/steps/groups-to-plugins.step.ts +460 -417
  1254. package/src/modules/workspace/startup/steps/personal-spaces.step.ts +191 -0
  1255. package/src/modules/workspace/startup/steps/plugin-display-names.step.ts +169 -0
  1256. package/src/modules/workspace/startup/steps/plugin-layout.ts +110 -0
  1257. package/src/modules/workspace/startup/steps/plugin-manifests.step.ts +90 -0
  1258. package/src/modules/workspace/startup/steps/roles-yaml.step.ts +6 -12
  1259. package/src/modules/workspace/startup/steps/seed-tree.ts +177 -54
  1260. package/src/modules/workspace/startup/steps/template-files.step.ts +581 -126
  1261. package/src/modules/workspace/startup/steps/template-source.ts +168 -0
  1262. package/src/modules/workspace/workspace.routes.ts +691 -269
  1263. package/src/modules/workspace/workspace.service.ts +1071 -292
  1264. package/src/modules/workspace/workspace.tools.ts +1787 -180
  1265. package/src/modules/workspace/write-denial.ts +189 -0
  1266. package/src/shared/__tests__/canonical-file-identity.test.ts +154 -0
  1267. package/src/shared/__tests__/email-identity.test.ts +35 -0
  1268. package/src/shared/__tests__/filename.test.ts +0 -0
  1269. package/src/shared/__tests__/frontmatter-id.test.ts +5 -0
  1270. package/src/shared/__tests__/git-failure.test.ts +198 -0
  1271. package/src/shared/__tests__/git-internals.test.ts +115 -0
  1272. package/src/shared/__tests__/http-errors.test.ts +42 -0
  1273. package/src/shared/__tests__/logging.test.ts +185 -0
  1274. package/src/shared/__tests__/path-containment.test.ts +60 -0
  1275. package/src/shared/__tests__/path-order.test.ts +24 -0
  1276. package/src/shared/__tests__/pending-proposals.readAt.test.ts +102 -0
  1277. package/src/shared/__tests__/printable.test.ts +64 -0
  1278. package/src/shared/__tests__/redact-secret.test.ts +122 -0
  1279. package/src/shared/__tests__/rename-no-replace.test.ts +466 -0
  1280. package/src/shared/__tests__/ssrf.test.ts +20 -0
  1281. package/src/shared/__tests__/ttl-cache.test.ts +55 -0
  1282. package/src/shared/canonical-file-identity.ts +92 -0
  1283. package/src/shared/domain-errors.ts +178 -0
  1284. package/src/shared/email-identity.ts +38 -0
  1285. package/src/shared/fs.contract.ts +225 -0
  1286. package/src/shared/git-failure.ts +196 -0
  1287. package/src/shared/git-internals.ts +124 -0
  1288. package/src/shared/git.contract.ts +170 -0
  1289. package/src/shared/http-errors.ts +36 -0
  1290. package/src/shared/logger.contract.ts +141 -0
  1291. package/src/shared/logging.ts +96 -0
  1292. package/src/shared/path-containment.ts +34 -0
  1293. package/src/shared/path-order.ts +33 -0
  1294. package/src/shared/pending-proposals.ts +161 -0
  1295. package/src/shared/printable.ts +68 -0
  1296. package/src/shared/redact-secret.ts +118 -0
  1297. package/src/shared/rename-no-replace.ts +386 -0
  1298. package/src/shared/ssrf.ts +44 -16
  1299. package/src/shared/ttl-cache.ts +33 -1
  1300. package/src/version.ts +10 -6
  1301. package/src/modules/diff/diff-paths.ts +0 -9
  1302. package/src/modules/mcp/__tests__/mcp-session-store.test.ts +0 -151
  1303. package/src/modules/mcp/__tests__/mcp.routes.session.test.ts +0 -226
  1304. package/src/modules/mcp/mcp-session-store.ts +0 -162
  1305. package/src/shared/__tests__/fs-walk.test.ts +0 -42
  1306. package/src/shared/fs-walk.ts +0 -30
  1307. package/src/shared/hash-email.ts +0 -14
@@ -17,23 +17,35 @@
17
17
  * those backings land.
18
18
  */
19
19
  import path from 'node:path';
20
+ import { logger } from '../../shared/logging.js';
21
+ // One logger per tag this file has always written under, so a line's prefix
22
+ // is unchanged for a reader and its `module` field is exact for a filter.
23
+ const log = logger('workflow');
24
+ const syncLog = logger('sync');
25
+ const lockLog = logger('lock');
26
+ const crLog = logger('cr');
27
+ const mergeLog = logger('merge');
20
28
  import { promises as fs } from 'node:fs';
21
29
  import { execFile } from 'node:child_process';
22
30
  import { promisify } from 'node:util';
23
- import { isProtectedBranch, DEFAULT_BRANCH } from '@bevel-software/platform-shared';
24
- import { and, eq, or } from 'drizzle-orm';
31
+ import { isProtectedBranch, DEFAULT_BRANCH, isFolderPlaceholder, folderPlaceholderPath, isPlatformRestoreShape, } from '@bevel-software/platform-shared';
32
+ import { and, eq, inArray, isNotNull, isNull, lt, or } from 'drizzle-orm';
25
33
  import { changeRequests } from '../database/schema.js';
26
34
  import { workspaceIdForBranch, branchForWorkspaceId } from '../../shared/workspace-id.js';
35
+ import { canonicalFileIdentity } from '../../shared/canonical-file-identity.js';
27
36
  import { sanitizeError } from './sanitize-error.js';
28
37
  import { WorkflowHooks } from './workflow-hooks.js';
29
38
  import { WorkspaceMutex } from '../kb-fs/mutex.js';
30
- import { hashEmail } from '../../shared/hash-email.js';
31
- import { ChangeRequestConflictsError, DuplicateChangeRequestError, RolesYamlPreservationError, PullRebaseConflictError, PushNeedsAgentResolutionError, WorkflowDomainError, WorkflowValidationError, } from '../../shared/domain-errors.js';
39
+ import { canonicalEmail, hashEmail } from '../../shared/email-identity.js';
40
+ import { ChangeRequestConflictsError, DuplicateChangeRequestError, OpenChangeRequestBlocksMergeError, RolesYamlPreservationError, PullRebaseConflictError, PushNeedsAgentResolutionError, RemoteBranchGoneError, WorkflowDomainError, WorkflowValidationError, } from '../../shared/domain-errors.js';
32
41
  import { RECOVERY_BOT_EMAIL, RECOVERY_BOT_NAME } from './recovery-bot.js';
33
42
  import { AccessDeniedError } from '../access-model/access-errors.js';
43
+ import { printable } from '../../shared/printable.js';
34
44
  const execFileAsync = promisify(execFile);
35
45
  /** The partial unique index that enforces one OPEN change request per (source, target) pair. */
36
46
  const OPEN_PAIR_CONSTRAINT = 'change_requests_open_pair_unq';
47
+ /** Cap on a persisted apply refusal — long enough to name the files a gate waits on. */
48
+ const APPLY_FAILURE_MAX_LEN = 1000;
37
49
  /**
38
50
  * True only for a Postgres unique violation (23505) on the open-CR-per-pair
39
51
  * index. `change_requests` also carries `change_requests_number_unq` on its
@@ -58,6 +70,28 @@ function isOpenPairViolation(err) {
58
70
  const message = fieldOf(err, 'message') ?? fieldOf(cause, 'message');
59
71
  return typeof message === 'string' && message.includes(OPEN_PAIR_CONSTRAINT);
60
72
  }
73
+ /**
74
+ * The one sentence a remote-sync conflict is reported with — in the sync
75
+ * response, on the branch's banner and in the log — so whoever sees any of
76
+ * the three reads the same thing: which branch, which files, that automatic
77
+ * recovery is under way, and what to do if it does not clear.
78
+ */
79
+ export function syncConflictMessage(branch, conflictedPaths) {
80
+ // Bounded: the same sentence goes to the response, the banner and the log,
81
+ // and it must be one sentence in all three whatever the conflict's size.
82
+ // The full path list rides beside it as `conflictedPaths`.
83
+ const shown = conflictedPaths.slice(0, SYNC_CONFLICT_FILES_NAMED);
84
+ const more = conflictedPaths.length - shown.length;
85
+ const files = conflictedPaths.length === 0
86
+ ? 'some files'
87
+ : more > 0
88
+ ? `${shown.join(', ')} and ${more} more file${more === 1 ? '' : 's'}`
89
+ : shown.join(', ');
90
+ return (`${branch} is not in sync yet: ${files} changed both in Hexis and on the git host. ` +
91
+ `Recovery is queued; if this stays, open the files on ${branch} in Hexis, keep what you want, and save.`);
92
+ }
93
+ /** How many conflicted files the sync-conflict sentence names before "and N more". */
94
+ const SYNC_CONFLICT_FILES_NAMED = 3;
61
95
  /** Redact the shared GitHub token from any error string before surfacing it. */
62
96
  function redactTokens(msg) {
63
97
  const tokens = [process.env.GITHUB_TOKEN, process.env.GH_TOKEN].filter((t) => !!t && t.length > 0);
@@ -77,7 +111,9 @@ const formatWriter = (u) => u.name ? `${u.name} <${u.email}>` : u.email;
77
111
  * the overflow is collapsed into a single deduped summary of every eligible
78
112
  * approver across the change. This keeps the body bounded regardless of how
79
113
  * many files the change touches (see {@link MAX_AFFECTED_PATHS_LISTED}).
80
- * Returns '' when no path has resolvable eligibility.
114
+ * Returns '' when no path has resolvable eligibility. The empty-folder
115
+ * placeholder is never listed or counted: it is not content, and no one
116
+ * reviews it.
81
117
  */
82
118
  export function formatAffectedOwnersBlock(paths, resolved, maxListed = MAX_AFFECTED_PATHS_LISTED) {
83
119
  const listed = [];
@@ -85,6 +121,8 @@ export function formatAffectedOwnersBlock(paths, resolved, maxListed = MAX_AFFEC
85
121
  const allUsers = new Map(); // email -> display, deduped
86
122
  let eligible = 0;
87
123
  for (const p of paths) {
124
+ if (isFolderPlaceholder(p))
125
+ continue;
88
126
  const info = resolved.get(p);
89
127
  if (!info)
90
128
  continue;
@@ -116,6 +154,26 @@ export function formatAffectedOwnersBlock(paths, resolved, maxListed = MAX_AFFEC
116
154
  }
117
155
  return out.join('\n');
118
156
  }
157
+ /** Git commit subjects are capped at 200 characters (`GitService.commitFile`). */
158
+ const MAX_COMMIT_SUBJECT = 200;
159
+ /**
160
+ * A commit subject that always fits: `full` when it does, else `short` (the
161
+ * file's name instead of its path, a count instead of every request), cut to
162
+ * the cap as a last resort. A long path or many requests on one branch must
163
+ * never fail the commit — and with it the whole folder removal.
164
+ */
165
+ function commitSubject(full, short) {
166
+ if (full.length <= MAX_COMMIT_SUBJECT)
167
+ return full;
168
+ if (short.length <= MAX_COMMIT_SUBJECT)
169
+ return short;
170
+ return `${short.slice(0, MAX_COMMIT_SUBJECT - 1)}…`;
171
+ }
172
+ const basenameOf = (repoRelPath) => repoRelPath.split('/').pop() ?? repoRelPath;
173
+ /** `Docs/Sub` and `Docs/Sub/` both → `Docs/Sub/`: a prefix that cannot match `Docs/Subway`. */
174
+ function folderPrefix(folder) {
175
+ return `${folder.replace(/\/+$/, '')}/`;
176
+ }
119
177
  export class WorkflowService {
120
178
  db;
121
179
  git;
@@ -126,6 +184,7 @@ export class WorkflowService {
126
184
  fileLocks;
127
185
  pendingCommits;
128
186
  kbDirName;
187
+ changeGate;
129
188
  events;
130
189
  fileChanges;
131
190
  hooks;
@@ -157,6 +216,13 @@ export class WorkflowService {
157
216
  * See `lock-decoupling-plan.md` for the full design.
158
217
  */
159
218
  pendingCommits, kbDirName,
219
+ /**
220
+ * The read-before-write gate `acquireLock` asks on every branch (see
221
+ * `access-model/change-gate.ts`). Required, and checked at construction:
222
+ * a service without it would let a change land where its author cannot
223
+ * read, so there is no such service.
224
+ */
225
+ changeGate,
160
226
  /**
161
227
  * Event bus that fans state changes out to connected SSE sessions.
162
228
  * Optional so test harnesses / minimal boots can omit it — emits
@@ -190,9 +256,15 @@ export class WorkflowService {
190
256
  this.fileLocks = fileLocks;
191
257
  this.pendingCommits = pendingCommits;
192
258
  this.kbDirName = kbDirName;
259
+ this.changeGate = changeGate;
193
260
  this.events = events;
194
261
  this.fileChanges = fileChanges;
195
262
  this.hooks = hooks;
263
+ // The type already requires it; this holds for a caller the compiler did
264
+ // not see (plain JavaScript, a positional slip in a harness).
265
+ if (!changeGate || typeof changeGate.assertMayChange !== 'function') {
266
+ throw new Error('WorkflowService requires a read-before-write gate (IChangeReadGate)');
267
+ }
196
268
  }
197
269
  // ── Branches ──────────────────────────────────────────────────────────────
198
270
  /**
@@ -212,17 +284,22 @@ export class WorkflowService {
212
284
  .sweepOrphanedWorkspaces(branches.map((b) => b.name))
213
285
  .then(({ removed }) => {
214
286
  if (removed.length > 0) {
215
- console.log(`[workflow] swept ${removed.length} orphaned workspace(s):`, removed);
287
+ log.info(`swept ${removed.length} orphaned workspace(s):`, { removed });
216
288
  }
217
289
  })
218
290
  .catch((err) => {
219
- console.warn('[workflow] orphan workspace sweep failed:', err);
291
+ log.warn('orphan workspace sweep failed:', { err });
220
292
  });
221
293
  }
222
294
  return branches;
223
295
  }
224
296
  createBranch(workspaceId, name, fromBase) {
225
- return this.git.createBranch(workspaceId, name, fromBase);
297
+ // Under the branch's lifecycle lock, like `deleteBranch` and the
298
+ // retirement of a clone the host deleted: creating a branch is the one
299
+ // operation that can bring a "gone" branch back, and the retirement's
300
+ // "still gone on origin?" check is only a guarantee if nothing can
301
+ // recreate the branch between that check and the delete.
302
+ return this.branchLifecycle.run(`branch:${name}`, () => this.git.createBranch(workspaceId, name, fromBase));
226
303
  }
227
304
  /**
228
305
  * Serialises everything that decides a branch's fate — opening a change
@@ -339,7 +416,7 @@ export class WorkflowService {
339
416
  }
340
417
  }
341
418
  catch (err) {
342
- console.warn(`[workflow] could not retire workspace clone of deleted branch "${name}":`, err);
419
+ log.warn(`could not retire workspace clone of deleted branch "${name}":`, { err });
343
420
  }
344
421
  }
345
422
  // No `switchBranch` here on purpose. Under the per-branch workspace model,
@@ -361,9 +438,42 @@ export class WorkflowService {
361
438
  refreshRemotes(workspaceId) {
362
439
  return this.git.fetch(workspaceId);
363
440
  }
441
+ /**
442
+ * `git pull` on a workspace, plus the one announcement a pull owes the rest
443
+ * of the process.
444
+ *
445
+ * A pull rewrites a working tree without going through the write routes or
446
+ * the commit pipeline, so it emits neither of the signals those produce —
447
+ * and the catalogs that scan the DEFAULT branch's tree (skills, tool
448
+ * manuals, the plugin index) are keyed to exactly that tree. Every path that
449
+ * pulls the default workspace therefore has to say so, or those catalogs
450
+ * keep serving the pre-pull scan for the rest of their TTL: a merge landing
451
+ * an approved skill, the recovery ladder after a non-fast-forward push, and
452
+ * a user's update-from-remote alike. Routing every pull through here makes
453
+ * that ONE mechanism — "the default branch's tree changed" — instead of a
454
+ * separate special case per event. (The remote sync, `syncWorkspaceFromRemote`,
455
+ * pulls directly: it owes the same announcement to every branch and per
456
+ * file, and makes it from the same `treeChanged` verdict.)
457
+ *
458
+ * Announced only after the pull RESOLVES, and only when it CHANGED the
459
+ * tree's content: a pull that threw left the tree as it was, and so did an
460
+ * "already up to date" pull or one that landed only content-identical
461
+ * commits — dropping every catalog and making every attached browser
462
+ * refetch its file tree for a change that did not happen buys a re-scan
463
+ * and nothing else. The pull itself reports the change, since it is the
464
+ * only code that can observe it under the workspace mutex.
465
+ */
466
+ async pullWorkspace(workspaceId) {
467
+ const { treeChanged } = await this.git.pull(workspaceId);
468
+ if (treeChanged && branchForWorkspaceId(workspaceId) === DEFAULT_BRANCH) {
469
+ this.events?.emit({ kind: 'fs-tree-changed', workspaceId, branch: DEFAULT_BRANCH });
470
+ return true;
471
+ }
472
+ return false;
473
+ }
364
474
  async updateFromRemote(workspaceId, user) {
365
475
  try {
366
- await this.git.pull(workspaceId);
476
+ await this.pullWorkspace(workspaceId);
367
477
  }
368
478
  catch (err) {
369
479
  if (err instanceof PullRebaseConflictError) {
@@ -372,6 +482,166 @@ export class WorkflowService {
372
482
  throw err;
373
483
  }
374
484
  }
485
+ async retireRemoteGoneClone(workspaceId) {
486
+ const branch = branchForWorkspaceId(workspaceId);
487
+ const id = workspaceIdForBranch(branch);
488
+ // The lock `createBranch` and `deleteBranch` take: no Hexis operation can
489
+ // bring this branch back into being while the clone is examined and
490
+ // removed. The sync's own hold on the clone ended when its outcome was
491
+ // returned, so the branch is asked about AGAIN here rather than trusted
492
+ // from then — a recreate that landed in between keeps its clone. A clone
493
+ // being bootstrapped right now is left alone too: it can only be cloning
494
+ // a branch that exists, so the retirement is already moot. What remains
495
+ // is a recreate pushed from OUTSIDE Hexis in the milliseconds between the
496
+ // origin check and the delete, with a bootstrap starting in that same
497
+ // window; the next sync of that branch clones it fresh.
498
+ return this.branchLifecycle.run(`branch:${branch}`, async () => {
499
+ if (!(await this.workspaceService.hasBootstrappedWorkspace(id)))
500
+ return false;
501
+ if (this.workspaceService.isBootstrapInFlight(branch))
502
+ return false;
503
+ if (await this.git.remoteBranchExists(branch, branch))
504
+ return false;
505
+ await this.workspaceService.deleteWorkspace(id);
506
+ // Whatever a sync still owed that clone is owed to nothing now.
507
+ this.owedAnnouncements.delete(id);
508
+ return true;
509
+ });
510
+ }
511
+ /**
512
+ * Cap on per-path `file-changed` events one sync announces for a branch.
513
+ * Past it the tree event alone carries the news — a bulk import that
514
+ * rewrote hundreds of files would otherwise fan hundreds of events out to
515
+ * every open browser to say what one refresh says.
516
+ */
517
+ static SYNC_FILE_EVENT_CAP = 200;
518
+ /**
519
+ * Announcements a sync still OWES, per workspace: a pull landed but the
520
+ * events for it could not be sent. The pull is not redone on retry — HEAD
521
+ * has not moved since — so the announcement cannot be re-derived from sha
522
+ * movement; it is remembered here and delivered by the next sync of that
523
+ * workspace, merged with whatever that sync finds.
524
+ */
525
+ owedAnnouncements = new Map();
526
+ /**
527
+ * The latest RUNNING apply attempt per change-request number (see
528
+ * `beginApplyAttempt`). Entries leave when their attempt ends, so the map
529
+ * holds only attempts in flight, never every number anyone ever posted.
530
+ */
531
+ applyAttempts = new Map();
532
+ /** Process-wide attempt counter: a token is never reissued, even after its entry leaves. */
533
+ applyAttemptSeq = 0;
534
+ async syncWorkspaceFromRemote(workspaceId) {
535
+ const branch = branchForWorkspaceId(workspaceId);
536
+ const id = workspaceIdForBranch(branch);
537
+ // Two spellings of one workspace id are in circulation — the encoded
538
+ // directory name (`ali%2Fx`, what the clone listing yields) and the
539
+ // Express-decoded form (`ali/x`, what every HTTP route hands the git
540
+ // layer) — and the git layer's mutex keys on the raw string. Speaking the
541
+ // routes' spelling to git is what puts a webhook's rebase in the same
542
+ // queue as the author's concurrent save on a slashed branch; the event
543
+ // bus gets the encoded form, which every SSE consumer canonicalises.
544
+ const gitId = branch;
545
+ // Every failure below is an OUTCOME, never a throw: the caller runs this
546
+ // over many clones, and one broken clone must not abort the others.
547
+ let pulled;
548
+ try {
549
+ pulled = await this.git.syncFromRemote(gitId);
550
+ }
551
+ catch (err) {
552
+ if (err instanceof RemoteBranchGoneError) {
553
+ // Not a failure: the host deleted the branch, so there is nothing to
554
+ // sync and the caller retires the stale clone. No banner — nobody is
555
+ // editing a branch that no longer exists. An announcement still owed
556
+ // for that clone dies with it: a branch recreated under the same name
557
+ // is a different tree, and replaying the old paths against it would
558
+ // announce changes that never happened there.
559
+ this.owedAnnouncements.delete(id);
560
+ syncLog.info(`branch "${branch}" no longer exists on the host`);
561
+ return { branch, outcome: 'remote-gone' };
562
+ }
563
+ if (err instanceof PullRebaseConflictError) {
564
+ // Same path as `updateFromRemote`: the rebase was aborted inside
565
+ // the pull (the clone is exactly what it was) and the divergence goes
566
+ // to the retry → recovery-agent → escalate ladder, attributed to the
567
+ // recovery bot since no person triggered this. It is still reported
568
+ // as a conflict because it is not resolved at request time — the
569
+ // caller's retry, or the ladder, settles it; the banner clears on the
570
+ // next clean pull or push.
571
+ await this.queuePullConflictRecovery(gitId, err);
572
+ const message = syncConflictMessage(branch, err.conflictedPaths);
573
+ syncLog.warn(message);
574
+ this.noteGitSyncFailed(gitId, branch, err, { paths: err.conflictedPaths, message });
575
+ return { branch, outcome: 'conflict', conflictedPaths: err.conflictedPaths, error: message };
576
+ }
577
+ const message = sanitizeError(err);
578
+ syncLog.warn(`pull failed for branch "${branch}": ${message}`);
579
+ this.noteGitSyncFailed(gitId, branch, err);
580
+ return { branch, outcome: 'error', error: message };
581
+ }
582
+ // A clean pull is proof origin is reachable and this clone rebases onto
583
+ // it — the condition a `git-sync-failed` banner on this branch was
584
+ // waiting for, whether a sync or a push raised it.
585
+ this.noteGitSyncOk(gitId, branch);
586
+ const { before, after, treeChanged, changedPaths } = pulled;
587
+ // What this branch reports about ITS clone is decided by the pull alone:
588
+ // `updated` when HEAD moved, `up-to-date` otherwise.
589
+ const outcome = after === null || after === before
590
+ ? { branch, outcome: 'up-to-date', to: after ?? '' }
591
+ : { branch, outcome: 'updated', from: before, to: after };
592
+ // What it ANNOUNCES is what has changed since the last announcement that
593
+ // was actually delivered: this pull's content change, if any, plus one an
594
+ // earlier sync landed but could not send. The announcement keys on the
595
+ // pull's verdict about CONTENT — commits that left the tree identical are
596
+ // nothing for a browser to refetch or a catalog to drop.
597
+ const owed = this.owedAnnouncements.get(id);
598
+ if (!treeChanged && !owed)
599
+ return outcome;
600
+ const announce = {
601
+ after: after ?? owed?.after ?? '',
602
+ changedPaths: [...new Set([...(owed?.changedPaths ?? []), ...changedPaths])],
603
+ };
604
+ try {
605
+ // Announce the new tree. The tree event alone already refreshes every
606
+ // open file explorer and, on the default branch, drops the skill / tool /
607
+ // plugin catalogues (`catalog-cache-invalidation.ts`); the CR list
608
+ // caches the touched paths it resolved against the old tree, so drop
609
+ // that too.
610
+ this.prs.invalidateListCache();
611
+ this.events?.emit({ kind: 'fs-tree-changed', workspaceId: id, branch });
612
+ // Per-file events so an open tab on a file the host rewrote refetches
613
+ // instead of showing stale bytes until its next tree refresh. The paths
614
+ // were observed under the same hold as the pull, so they are exactly
615
+ // this sync's — never a save that landed alongside. Capped: a bulk
616
+ // import would otherwise fan hundreds of events out to say what one
617
+ // refresh says.
618
+ if (announce.changedPaths.length <= WorkflowService.SYNC_FILE_EVENT_CAP) {
619
+ for (const p of announce.changedPaths) {
620
+ this.events?.emit({
621
+ kind: 'file-changed',
622
+ workspaceId: id,
623
+ branch,
624
+ path: `${this.kbDirName}/${p}`,
625
+ newSha: announce.after,
626
+ byUserId: 'system',
627
+ byUserName: 'Git sync',
628
+ });
629
+ }
630
+ }
631
+ }
632
+ catch (err) {
633
+ // The pull landed, so origin is fine and no banner is raised — but the
634
+ // caller must hear that this branch's announcement did not happen, and
635
+ // the announcement stays owed: the retry the error invites will not
636
+ // move HEAD again, so it must find the debt here rather than in the shas.
637
+ this.owedAnnouncements.set(id, announce);
638
+ const message = sanitizeError(err);
639
+ syncLog.warn(`pulled "${branch}" but could not announce it: ${message}`);
640
+ return { branch, outcome: 'error', error: message };
641
+ }
642
+ this.owedAnnouncements.delete(id);
643
+ return outcome;
644
+ }
375
645
  /**
376
646
  * A pull hit a rebase conflict — the workspace carries local commits that
377
647
  * conflict with origin, a state that never resolves on its own (every
@@ -408,11 +678,11 @@ export class WorkflowService {
408
678
  authorName: user?.name ?? RECOVERY_BOT_NAME,
409
679
  });
410
680
  if (queued) {
411
- console.warn(`[workflow] pull conflict on ws=${workspaceId} branch=${err.branch} (${err.conflictedPaths.join(', ')}) — queued background recovery`);
681
+ log.warn(`pull conflict on ws=${workspaceId} branch=${err.branch} (${err.conflictedPaths.join(', ')}) — queued background recovery`);
412
682
  }
413
683
  }
414
684
  catch (queueErr) {
415
- console.error(`[workflow] failed to queue pull-conflict recovery for ws=${workspaceId}:`, queueErr instanceof Error ? queueErr.message : queueErr);
685
+ log.error(`failed to queue pull-conflict recovery for ws=${workspaceId}:`, { err: queueErr });
416
686
  }
417
687
  }
418
688
  resetToRemote(workspaceId, branch) {
@@ -461,6 +731,12 @@ export class WorkflowService {
461
731
  compareFile(workspaceId, path, fromBranch, toBranch) {
462
732
  return this.git.diffFileBetweenBranches(workspaceId, path, fromBranch, toBranch);
463
733
  }
734
+ fileAtForkPoint(workspaceId, baseBranch, sha, path) {
735
+ return this.git.readFileAtForkPoint(workspaceId, baseBranch, sha, path);
736
+ }
737
+ changeRequestForkPoint(workspaceId, baseBranch, headBranch) {
738
+ return this.git.mergeBaseForPr(workspaceId, baseBranch, headBranch);
739
+ }
464
740
  showFileAtChange(workspaceId, path, sha) {
465
741
  return this.git.diffFileAtCommit(workspaceId, path, sha);
466
742
  }
@@ -482,8 +758,18 @@ export class WorkflowService {
482
758
  * Always reads at-ref, never at the working tree, so a user can't broaden
483
759
  * their own access by editing `roles.yaml` / `access.md` in the same
484
760
  * session.
761
+ *
762
+ * Resolves to `'restore'` when the write passed ONLY as a platform-file
763
+ * restore — the caller then knows the destination denies this admin, and
764
+ * the read gate that follows stands aside for the same rescue.
485
765
  */
486
- async assertCanWriteAtPath(workspaceId, branch, userEmail, targetPath) {
766
+ async assertCanWriteAtPath(workspaceId, branch, userEmail, targetPath,
767
+ /**
768
+ * The caller says this write is an admin putting a misplaced platform file
769
+ * back, and names where the file is coming from. Checked in both halves,
770
+ * never taken on trust — see `acquireLock`'s `opts`.
771
+ */
772
+ platformRestore) {
487
773
  // The lock route passes workspace-relative paths
488
774
  // (`knowledge-base/GTM/.../Foo.md`), but the access model is keyed by
489
775
  // *repo-relative* paths — `git ls-tree` runs inside the inner repo dir,
@@ -493,14 +779,30 @@ export class WorkflowService {
493
779
  // typically only grants Admin → non-admins get 403'd on files their
494
780
  // role's nested access.md actually permits. Mirrors the frontend's
495
781
  // `useFileAccess` prefix-strip on the way INTO the API.
496
- const repoRelative = targetPath.startsWith(`${this.kbDirName}/`)
497
- ? targetPath.slice(this.kbDirName.length + 1)
498
- : targetPath;
782
+ const toRepoRelative = (p) => p.startsWith(`${this.kbDirName}/`) ? p.slice(this.kbDirName.length + 1) : p;
783
+ const repoRelative = toRepoRelative(targetPath);
499
784
  const result = await this.accessControl.canWriteBatchAtRef(workspaceId, `HEAD`, userEmail, [repoRelative]);
500
785
  if (!result)
501
- return; // no config at ref → default-allow (bootstrap)
786
+ return 'granted'; // no config at ref → default-allow (bootstrap)
502
787
  if (result.get(repoRelative))
503
- return;
788
+ return 'granted';
789
+ // The one write allowed past a destination that denies it: an admin
790
+ // putting a platform file back where the platform reads it. The access
791
+ // module decides who — a repository whose root `access.md` is the file
792
+ // that went missing denies everyone, including the admin who would
793
+ // restore it — and the shape check decides WHICH MOVE may even ask.
794
+ //
795
+ // Both are asked here rather than trusted from the route, because
796
+ // `canRestorePlatformFile` answers only on where the write LANDS: on its
797
+ // own it would approve carrying the root's own `access.md` into a folder
798
+ // that has none, which is the loss this feature exists to prevent, spelled
799
+ // as its own rescue. The source is part of the claim precisely so no
800
+ // future caller can make that claim by passing a flag.
801
+ if (typeof platformRestore?.source === 'string' &&
802
+ isPlatformRestoreShape(toRepoRelative(platformRestore.source), repoRelative) &&
803
+ (await this.accessControl.canRestorePlatformFile(workspaceId, userEmail, repoRelative))) {
804
+ return 'restore';
805
+ }
504
806
  const eligible = await this.accessControl.eligibleWritersAtRef(workspaceId, `HEAD`, repoRelative);
505
807
  throw new AccessDeniedError({
506
808
  path: targetPath,
@@ -509,7 +811,19 @@ export class WorkflowService {
509
811
  });
510
812
  }
511
813
  // ── File locks ────────────────────────────────────────────────────────────
512
- async acquireLock(workspaceId, branch, targetPath, user, opts) {
814
+ // Every method below canonicalises the caller's spelling into ONE file
815
+ // identity before it does anything with it. `FileLockService` canonicalises
816
+ // too, and has to: it is the coordination point and a caller can reach it
817
+ // without coming through here. The reason to do it again at this layer is
818
+ // that the path does not only key a lock row here. It is also what the
819
+ // permission gate is evaluated against, what `commitFile` stages, what the
820
+ // commit queue enqueues, and what rides out on every `lock-*` event. Left
821
+ // raw, a checkpoint on `./x//a.md` would find its lock and then fail at the
822
+ // commit, and a release would enqueue a row the worker keys differently from
823
+ // the lock it just dropped. Canonicalising is idempotent, so the second pass
824
+ // inside the lock service is free.
825
+ async acquireLock(workspaceId, branch, rawPath, user, opts) {
826
+ const targetPath = canonicalFileIdentity(rawPath);
513
827
  // **Permission check at lock acquisition, not at commit time.** Under the
514
828
  // "disk is the source of truth" rule, once a write has landed on disk we
515
829
  // must never reject the commit that publishes it — otherwise we'd be
@@ -519,14 +833,24 @@ export class WorkflowService {
519
833
  // the lock and the editor never opens. Once the lock is in hand, the
520
834
  // commit + push pipeline does not re-check (see `commitFile`).
521
835
  //
522
- // **Protected branches only** (mirrors the legacy commit-time gate). On
523
- // feature/draft branches anyone can write; canonical state changes go
524
- // through change-request approval, which is where the real security
525
- // boundary lives. Checking at HEAD (not at the working tree) so a user
526
- // can't grant themselves access by editing `roles.yaml` in the same
836
+ // **Write gate — protected branches only** (mirrors the legacy commit-time
837
+ // gate). On feature/draft branches anyone can write; canonical state
838
+ // changes go through change-request approval, which is where the real
839
+ // security boundary lives. Checking at HEAD (not at the working tree) so a
840
+ // user can't grant themselves access by editing `roles.yaml` in the same
527
841
  // session.
528
842
  //
529
- // **Coordination acquires skip the gate** (see the interface doc): the
843
+ // **Read gate — every branch.** Nothing is created, changed or removed
844
+ // where its author cannot read, drafts included: a proposal that lands in
845
+ // a folder its author cannot see would vanish from them the moment it was
846
+ // added, and a write grant only implies read — a nearer `deny read` can
847
+ // take the read away while the write stands. The one exception, a new
848
+ // folder directly under one of the three roots, and the reasons are in
849
+ // `access-model/change-gate.ts`. A write that passed only as a
850
+ // platform-file restore is not asked: that rescue exists exactly for a
851
+ // destination whose rules deny the admin making it.
852
+ //
853
+ // **Coordination acquires skip both gates** (see the interface doc): the
530
854
  // caller wants only mutual exclusion with the path's writer — e.g. the
531
855
  // roles admin holding machine-owned `synced-groups.yaml` steady across
532
856
  // its IdP-mode recheck — and will never write the path. Gating those on
@@ -535,14 +859,20 @@ export class WorkflowService {
535
859
  // authority flows from the hold: the mode is persisted on the lock row
536
860
  // and `commitFileWhileLocked` / `releaseLock` refuse to treat a
537
861
  // coordination hold as write possession (see those methods).
538
- if (isProtectedBranch(branch) && !opts?.coordination) {
539
- // `assertCanWriteAtPath` throws AccessDeniedError on denial, with the
540
- // eligible-writers payload so the frontend can render a useful refusal.
541
- await this.assertCanWriteAtPath(workspaceId, branch, user.email, targetPath);
862
+ if (!opts?.coordination) {
863
+ let via = 'granted';
864
+ if (isProtectedBranch(branch)) {
865
+ // `assertCanWriteAtPath` throws AccessDeniedError on denial, with the
866
+ // eligible-writers payload so the frontend can render a useful refusal.
867
+ via = await this.assertCanWriteAtPath(workspaceId, branch, user.email, targetPath, opts?.platformRestore);
868
+ }
869
+ if (via !== 'restore') {
870
+ await this.changeGate.assertMayChange(workspaceId, user.email, targetPath, 'file');
871
+ }
542
872
  }
543
873
  const result = await this.fileLocks.acquire(workspaceId, branch, targetPath, user, opts);
544
874
  if (result.acquired) {
545
- console.log(`[lock] ACQUIRE ws=${workspaceId} branch=${branch} path=${targetPath} user=${user.id} → acquired`);
875
+ lockLog.info(`ACQUIRE ws=${workspaceId} branch=${branch} path=${targetPath} user=${user.id} → acquired`);
546
876
  // Only fire on successful acquisition — a contention "false" return
547
877
  // means nothing observable changed for other users (someone else
548
878
  // already held the lock and still does). Stale-lock takeover is also
@@ -557,18 +887,19 @@ export class WorkflowService {
557
887
  });
558
888
  }
559
889
  else {
560
- console.log(`[lock] ACQUIRE ws=${workspaceId} branch=${branch} path=${targetPath} user=${user.id} → contended, held by ${result.lock.holderName} (${result.lock.holderUserId})`);
890
+ lockLog.info(`ACQUIRE ws=${workspaceId} branch=${branch} path=${targetPath} user=${user.id} → contended, held by ${result.lock.holderName} (${result.lock.holderUserId})`);
561
891
  }
562
892
  return result;
563
893
  }
564
- async heartbeatLock(workspaceId, branch, targetPath, user) {
894
+ async heartbeatLock(workspaceId, branch, rawPath, user) {
895
+ const targetPath = canonicalFileIdentity(rawPath);
565
896
  try {
566
897
  const lock = await this.fileLocks.heartbeat(workspaceId, branch, targetPath, user);
567
- console.log(`[lock] HEARTBEAT ws=${workspaceId} branch=${branch} path=${targetPath} user=${user.id} → ok (expires ${lock.expiresAt})`);
898
+ lockLog.info(`HEARTBEAT ws=${workspaceId} branch=${branch} path=${targetPath} user=${user.id} → ok (expires ${lock.expiresAt})`);
568
899
  return lock;
569
900
  }
570
901
  catch (err) {
571
- console.warn(`[lock] HEARTBEAT ws=${workspaceId} branch=${branch} path=${targetPath} user=${user.id} → FAIL ${err instanceof Error ? err.message : err}`);
902
+ lockLog.warn(`HEARTBEAT ws=${workspaceId} branch=${branch} path=${targetPath} user=${user.id} → FAIL ${err instanceof Error ? err.message : err}`);
572
903
  throw err;
573
904
  }
574
905
  }
@@ -579,7 +910,8 @@ export class WorkflowService {
579
910
  * the lock so a stale client can't sneak commits through after their
580
911
  * lock expired and another user took over.
581
912
  */
582
- async commitFileWhileLocked(workspaceId, branch, targetPath, user, summary) {
913
+ async commitFileWhileLocked(workspaceId, branch, rawPath, user, summary) {
914
+ const targetPath = canonicalFileIdentity(rawPath);
583
915
  const lock = await this.fileLocks.get(workspaceId, branch, targetPath);
584
916
  if (!lock || lock.holderUserId !== user.id) {
585
917
  throw new WorkflowValidationError(`Cannot commit "${targetPath}": you don't hold the lock.`, { kind: 'lock-not-held', branch, path: targetPath });
@@ -619,14 +951,14 @@ export class WorkflowService {
619
951
  let recoveryError = null;
620
952
  if (looksLikeNonFastForward) {
621
953
  try {
622
- await this.git.pull(workspaceId);
954
+ await this.pullWorkspace(workspaceId);
623
955
  await this.git.push(workspaceId, user);
624
956
  recovered = true;
625
957
  this.noteGitSyncOk(workspaceId, branch);
626
958
  }
627
959
  catch (recoveryErr) {
628
960
  recoveryError = recoveryErr;
629
- console.warn('[workflow] autosave cooperative recovery (pull-rebase) failed; leaving the unpushed commit for the next save / releaseLock to surface:', recoveryErr instanceof Error ? recoveryErr.message : recoveryErr);
961
+ log.warn('autosave cooperative recovery (pull-rebase) failed; leaving the unpushed commit for the next save / releaseLock to surface:', { err: recoveryErr });
630
962
  }
631
963
  }
632
964
  if (!recovered) {
@@ -644,7 +976,7 @@ export class WorkflowService {
644
976
  if (!looksLikeNonFastForward) {
645
977
  this.noteGitSyncFailed(workspaceId, branch, recoveryError ?? err);
646
978
  }
647
- console.warn('[workflow] push after autosave commit failed (commit landed locally):', detail);
979
+ log.warn('push after autosave commit failed (commit landed locally):', { detail });
648
980
  }
649
981
  }
650
982
  this.events?.emit({
@@ -689,16 +1021,17 @@ export class WorkflowService {
689
1021
  * worker; push is the worker's job; there's no synchronous `Change`
690
1022
  * object to return because the commit hasn't happened yet.
691
1023
  */
692
- async releaseLock(workspaceId, branch, targetPath, user) {
1024
+ async releaseLock(workspaceId, branch, rawPath, user) {
1025
+ const targetPath = canonicalFileIdentity(rawPath);
693
1026
  // Ownership check — the lock service's `release` is idempotent and
694
1027
  // would silently no-op for a non-holder, but we'd still enqueue a
695
1028
  // commit attributed to whoever called us. The guard rejects callers
696
1029
  // who don't actually hold the row so impersonation can't enqueue
697
1030
  // commits as a third party.
698
- console.log(`[lock] RELEASE start ws=${workspaceId} branch=${branch} path=${targetPath} user=${user.id}`);
1031
+ lockLog.info(`RELEASE start ws=${workspaceId} branch=${branch} path=${targetPath} user=${user.id}`);
699
1032
  const lock = await this.fileLocks.get(workspaceId, branch, targetPath);
700
1033
  if (!lock || lock.holderUserId !== user.id) {
701
- console.warn(`[lock] RELEASE refused ws=${workspaceId} branch=${branch} path=${targetPath} user=${user.id} → ${lock ? `held by ${lock.holderName} (${lock.holderUserId})` : 'no lock row'}`);
1034
+ lockLog.warn(`RELEASE refused ws=${workspaceId} branch=${branch} path=${targetPath} user=${user.id} → ${lock ? `held by ${lock.holderName} (${lock.holderUserId})` : 'no lock row'}`);
702
1035
  throw new WorkflowValidationError(`Cannot release lock on "${targetPath}": not held by you (or no longer exists).`, { kind: 'lock-not-held', branch, path: targetPath });
703
1036
  }
704
1037
  // Releasing a coordination hold must NEVER enqueue a commit: the hold was
@@ -711,7 +1044,7 @@ export class WorkflowService {
711
1044
  // and `releaseLockUntouched`. Worst case a refused caller strands the
712
1045
  // row until its TTL.
713
1046
  if (lock.mode === 'coordination') {
714
- console.warn(`[lock] RELEASE refused ws=${workspaceId} branch=${branch} path=${targetPath} user=${user.id} → coordination hold (no commit may be enqueued)`);
1047
+ lockLog.warn(`RELEASE refused ws=${workspaceId} branch=${branch} path=${targetPath} user=${user.id} → coordination hold (no commit may be enqueued)`);
715
1048
  throw new WorkflowValidationError(`Cannot release lock on "${targetPath}" with a commit: it is a coordination hold. Use releaseLockNoCommit.`, { kind: 'coordination-hold', branch, path: targetPath });
716
1049
  }
717
1050
  // Order matters here:
@@ -732,7 +1065,7 @@ export class WorkflowService {
732
1065
  authorName: user.name,
733
1066
  });
734
1067
  await this.fileLocks.release(workspaceId, branch, targetPath, user);
735
- console.log(`[lock] RELEASE done ws=${workspaceId} branch=${branch} path=${targetPath} user=${user.id} (commit queued for background worker)`);
1068
+ lockLog.info(`RELEASE done ws=${workspaceId} branch=${branch} path=${targetPath} user=${user.id} (commit queued for background worker)`);
736
1069
  // The `file-changed` SSE event used to fire here after the synchronous
737
1070
  // commit landed, carrying the new sha. Under the queue model the
738
1071
  // commit hasn't happened yet — the worker emits `file-changed` itself
@@ -819,7 +1152,7 @@ export class WorkflowService {
819
1152
  const id = branchForWorkspaceId(workspaceId);
820
1153
  if (!this.gitSyncFailing.delete(id))
821
1154
  return;
822
- console.log(`[workflow] git sync recovered for workspace=${id} branch=${branch}`);
1155
+ log.info(`git sync recovered for workspace=${id} branch=${branch}`);
823
1156
  this.events?.emit({ kind: 'git-sync-recovered', workspaceId: id, branch });
824
1157
  }
825
1158
  /**
@@ -830,7 +1163,29 @@ export class WorkflowService {
830
1163
  * machinery is unaffected — all this adds is that someone finds out, rather
831
1164
  * than the deployment looking healthy while nothing reaches the remote.
832
1165
  */
833
- noteGitSyncFailed(workspaceId, branch, err) {
1166
+ noteGitSyncFailed(workspaceId, branch, err,
1167
+ /**
1168
+ * The files a person must reconcile, and the sentence we composed about
1169
+ * them. That sentence is ours (branch name + repo paths, no git stderr),
1170
+ * so it goes out verbatim — the same string the sync response and the
1171
+ * log carry — rather than through the sanitiser, whose 200-character
1172
+ * clip would make the banner disagree with them. Callers that already
1173
+ * hold the sentence pass it; otherwise it is derived below.
1174
+ */
1175
+ explicitConflict) {
1176
+ // A rebase conflict is a conflict whichever path ran into it. The remote
1177
+ // sync names its files on purpose; the push-recovery paths (autosave, the
1178
+ // lock release, the queued retries) raise the same banner with the raw
1179
+ // error — and the banner keeps the LATEST failure per branch, so a retry
1180
+ // landing after the sync would replace the conflict variant, with the
1181
+ // files as links, by the generic "check the server logs" one. Seen on
1182
+ // staging: the sync's 409 named the file, the worker's retry a moment
1183
+ // later took the links away. Derive the paths from the error itself so
1184
+ // every path agrees.
1185
+ const conflict = explicitConflict ??
1186
+ (err instanceof PullRebaseConflictError
1187
+ ? { paths: err.conflictedPaths, message: syncConflictMessage(branch, err.conflictedPaths) }
1188
+ : undefined);
834
1189
  // Same canonicalization as `noteGitSyncOk` — the pair must agree on keys.
835
1190
  const id = branchForWorkspaceId(workspaceId);
836
1191
  this.gitSyncFailing.add(id);
@@ -840,7 +1195,8 @@ export class WorkflowService {
840
1195
  branch,
841
1196
  // Git stderr can quote a credentialed URL; the banner is user-facing and
842
1197
  // the string also lands in client logs, so sanitize before it leaves.
843
- reason: sanitizeError(err),
1198
+ reason: conflict ? conflict.message : sanitizeError(err),
1199
+ ...(conflict ? { conflictedPaths: conflict.paths } : {}),
844
1200
  });
845
1201
  }
846
1202
  /**
@@ -889,16 +1245,16 @@ export class WorkflowService {
889
1245
  let recoveryError = null;
890
1246
  if (looksLikeNonFastForward) {
891
1247
  try {
892
- await this.git.pull(workspaceId);
1248
+ await this.pullWorkspace(workspaceId);
893
1249
  await this.git.push(workspaceId, user, opts);
894
1250
  recovered = true;
895
1251
  this.noteGitSyncOk(workspaceId, branch);
896
- console.log(`[workflow] non-fast-forward push recovered via pull --rebase for workspace=${workspaceId} branch=${branch} path=${targetPath}`);
1252
+ log.info(`non-fast-forward push recovered via pull --rebase for workspace=${workspaceId} branch=${branch} path=${targetPath}`);
897
1253
  }
898
1254
  catch (recoveryErr) {
899
1255
  recoveryError = recoveryErr;
900
1256
  recoveryDetail = recoveryErr instanceof Error ? recoveryErr.message : String(recoveryErr);
901
- console.warn(`[workflow] cooperative recovery (pull-rebase) failed for workspace=${workspaceId} user=${user.id}; handing off to agent:`, recoveryDetail);
1257
+ log.warn(`cooperative recovery (pull-rebase) failed for workspace=${workspaceId} user=${user.id}; handing off to agent:`, { detail: recoveryDetail });
902
1258
  }
903
1259
  }
904
1260
  if (!recovered) {
@@ -910,7 +1266,7 @@ export class WorkflowService {
910
1266
  // first rejection may be a stale non-fast-forward the pull already
911
1267
  // cured); when it was skipped, the first error is all there is.
912
1268
  this.noteGitSyncFailed(workspaceId, branch, recoveryError ?? firstPushErr);
913
- console.warn(`[workflow] push failed for workspace=${workspaceId} user=${user.id}; throwing PushNeedsAgentResolutionError so the frontend can hand off to the agent:`, firstDetail);
1269
+ log.warn(`push failed for workspace=${workspaceId} user=${user.id}; throwing PushNeedsAgentResolutionError so the frontend can hand off to the agent:`, { detail: firstDetail });
914
1270
  throw new PushNeedsAgentResolutionError(branch, targetPath, firstDetail, recoveryDetail);
915
1271
  }
916
1272
  }
@@ -924,17 +1280,18 @@ export class WorkflowService {
924
1280
  * its catch arm so a thrown op (path traversal, validator 422,
925
1281
  * permission denied) doesn't end up committing.
926
1282
  */
927
- async releaseLockNoCommit(workspaceId, branch, targetPath, user) {
1283
+ async releaseLockNoCommit(workspaceId, branch, rawPath, user) {
1284
+ const targetPath = canonicalFileIdentity(rawPath);
928
1285
  // Verify ownership first so the emit only fires when something
929
1286
  // observable actually changed. `fileLocks.release` silently no-ops
930
1287
  // when the caller doesn't hold the row (idempotent-by-design), so
931
1288
  // without this guard a non-holder calling this method would emit
932
1289
  // `lock-released` and trick every other client into clearing their
933
1290
  // "Locked by X" banner even though the lock is still held.
934
- console.log(`[lock] RELEASE-NO-COMMIT start ws=${workspaceId} branch=${branch} path=${targetPath} user=${user.id}`);
1291
+ lockLog.info(`RELEASE-NO-COMMIT start ws=${workspaceId} branch=${branch} path=${targetPath} user=${user.id}`);
935
1292
  const lock = await this.fileLocks.get(workspaceId, branch, targetPath);
936
1293
  if (!lock || lock.holderUserId !== user.id) {
937
- console.log(`[lock] RELEASE-NO-COMMIT no-op ws=${workspaceId} path=${targetPath} → ${lock ? `held by ${lock.holderName}` : 'no lock row'}`);
1294
+ lockLog.info(`RELEASE-NO-COMMIT no-op ws=${workspaceId} path=${targetPath} → ${lock ? `held by ${lock.holderName}` : 'no lock row'}`);
938
1295
  return;
939
1296
  }
940
1297
  // **Discard the file's working-tree changes before dropping the lock.**
@@ -962,7 +1319,9 @@ export class WorkflowService {
962
1319
  catch (err) {
963
1320
  // Queue unreadable — fall back to the discard (the strict default:
964
1321
  // never let a coordination hold end with publishable stray bytes).
965
- console.warn(`[workflow] pending-commit lookup failed for workspace=${workspaceId} path=${targetPath}; discarding:`, err instanceof Error ? err.message : err);
1322
+ log.warn(`pending-commit lookup failed for workspace=${workspaceId} path=${targetPath}; discarding:`, {
1323
+ err,
1324
+ });
966
1325
  }
967
1326
  }
968
1327
  let discarded = false;
@@ -975,11 +1334,13 @@ export class WorkflowService {
975
1334
  // Best-effort: a discard failure is logged but doesn't block the
976
1335
  // lock release. Worst case the working tree stays dirty for one
977
1336
  // path until the next save on it cleans up.
978
- console.warn(`[workflow] discardPath failed for workspace=${workspaceId} branch=${branch} path=${targetPath}:`, err instanceof Error ? err.message : err);
1337
+ log.warn(`discardPath failed for workspace=${workspaceId} branch=${branch} path=${targetPath}:`, {
1338
+ err,
1339
+ });
979
1340
  }
980
1341
  }
981
1342
  await this.fileLocks.release(workspaceId, branch, targetPath, user);
982
- console.log(`[lock] RELEASE-NO-COMMIT done ws=${workspaceId} branch=${branch} path=${targetPath} user=${user.id} → ${discarded ? 'discarded + released' : 'released (working tree untouched)'}`);
1343
+ lockLog.info(`RELEASE-NO-COMMIT done ws=${workspaceId} branch=${branch} path=${targetPath} user=${user.id} → ${discarded ? 'discarded + released' : 'released (working tree untouched)'}`);
983
1344
  this.events?.emit({
984
1345
  kind: 'lock-released',
985
1346
  workspaceId,
@@ -1013,15 +1374,16 @@ export class WorkflowService {
1013
1374
  * lock row, nothing else. Same idempotent non-holder no-op (and emit
1014
1375
  * discipline) as `releaseLockNoCommit`.
1015
1376
  */
1016
- async releaseLockUntouched(workspaceId, branch, targetPath, user) {
1017
- console.log(`[lock] RELEASE-UNTOUCHED start ws=${workspaceId} branch=${branch} path=${targetPath} user=${user.id}`);
1377
+ async releaseLockUntouched(workspaceId, branch, rawPath, user) {
1378
+ const targetPath = canonicalFileIdentity(rawPath);
1379
+ lockLog.info(`RELEASE-UNTOUCHED start ws=${workspaceId} branch=${branch} path=${targetPath} user=${user.id}`);
1018
1380
  const lock = await this.fileLocks.get(workspaceId, branch, targetPath);
1019
1381
  if (!lock || lock.holderUserId !== user.id) {
1020
- console.log(`[lock] RELEASE-UNTOUCHED no-op ws=${workspaceId} path=${targetPath} → ${lock ? `held by ${lock.holderName}` : 'no lock row'}`);
1382
+ lockLog.info(`RELEASE-UNTOUCHED no-op ws=${workspaceId} path=${targetPath} → ${lock ? `held by ${lock.holderName}` : 'no lock row'}`);
1021
1383
  return;
1022
1384
  }
1023
1385
  await this.fileLocks.release(workspaceId, branch, targetPath, user);
1024
- console.log(`[lock] RELEASE-UNTOUCHED done ws=${workspaceId} branch=${branch} path=${targetPath} user=${user.id} → released (disk + queue untouched)`);
1386
+ lockLog.info(`RELEASE-UNTOUCHED done ws=${workspaceId} branch=${branch} path=${targetPath} user=${user.id} → released (disk + queue untouched)`);
1025
1387
  this.events?.emit({
1026
1388
  kind: 'lock-released',
1027
1389
  workspaceId,
@@ -1031,14 +1393,16 @@ export class WorkflowService {
1031
1393
  // No `file-changed` here on purpose — unlike the no-commit release,
1032
1394
  // nothing on disk moved.
1033
1395
  }
1034
- hasQueuedCommit(workspaceId, branch, targetPath) {
1035
- return this.pendingCommits.hasLiveRowFor(workspaceId, branch, targetPath);
1396
+ hasQueuedCommit(workspaceId, branch, rawPath) {
1397
+ // The row was stored under the canonical identity (`releaseLock`), so the
1398
+ // question is asked under it too — whatever spelling the caller has.
1399
+ return this.pendingCommits.hasLiveRowFor(workspaceId, branch, canonicalFileIdentity(rawPath));
1036
1400
  }
1037
1401
  hasUnpushedCommits(workspaceId) {
1038
1402
  return this.git.hasUnpushedCommits(workspaceId);
1039
1403
  }
1040
- getLock(workspaceId, branch, targetPath) {
1041
- return this.fileLocks.get(workspaceId, branch, targetPath);
1404
+ getLock(workspaceId, branch, rawPath) {
1405
+ return this.fileLocks.get(workspaceId, branch, canonicalFileIdentity(rawPath));
1042
1406
  }
1043
1407
  // ── Change Requests ───────────────────────────────────────────────────────
1044
1408
  listChangeRequests(opts) {
@@ -1143,7 +1507,7 @@ export class WorkflowService {
1143
1507
  targetBranch: input.targetBranch,
1144
1508
  title: input.title.trim(),
1145
1509
  body,
1146
- authorEmail: user.email.trim().toLowerCase(),
1510
+ authorEmail: canonicalEmail(user.email),
1147
1511
  authorName: user.name,
1148
1512
  })
1149
1513
  .returning({ number: changeRequests.number });
@@ -1206,10 +1570,20 @@ export class WorkflowService {
1206
1570
  * as `ChangeRequestConflictsError`; the caller (UI or agent) is expected
1207
1571
  * to route into the resolution flow exactly as on open.
1208
1572
  *
1209
- * Approvals on files touched by the merge resolution invalidate
1210
- * automatically — they're pinned to the head SHA, and any new commit on
1211
- * source drops the per-file approval gate (handled by the existing
1212
- * approval staleness check). No special bookkeeping needed here.
1573
+ * Approvals are carried across the merge BY CONTENT: every approval on a
1574
+ * file whose bytes the merge left alone is re-pinned to the new head, and
1575
+ * only a file the merge actually changed loses its approval (staleness is
1576
+ * still `row.headSha !== headSha`, unchanged). Without this, a merge
1577
+ * nobody asked for would void every approval on the request — including
1578
+ * the files it never touched — and since the dialog now runs this update
1579
+ * by itself, every stale request would cost a second round of review.
1580
+ * This holds for EVERY caller of the route, agent or UI.
1581
+ *
1582
+ * Authority: the request's author, or anyone who may apply it — the
1583
+ * detail's `viewerCanUpdate`, computed for THIS caller, so the button and
1584
+ * the enforcement read one predicate. A conflicting merge is aborted
1585
+ * inside `mergeFromOrigin`: nothing is committed or pushed, and the branch
1586
+ * is exactly what it was.
1213
1587
  */
1214
1588
  async updateFromTarget(workspaceId, user, number) {
1215
1589
  const detail = await this.prs.getPrDetail(number, { fresh: true, workspaceId, viewerEmail: user.email });
@@ -1219,6 +1593,47 @@ export class WorkflowService {
1219
1593
  if (detail.state !== 'open') {
1220
1594
  throw new WorkflowValidationError(`Change request #${number} is ${detail.state} — only open requests can refresh from target.`, { kind: 'change-request-not-open', state: detail.state });
1221
1595
  }
1596
+ if (!detail.viewerCanUpdate) {
1597
+ throw new WorkflowDomainError('Only the author of this change request, or someone who may apply it, can update it.', 403);
1598
+ }
1599
+ // Taken BEFORE the head moves (the pull below can move it too): only a
1600
+ // refusal recorded earlier describes the revision this replaces (see
1601
+ // `clearApplyFailure`).
1602
+ const headMovedAfter = new Date();
1603
+ // Freshen the source checkout first, and let a failure stop the Update:
1604
+ // merging onto a head behind its own origin branch would leave a local
1605
+ // merge commit that then cannot push, stranding it in the workspace. A
1606
+ // rebase conflict hands the stranded saves to recovery, as every other
1607
+ // pull does, before the refusal reaches the caller.
1608
+ try {
1609
+ await this.pullWorkspace(workspaceId);
1610
+ }
1611
+ catch (err) {
1612
+ if (err instanceof PullRebaseConflictError) {
1613
+ await this.queuePullConflictRecovery(workspaceId, err, user);
1614
+ }
1615
+ throw err;
1616
+ }
1617
+ // The head an approval is pinned to is whatever is PUBLISHED on the
1618
+ // request's branch — and the pull above may just have moved it: an agent,
1619
+ // or the author from another client, can push while this dialog is still
1620
+ // reading the detail. Resolve it HERE rather than trusting the
1621
+ // `detail.headSha` read before the pull: an approval made against that
1622
+ // newer head carries a sha the pre-pull detail does not name, so carrying
1623
+ // forward from the stale one would find no rows and void every approval
1624
+ // the merge never touched.
1625
+ //
1626
+ // Best effort, like the carry-forward it feeds: the bookkeeping must never
1627
+ // be what stops a request from being brought up to date, so a failed
1628
+ // resolve falls back to the sha the detail reported.
1629
+ let headBeforeMerge = detail.headSha;
1630
+ try {
1631
+ const at = await this.git.resolvePrShas(workspaceId, detail.base, detail.branch);
1632
+ headBeforeMerge = at.headSha;
1633
+ }
1634
+ catch (err) {
1635
+ log.warn(`could not resolve the published head of change request #${number} before updating it from target; falling back to the head its detail reported`, { err });
1636
+ }
1222
1637
  const outcome = await this.git.mergeFromOrigin(workspaceId, detail.branch, detail.base, user);
1223
1638
  if (outcome.kind === 'conflicts') {
1224
1639
  throw new ChangeRequestConflictsError(detail.branch, detail.base, outcome.paths);
@@ -1227,6 +1642,8 @@ export class WorkflowService {
1227
1642
  // already up to date there's nothing to share — short-circuit the push.
1228
1643
  if (!outcome.alreadyUpToDate) {
1229
1644
  await this.trackedPush(workspaceId, user);
1645
+ // The source head moved: the refusal described a revision that is gone.
1646
+ await this.clearApplyFailure(number, { recordedBefore: headMovedAfter });
1230
1647
  }
1231
1648
  this.prs.invalidateDetailCache(number);
1232
1649
  const refreshed = await this.prs.getPrDetail(number, {
@@ -1237,6 +1654,41 @@ export class WorkflowService {
1237
1654
  if (!refreshed) {
1238
1655
  throw new WorkflowValidationError(`Refreshed change request #${number} but could not re-fetch its detail.`);
1239
1656
  }
1657
+ // The merge is a commit the APPROVERS did not make. Re-pin their
1658
+ // approvals onto the new head for every file whose bytes it left alone —
1659
+ // git's own two-dot diff between the two heads is the verdict on which
1660
+ // those are. Best effort: a request that IS up to date is worth far more
1661
+ // than the bookkeeping, so a failure here is logged and the fresh detail
1662
+ // still goes back.
1663
+ //
1664
+ // `alreadyUpToDate` is the real gate: no merge commit, no head to compare,
1665
+ // nothing moved under anyone. The other two conditions are belt-and-braces
1666
+ // over a sha that is not a sha — a resolve that failed onto a detail with
1667
+ // no head, or two heads that somehow read equal — both of which
1668
+ // `pathsChangedBetween` and `carryApprovalsForward` would refuse anyway.
1669
+ // A merge that authored a commit cannot leave the head where it was.
1670
+ if (!outcome.alreadyUpToDate && headBeforeMerge && refreshed.headSha !== headBeforeMerge) {
1671
+ try {
1672
+ const changedPaths = await this.git.pathsChangedBetween(workspaceId, headBeforeMerge, refreshed.headSha);
1673
+ const carried = await this.reviewWorkflow.carryApprovalsForward(number, headBeforeMerge, refreshed.headSha, changedPaths);
1674
+ // The detail above was assembled before those rows existed, so its
1675
+ // `approvals` (and the merge gate derived from them) still describe
1676
+ // the pre-merge head. Only re-read when something actually carried.
1677
+ if (carried > 0) {
1678
+ this.prs.invalidateDetailCache(number);
1679
+ const withApprovals = await this.prs.getPrDetail(number, {
1680
+ fresh: true,
1681
+ workspaceId,
1682
+ viewerEmail: user.email,
1683
+ });
1684
+ if (withApprovals)
1685
+ return withApprovals;
1686
+ }
1687
+ }
1688
+ catch (err) {
1689
+ log.warn(`could not carry approvals forward on change request #${number} after updating from target`, { err });
1690
+ }
1691
+ }
1240
1692
  return refreshed;
1241
1693
  }
1242
1694
  listComments(number) {
@@ -1257,7 +1709,17 @@ export class WorkflowService {
1257
1709
  this.prs.invalidateDetailCache(number);
1258
1710
  }
1259
1711
  async approveFile(number, path, user, files, headSha, baseBranch, authorIdHash, workspaceId) {
1712
+ // Taken BEFORE the approval lands (see `clearApplyFailure`).
1713
+ const approvedAfter = new Date();
1260
1714
  const approvals = await this.reviewWorkflow.approveFile(number, path, user, files, headSha, baseBranch, authorIdHash, workspaceId);
1715
+ // A recorded approval can answer a GATE refusal only — a conflict or a git
1716
+ // error still describes the request — and only once the gate that refused
1717
+ // would now pass: approving one file of several leaves the rest waiting,
1718
+ // and the refusal naming them still stands. Warnings count too, since an
1719
+ // apply without bypass is refused on them.
1720
+ if (this.gateWouldPass(number, approvals)) {
1721
+ await this.clearApplyFailure(number, { kinds: ['gate'], recordedBefore: approvedAfter });
1722
+ }
1261
1723
  this.prs.invalidateDetailCache(number);
1262
1724
  this.events?.emit({
1263
1725
  kind: 'approval-changed',
@@ -1280,6 +1742,26 @@ export class WorkflowService {
1280
1742
  });
1281
1743
  return approvals;
1282
1744
  }
1745
+ /**
1746
+ * The folder placeholder a file's revert takes along, or null. When a
1747
+ * request removed a folder's last file, the placeholder that keeps the
1748
+ * folder came with it (git may even pair them as one rename); reverting the
1749
+ * file alone would leave that hidden placeholder as a change nobody can
1750
+ * see. So it is reverted too — when the request changed it, and only when
1751
+ * that cannot leave the folder with nothing in git: either the file comes
1752
+ * back, or the base had the placeholder to restore.
1753
+ */
1754
+ async placeholderRevertedWith(workspaceId, mergeBase, repoRelPath, changedPaths) {
1755
+ if (isFolderPlaceholder(repoRelPath))
1756
+ return null;
1757
+ const slash = repoRelPath.lastIndexOf('/');
1758
+ const placeholder = folderPlaceholderPath(slash === -1 ? '' : repoRelPath.slice(0, slash));
1759
+ if (!changedPaths.includes(placeholder))
1760
+ return null;
1761
+ if (await this.git.pathExistsAtRef(workspaceId, mergeBase, repoRelPath))
1762
+ return placeholder;
1763
+ return (await this.git.pathExistsAtRef(workspaceId, mergeBase, placeholder)) ? placeholder : null;
1764
+ }
1283
1765
  /**
1284
1766
  * Decline ONE file of an open change request: restore its merge-base
1285
1767
  * version on the SOURCE branch (commit + push), so the file drops out of
@@ -1306,12 +1788,14 @@ export class WorkflowService {
1306
1788
  }
1307
1789
  const baseBranch = summary.base;
1308
1790
  const headBranch = summary.branch;
1791
+ // Taken BEFORE the head moves (see `clearApplyFailure`).
1792
+ const headMovedAfter = new Date();
1309
1793
  const ws = await this.workspaceService.getOrCreateForBranch(headBranch);
1310
1794
  // Best-effort freshen of the source checkout: the diff below reads origin
1311
1795
  // refs, but the restore commits from the working tree — a stale tree
1312
1796
  // would push non-fast-forward and fail loudly anyway; this just makes
1313
1797
  // that rare.
1314
- await this.git.pull(ws.id).catch(() => undefined);
1798
+ await this.pullWorkspace(ws.id).catch(() => undefined);
1315
1799
  // The verb acts on the request as it is NOW — never a cached file list.
1316
1800
  const paths = await this.git.changedPathsForPr(ws.id, baseBranch, headBranch);
1317
1801
  if (!paths.includes(repoRelPath)) {
@@ -1325,32 +1809,310 @@ export class WorkflowService {
1325
1809
  if (!mergeBase) {
1326
1810
  throw new WorkflowDomainError('These branches share no history to revert to.', 422);
1327
1811
  }
1812
+ const placeholder = await this.placeholderRevertedWith(ws.id, mergeBase, repoRelPath, paths);
1813
+ const reverted = placeholder ? [repoRelPath, placeholder] : [repoRelPath];
1328
1814
  // Same per-file lock every other editor of this path takes — a concurrent
1329
1815
  // save must not race the restore between write and commit.
1330
- const lockPath = `${this.kbDirName}/${repoRelPath}`;
1331
- const lock = await this.fileLocks.acquire(ws.id, headBranch, lockPath, user);
1332
- if (!lock.acquired) {
1333
- throw new WorkflowDomainError(`${repoRelPath} is being edited by ${lock.lock.holderName} — try again once the edit settles.`, 409);
1334
- }
1816
+ const held = [];
1817
+ let revertError = null;
1335
1818
  try {
1336
- await this.git.restorePathFromRef(ws.id, mergeBase, repoRelPath);
1337
- await this.git.commitFile(ws.id, user, repoRelPath, `Revert ${repoRelPath} (declined in change request #${number})`, true);
1819
+ for (const p of reverted) {
1820
+ const lockPath = `${this.kbDirName}/${p}`;
1821
+ const lock = await this.fileLocks.acquire(ws.id, headBranch, lockPath, user);
1822
+ if (!lock.acquired) {
1823
+ throw new WorkflowDomainError(p === repoRelPath
1824
+ ? `${repoRelPath} is being edited by ${lock.lock.holderName} — try again once the edit settles.`
1825
+ : `The folder of ${repoRelPath} is being changed by ${lock.lock.holderName} — try again once the edit settles.`, 409);
1826
+ }
1827
+ held.push(lockPath);
1828
+ }
1829
+ for (const p of reverted) {
1830
+ await this.git.restorePathFromRef(ws.id, mergeBase, p);
1831
+ await this.git.commitFile(ws.id, user, p, `Revert ${p} (declined in change request #${number})`, true);
1832
+ }
1338
1833
  await this.trackedPush(ws.id, user);
1339
1834
  }
1340
- finally {
1341
- // Committed inline — drop the lock row directly rather than enqueueing
1342
- // a duplicate commit through releaseLock.
1343
- await this.fileLocks.release(ws.id, headBranch, lockPath, user);
1344
- }
1835
+ catch (err) {
1836
+ revertError = { reason: err };
1837
+ }
1838
+ // Committed inline — drop the lock rows directly rather than enqueueing
1839
+ // a duplicate commit through releaseLock. Each is released on its own, on
1840
+ // success and failure alike: one failed release must not leave the other
1841
+ // lock held. The revert's own error wins; otherwise a failed release
1842
+ // still surfaces, as a single release's did.
1843
+ const released = await Promise.allSettled(held.map((lockPath) => this.fileLocks.release(ws.id, headBranch, lockPath, user)));
1844
+ if (revertError)
1845
+ throw revertError.reason;
1846
+ const failedRelease = released.find((r) => r.status === 'rejected');
1847
+ if (failedRelease)
1848
+ throw failedRelease.reason;
1849
+ // The source head moved: the refusal described a revision that is gone.
1850
+ await this.clearApplyFailure(number, { recordedBefore: headMovedAfter });
1345
1851
  this.prs.invalidateDetailCache(number);
1346
1852
  const remaining = await this.git.changedPathsForPr(ws.id, baseBranch, headBranch);
1347
1853
  if (remaining.length > 0) {
1348
- return { closed: false, remainingPaths: remaining };
1854
+ // Answered like the file list: the placeholder is not a remaining file.
1855
+ return { closed: false, remainingPaths: remaining.filter((p) => !isFolderPlaceholder(p)) };
1349
1856
  }
1350
1857
  // Every change declined → the request proposes nothing.
1351
1858
  await this.closeEmptyChangeRequest(number, user);
1352
1859
  return { closed: true, remainingPaths: [] };
1353
1860
  }
1861
+ /**
1862
+ * The open change requests that propose files under a KB folder, and
1863
+ * whether the caller may take those files out of each — what a folder
1864
+ * delete asks before it offers "Delete folder and its proposed changes".
1865
+ *
1866
+ * The caller may act on a request they authored, on any request as an
1867
+ * admin, or on a request whose every proposed file under the folder they
1868
+ * may write — per file, not at the folder's top: a subfolder can deny a
1869
+ * writer of its parent. The last two are judged on `origin/<base>`, like
1870
+ * every other request verb. An access answer that cannot be resolved
1871
+ * (null) is a no.
1872
+ */
1873
+ async changeRequestsUnderFolder(folder, user) {
1874
+ const prefix = folderPrefix(folder);
1875
+ const open = await this.prs.listOpenPrs({ fresh: true });
1876
+ // The list's touched paths are best-effort: a diff it could not read comes
1877
+ // back EMPTY, indistinguishable from a request proposing nothing. This
1878
+ // answer gates a destructive action, so a request listed empty is read
1879
+ // again here, and one whose diff still cannot be read fails the question
1880
+ // rather than dropping out of it.
1881
+ const resolved = await Promise.all(open.map(async (cr) => {
1882
+ if (cr.touchedNodePaths.length > 0)
1883
+ return { cr, touched: cr.touchedNodePaths };
1884
+ try {
1885
+ const ws = await this.workspaceService.getOrCreateForBranch(cr.branch);
1886
+ return { cr, touched: await this.git.changedPathsForPr(ws.id, cr.base, cr.branch) };
1887
+ }
1888
+ catch (err) {
1889
+ log.warn(`changedPathsForPr failed for #${cr.number} under folder ${printable(folder)}: ${printable(sanitizeError(err))}`);
1890
+ throw new WorkflowDomainError(`Couldn't read what change request #${cr.number} proposes — try again in a moment.`, 500);
1891
+ }
1892
+ }));
1893
+ const touching = resolved
1894
+ .map(({ cr, touched }) => ({ cr, paths: touched.filter((p) => p.startsWith(prefix)) }))
1895
+ .filter((t) => t.paths.length > 0);
1896
+ if (touching.length === 0)
1897
+ return [];
1898
+ const callerHash = hashEmail(user.email);
1899
+ // One admin answer per base branch; the requests almost always share one.
1900
+ const baseWorkspaces = new Map();
1901
+ const baseWorkspace = (base) => {
1902
+ let ws = baseWorkspaces.get(base);
1903
+ if (!ws) {
1904
+ ws = this.workspaceService.getOrCreateForBranch(base).then((w) => w.id);
1905
+ baseWorkspaces.set(base, ws);
1906
+ }
1907
+ return ws;
1908
+ };
1909
+ const adminByBase = new Map();
1910
+ const adminOn = (base) => {
1911
+ let admin = adminByBase.get(base);
1912
+ if (!admin) {
1913
+ admin = baseWorkspace(base).then(async (wsId) => (await this.accessControl.canWriteAtRef(wsId, `origin/${base}`, user.email, 'roles.yaml')) === true);
1914
+ adminByBase.set(base, admin);
1915
+ }
1916
+ return admin;
1917
+ };
1918
+ const writerOfAll = async (base, paths) => {
1919
+ const answers = await this.accessControl.canWriteBatchAtRef(await baseWorkspace(base), `origin/${base}`, user.email, paths);
1920
+ return answers !== null && paths.every((p) => answers.get(p) === true);
1921
+ };
1922
+ return Promise.all(touching.map(async ({ cr, paths }) => {
1923
+ const mine = !!cr.authorId && cr.authorId === callerHash;
1924
+ const listed = {
1925
+ number: cr.number,
1926
+ title: cr.title,
1927
+ authorName: cr.appAuthor?.name ?? cr.author.name ?? null,
1928
+ mine,
1929
+ paths,
1930
+ };
1931
+ if (mine || (await adminOn(cr.base)) || (await writerOfAll(cr.base, paths))) {
1932
+ return { ...listed, mayRemove: true };
1933
+ }
1934
+ return {
1935
+ ...listed,
1936
+ mayRemove: false,
1937
+ reason: `#${cr.number} was proposed by ${cr.appAuthor?.name ?? 'someone else'}; only its author, an admin or someone who can write every file it proposes here can change it.`,
1938
+ };
1939
+ }));
1940
+ }
1941
+ /**
1942
+ * Take every file under `folder` out of every open change request that
1943
+ * proposes one — the request half of "Delete folder and its proposed
1944
+ * changes". All-or-nothing on permission: when the caller may not act on
1945
+ * even one of the requests, nothing is touched. A request left proposing
1946
+ * nothing is withdrawn (closed, its branch retired), exactly as declining
1947
+ * its last file would.
1948
+ */
1949
+ async removeFolderFromChangeRequests(folder, user) {
1950
+ const requests = await this.changeRequestsUnderFolder(folder, user);
1951
+ const refused = requests.filter((r) => !r.mayRemove);
1952
+ if (refused.length > 0) {
1953
+ throw new WorkflowDomainError(`You can't remove proposed changes from ${refused.map((r) => `#${r.number}`).join(', ')} — only the author, an admin or someone who can write every file it proposes here can.`, 403);
1954
+ }
1955
+ const prefix = folderPrefix(folder);
1956
+ // Everything that can refuse happens before the first commit, so a
1957
+ // refusal leaves every request as it was: each request's source checkout
1958
+ // brought up to date (a failed pull stops here — restoring into a stale
1959
+ // or conflicted tree could commit the wrong content), its paths read as
1960
+ // they are NOW rather than as the list saw them, its merge base found,
1961
+ // and every file's lock held.
1962
+ const plans = [];
1963
+ for (const request of requests) {
1964
+ const summary = await this.prs.getPr(request.number);
1965
+ if (!summary || summary.state !== 'open')
1966
+ continue;
1967
+ const ws = await this.workspaceService.getOrCreateForBranch(summary.branch);
1968
+ try {
1969
+ await this.pullWorkspace(ws.id);
1970
+ }
1971
+ catch (err) {
1972
+ log.warn(`pull failed for #${summary.number} before removing folder ${printable(folder)}: ${printable(sanitizeError(err))}`);
1973
+ throw new WorkflowDomainError(`#${summary.number} could not be brought up to date, so nothing was removed — try again in a moment.`, 409);
1974
+ }
1975
+ const paths = (await this.git.changedPathsForPr(ws.id, summary.base, summary.branch)).filter((p) => p.startsWith(prefix));
1976
+ // The permission above was judged on the paths the request proposed
1977
+ // then. One proposed since was never judged — a writer of every earlier
1978
+ // file need not be one of it — so the request has to be asked about again.
1979
+ const unjudged = paths.filter((p) => !request.paths.includes(p));
1980
+ if (unjudged.length > 0) {
1981
+ throw new WorkflowDomainError(`#${summary.number} changed while the folder was being deleted (${unjudged.length === 1 ? unjudged[0] : `${unjudged.length} new files`}), so no proposed changes were removed — try again.`, 409);
1982
+ }
1983
+ let mergeBase = null;
1984
+ if (paths.length > 0) {
1985
+ mergeBase = await this.git.mergeBaseForPr(ws.id, summary.base, summary.branch);
1986
+ if (!mergeBase) {
1987
+ throw new WorkflowDomainError(`#${summary.number} shares no history with its target to revert to.`, 422);
1988
+ }
1989
+ }
1990
+ plans.push({ number: summary.number, branch: summary.branch, wsId: ws.id, paths, mergeBase });
1991
+ }
1992
+ // Requests can share a source branch, and so a workspace and even a file.
1993
+ // The work is grouped per workspace: each file is locked, restored and
1994
+ // committed once, and each branch pushed once. One restore can only undo a
1995
+ // shared file for requests that agree on what it reverts TO — requests on
1996
+ // one branch aimed at targets with different merge bases do not, and one
1997
+ // of them would be reported emptied while still proposing the file. That
1998
+ // combination is refused here, before anything is locked or changed.
1999
+ const byWorkspace = new Map();
2000
+ for (const plan of plans) {
2001
+ let group = byWorkspace.get(plan.wsId);
2002
+ if (!group) {
2003
+ group = { wsId: plan.wsId, branch: plan.branch, plans: [], paths: [] };
2004
+ byWorkspace.set(plan.wsId, group);
2005
+ }
2006
+ group.plans.push(plan);
2007
+ for (const repoRelPath of plan.paths) {
2008
+ const seen = group.paths.find((p) => p.path === repoRelPath);
2009
+ if (!seen) {
2010
+ group.paths.push({ path: repoRelPath, mergeBase: plan.mergeBase });
2011
+ }
2012
+ else if (seen.mergeBase !== plan.mergeBase) {
2013
+ const sharing = group.plans.map((other) => `#${other.number}`).join(', ');
2014
+ throw new WorkflowDomainError(`${sharing} propose ${repoRelPath} from the same branch against different targets, so it can't be taken out of both at once. Decline it in each request instead.`, 422);
2015
+ }
2016
+ }
2017
+ }
2018
+ const groups = [...byWorkspace.values()];
2019
+ const toChange = groups.filter((g) => g.paths.length > 0);
2020
+ const held = [];
2021
+ // Every held lock is let go, each on its own: one failed release must not
2022
+ // keep the rest held until they expire.
2023
+ const releaseAll = async () => {
2024
+ for (const h of held.splice(0)) {
2025
+ try {
2026
+ await this.fileLocks.release(h.wsId, h.branch, h.lockPath, user);
2027
+ }
2028
+ catch (err) {
2029
+ log.warn(`lock release failed for ${printable(h.lockPath)}: ${printable(sanitizeError(err))}`);
2030
+ }
2031
+ }
2032
+ };
2033
+ const pushed = [];
2034
+ let pushFailure = null;
2035
+ try {
2036
+ for (const group of toChange) {
2037
+ for (const { path: repoRelPath } of group.paths) {
2038
+ const lockPath = `${this.kbDirName}/${repoRelPath}`;
2039
+ const lock = await this.fileLocks.acquire(group.wsId, group.branch, lockPath, user);
2040
+ if (!lock.acquired) {
2041
+ throw new WorkflowDomainError(`${repoRelPath} is being edited by ${lock.lock.holderName} — try again once the edit settles.`, 409);
2042
+ }
2043
+ held.push({ wsId: group.wsId, branch: group.branch, lockPath });
2044
+ }
2045
+ }
2046
+ // The git half is ONE operation under one reservation of every checkout
2047
+ // involved: it records each HEAD, commits every revert before pushing
2048
+ // anything, pushes branch by branch, and undoes what didn't publish (see
2049
+ // `GitService.revertPathsAndPush`). A failed restore or commit throws
2050
+ // with every request left as it was. Pushes can't be made atomic, so a
2051
+ // failed push leaves the requests split into "done" and "untouched", and
2052
+ // running the action again finishes the rest.
2053
+ if (toChange.length > 0) {
2054
+ const outcome = await this.git.revertPathsAndPush(user, toChange.map((group) => {
2055
+ const numbers = group.plans.map((plan) => `#${plan.number}`).join(', ');
2056
+ return {
2057
+ workspaceId: group.wsId,
2058
+ paths: group.paths.map(({ path: repoRelPath, mergeBase }) => ({
2059
+ path: repoRelPath,
2060
+ ref: mergeBase,
2061
+ subject: commitSubject(`Revert ${repoRelPath} (folder deleted; removed from change request ${numbers})`, `Revert ${basenameOf(repoRelPath)} (folder deleted; removed from ${group.plans.length === 1 ? `change request ${numbers}` : `${group.plans.length} change requests`})`),
2062
+ undoSubject: commitSubject(`Undo revert of ${repoRelPath} (folder removal stopped)`, `Undo revert of ${basenameOf(repoRelPath)} (folder removal stopped)`),
2063
+ })),
2064
+ };
2065
+ }));
2066
+ for (const group of toChange) {
2067
+ if (outcome.pushed.includes(group.wsId)) {
2068
+ this.noteGitSyncOk(group.wsId, group.branch);
2069
+ pushed.push(group);
2070
+ for (const plan of group.plans)
2071
+ this.prs.invalidateDetailCache(plan.number);
2072
+ }
2073
+ else if (outcome.failed?.workspaceId === group.wsId) {
2074
+ this.noteGitSyncFailed(group.wsId, group.branch, outcome.failed.error);
2075
+ pushFailure = { group, err: outcome.failed.error };
2076
+ }
2077
+ }
2078
+ }
2079
+ }
2080
+ finally {
2081
+ await releaseAll();
2082
+ }
2083
+ const finished = (pushFailure ? pushed : groups).flatMap((group) => group.plans);
2084
+ if (pushFailure && finished.length === 0)
2085
+ throw pushFailure.err;
2086
+ const results = [];
2087
+ for (const plan of finished) {
2088
+ const withdrawn = await this.closeEmptyChangeRequest(plan.number, user);
2089
+ // A request still open is read once more, to say why: a file under the
2090
+ // folder proposed while this ran (never judged, so left alone), or —
2091
+ // proposing nothing at all — a save still landing kept it open.
2092
+ let stillProposed = [];
2093
+ let keptForSaves = false;
2094
+ if (!withdrawn) {
2095
+ try {
2096
+ const summary = await this.prs.getPr(plan.number);
2097
+ if (summary?.state === 'open') {
2098
+ const now = await this.git.changedPathsForPr(plan.wsId, summary.base, summary.branch);
2099
+ stillProposed = now.filter((p) => p.startsWith(prefix) && !isFolderPlaceholder(p));
2100
+ keptForSaves = now.length === 0;
2101
+ }
2102
+ }
2103
+ catch (err) {
2104
+ log.warn(`could not re-read #${plan.number} after removing folder ${printable(folder)}: ${printable(sanitizeError(err))}`);
2105
+ }
2106
+ }
2107
+ results.push({ number: plan.number, removedPaths: plan.paths, withdrawn, stillProposed, keptForSaves });
2108
+ }
2109
+ if (pushFailure) {
2110
+ const failed = pushFailure.group.plans.map((plan) => `#${plan.number}`).join(', ');
2111
+ log.warn(`push failed for ${failed} removing folder ${printable(folder)}: ${printable(sanitizeError(pushFailure.err))}`);
2112
+ throw new WorkflowDomainError(`The folder's files were removed from ${finished.map((plan) => `#${plan.number}`).join(', ')}, but ${failed} could not be updated and it and any later requests were left as they were. Run the delete again to finish.`, 500);
2113
+ }
2114
+ return results;
2115
+ }
1354
2116
  /**
1355
2117
  * DELETE a change request outright: close it (whatever its diff says) and
1356
2118
  * retire its source branch — the request, its proposal, and the branch that
@@ -1444,6 +2206,12 @@ export class WorkflowService {
1444
2206
  * is AUTHORITATIVE — recomputed, and a diff failure aborts rather than
1445
2207
  * closes, so a transient git error can never eat a live request.
1446
2208
  *
2209
+ * An empty diff is not yet an empty request while a save to its branch is
2210
+ * still landing: one under a held lock or queued for the commit worker is
2211
+ * on disk but not in the diff. Retiring the branch deletes its checkout and
2212
+ * that save with it, so such a request stays open — the save commits and it
2213
+ * proposes something again, or a later look closes it.
2214
+ *
1447
2215
  * Returns true when THIS call closed it.
1448
2216
  */
1449
2217
  async closeEmptyChangeRequest(number, user) {
@@ -1451,16 +2219,20 @@ export class WorkflowService {
1451
2219
  if (!summary || summary.state !== 'open')
1452
2220
  return false;
1453
2221
  let paths;
2222
+ let wsId;
1454
2223
  try {
1455
2224
  const ws = await this.workspaceService.getOrCreateForBranch(summary.branch);
2225
+ wsId = ws.id;
1456
2226
  paths = await this.git.changedPathsForPr(ws.id, summary.base, summary.branch);
1457
2227
  }
1458
2228
  catch (err) {
1459
- console.warn(`[cr] empty-check for change request #${number} failed — leaving it open:`, err);
2229
+ crLog.warn(`empty-check for change request #${number} failed — leaving it open:`, { err });
1460
2230
  return false;
1461
2231
  }
1462
2232
  if (paths.length > 0)
1463
2233
  return false;
2234
+ if (await this.savesInFlight(wsId))
2235
+ return false;
1464
2236
  // Guard on `state = 'open'` so a concurrent merge or withdraw wins the
1465
2237
  // race and this becomes a no-op.
1466
2238
  const now = new Date();
@@ -1476,6 +2248,21 @@ export class WorkflowService {
1476
2248
  await this.retireMergedSourceBranch(number, summary.base, user);
1477
2249
  return true;
1478
2250
  }
2251
+ /**
2252
+ * Whether a save to this branch's checkout is still landing: a live file
2253
+ * lock (the write is mid-flight) or any queued commit, stuck ones included
2254
+ * (their file is still only on disk). An answer that cannot be read is a
2255
+ * yes — the caller is about to delete the checkout.
2256
+ */
2257
+ async savesInFlight(wsId) {
2258
+ try {
2259
+ return (await this.fileLocks.hasAnyActive(wsId)) || (await this.pendingCommits.hasAnyForWorkspace(wsId));
2260
+ }
2261
+ catch (err) {
2262
+ crLog.warn(`could not tell whether saves are landing on ${printable(wsId)} — keeping its request open:`, { err });
2263
+ return true;
2264
+ }
2265
+ }
1479
2266
  /**
1480
2267
  * Close every open change request either of whose branches no longer exists.
1481
2268
  *
@@ -1540,13 +2327,13 @@ export class WorkflowService {
1540
2327
  live = new Set(branches.map((b) => b.name));
1541
2328
  }
1542
2329
  catch (err) {
1543
- console.warn('[cr] branch sweep skipped — could not list branches:', err);
2330
+ crLog.warn('branch sweep skipped — could not list branches:', { err });
1544
2331
  return 0;
1545
2332
  }
1546
2333
  // An empty branch list means something is wrong with the clone, not that
1547
2334
  // every branch in the repo was deleted at once. Refuse to act on it.
1548
2335
  if (live.size === 0) {
1549
- console.warn('[cr] branch sweep skipped — branch list came back empty');
2336
+ crLog.warn('branch sweep skipped — branch list came back empty');
1550
2337
  return 0;
1551
2338
  }
1552
2339
  let closed = 0;
@@ -1573,7 +2360,7 @@ export class WorkflowService {
1573
2360
  continue;
1574
2361
  this.prs.invalidateDetailCache(cr.number);
1575
2362
  this.events?.emit({ kind: 'change-request-rejected', number: cr.number });
1576
- console.log(`[cr] closed change request #${cr.number}: branch "${missing}" no longer exists`);
2363
+ crLog.info(`closed change request #${cr.number}: branch "${missing}" no longer exists`);
1577
2364
  closed++;
1578
2365
  }
1579
2366
  return closed;
@@ -1599,10 +2386,10 @@ export class WorkflowService {
1599
2386
  this.sweepKick = this.closeChangeRequestsWithDeletedBranches()
1600
2387
  .then((n) => {
1601
2388
  if (n > 0) {
1602
- console.log(`[cr] on-demand sweep closed ${n} stranded change request${n === 1 ? '' : 's'}`);
2389
+ crLog.info(`on-demand sweep closed ${n} stranded change request${n === 1 ? '' : 's'}`);
1603
2390
  }
1604
2391
  })
1605
- .catch((err) => console.warn('[cr] on-demand deleted-branch sweep failed:', err))
2392
+ .catch((err) => crLog.warn('on-demand deleted-branch sweep failed:', { err }))
1606
2393
  .finally(() => {
1607
2394
  this.sweepKick = null;
1608
2395
  });
@@ -1693,7 +2480,7 @@ export class WorkflowService {
1693
2480
  // roles.yaml differs from the base branch's, we restore the base version on
1694
2481
  // the source branch (commit + push) BEFORE merging, so the merged diff
1695
2482
  // carries no roles.yaml change. roles.yaml is mutable ONLY via the admin
1696
- // Roles & Members surface (itself admin-gated). The rest of the CR merges
2483
+ // App roles surface (itself admin-gated). The rest of the CR merges
1697
2484
  // normally. Best-effort by design is NOT acceptable here — a failure to
1698
2485
  // neutralise must abort the merge, never fall through.
1699
2486
  const preserved = await this.preserveBaseRolesYaml(number, user, baseBranch);
@@ -1730,17 +2517,30 @@ export class WorkflowService {
1730
2517
  }
1731
2518
  throw err;
1732
2519
  }
1733
- // The merge landed on `origin/<baseBranch>`. Pull the
1734
- // TARGET branch's own workspace so its working tree doesn't fall behind the
1735
- // remote — the file tools serve the working tree (not origin), so without
1736
- // this a read right after a merge misses the just-merged change. Best-effort:
1737
- // the merge already succeeded on origin, so a pull hiccup must not fail the
1738
- // response (a later fetch/pull reconciles).
2520
+ await this.pullMergeTarget(baseBranch, user);
2521
+ this.prs.invalidateDetailCache(number);
2522
+ this.events?.emit({ kind: 'change-request-merged', number });
2523
+ // AFTER the event: the applying UI is waiting on `change-request-merged`,
2524
+ // and branch retirement is git IO it must never wait behind.
2525
+ await this.retireMergedSourceBranch(number, baseBranch, user);
2526
+ return { kind: 'merged', result };
2527
+ }
2528
+ /**
2529
+ * The merge landed on `origin/<targetBranch>`. Pull the TARGET branch's own
2530
+ * workspace so its working tree doesn't fall behind the remote — the file
2531
+ * tools serve the working tree (not origin), so without this a read right
2532
+ * after a merge misses the just-merged change. Best-effort: the merge already
2533
+ * succeeded on origin, so a pull hiccup must not fail the response (a later
2534
+ * fetch/pull reconciles).
2535
+ */
2536
+ async pullMergeTarget(targetBranch, user) {
1739
2537
  let targetWorkspaceId;
2538
+ /** Whether the pull below already announced the new tree. See the emit after it. */
2539
+ let announced = false;
1740
2540
  try {
1741
- const targetWorkspace = await this.workspaceService.getOrCreateForBranch(baseBranch);
2541
+ const targetWorkspace = await this.workspaceService.getOrCreateForBranch(targetBranch);
1742
2542
  targetWorkspaceId = targetWorkspace.id;
1743
- await this.git.pull(targetWorkspace.id);
2543
+ announced = await this.pullWorkspace(targetWorkspace.id);
1744
2544
  }
1745
2545
  catch (err) {
1746
2546
  // Still best-effort for the merge response (the merge already landed
@@ -1752,14 +2552,243 @@ export class WorkflowService {
1752
2552
  if (err instanceof PullRebaseConflictError && targetWorkspaceId) {
1753
2553
  await this.queuePullConflictRecovery(targetWorkspaceId, err, user);
1754
2554
  }
1755
- console.warn(`[merge] post-merge pull of target "${baseBranch}" failed — its workspace may be momentarily behind origin`, err);
2555
+ mergeLog.warn(`post-merge pull of target "${targetBranch}" failed — its workspace may be momentarily behind origin`, {
2556
+ err,
2557
+ });
1756
2558
  }
2559
+ // THE MERGE ITSELF CHANGED THE TREE, and the pull above almost never says
2560
+ // so. Both callers — applying a change request, and an agent merging a
2561
+ // branch — run `git merge --no-ff` in this very workspace and push the
2562
+ // result, so by the time we pull, the clone is already at origin: the pull
2563
+ // reports "up to date", `treeChanged` is false, and nothing is announced —
2564
+ // while the working tree holds a `.tool` or a `SKILL.md` that was not
2565
+ // there a second ago.
2566
+ //
2567
+ // Nothing else covers it. `change-request-merged` carries no branch and no
2568
+ // workspace, so the catalog subscriber cannot act on it; a later
2569
+ // `POST /api/sync/<branch>` answers "up-to-date" without pulling, for the
2570
+ // same reason. So an approved tool stayed invisible — to every open MCP
2571
+ // connection and every browser — until some unrelated write to the default
2572
+ // branch happened to drop the caches, or the catalogs' own TTL ran out.
2573
+ // That is the merge path of "whichever path made the commit".
2574
+ //
2575
+ // Emitted only when the pull did not already: on the rare path where the
2576
+ // clone WAS behind (someone else pushed between the merge and the pull),
2577
+ // the pull announced the same tree and a second event buys a duplicate
2578
+ // re-scan and a duplicate browser refetch.
2579
+ //
2580
+ // A pull that FAILED is still announced, and that is not a guess about a
2581
+ // tree nobody reconciled. The merge ran in THIS workspace — the target
2582
+ // branch's own, resolved above — before pushing, so the merged bytes were
2583
+ // on that disk before the pull was attempted, and a pull that could not
2584
+ // run cannot un-merge them. (For a change request, `targetBranch` is the
2585
+ // branch the merge acted on: `preserveBaseRolesYaml` refuses the merge
2586
+ // outright if it disagrees with the request's own base.) The failure the
2587
+ // pull reports is about OTHER people's commits not arriving, which leaves
2588
+ // the catalogs no staler than they were.
2589
+ if (!announced && targetWorkspaceId && targetBranch === DEFAULT_BRANCH) {
2590
+ this.events?.emit({ kind: 'fs-tree-changed', workspaceId: targetWorkspaceId, branch: DEFAULT_BRANCH });
2591
+ }
2592
+ }
2593
+ async mergeBranch(user, sourceBranch, targetBranch) {
2594
+ if (sourceBranch === targetBranch) {
2595
+ throw new WorkflowValidationError(`\`source\` and \`target\` are both "${sourceBranch}" — name two different branches.`);
2596
+ }
2597
+ // Under the lifecycle lock of BOTH branches, the same protocol
2598
+ // `openChangeRequest` follows — and for the same window. That method holds
2599
+ // these keys across its whole check-then-insert (an auto-merge and a push,
2600
+ // seconds of it), so the "no open request" answer below is only sound
2601
+ // while they are held: a bare read would let a person open the request
2602
+ // mid-merge and the agent would land its content anyway, which is the one
2603
+ // thing this tool exists to prevent. `runOnBranchPair` drops a protected
2604
+ // target's key; the source key is held either way, and every competing
2605
+ // request from this source takes it too.
2606
+ return this.runOnBranchPair(sourceBranch, targetBranch, async () => {
2607
+ // A person merges a change request. Only the request's OWN direction is
2608
+ // blocked: the target merged into the source is how a draft under review
2609
+ // stays current, and that must keep working while the request is open.
2610
+ const [open] = await this.db
2611
+ .select({ number: changeRequests.number })
2612
+ .from(changeRequests)
2613
+ .where(and(eq(changeRequests.sourceBranch, sourceBranch), eq(changeRequests.targetBranch, targetBranch), eq(changeRequests.state, 'open')))
2614
+ .limit(1);
2615
+ if (open)
2616
+ throw new OpenChangeRequestBlocksMergeError(sourceBranch, targetBranch, open.number);
2617
+ // The merge runs in the target's own workspace, like a change request's.
2618
+ const targetWorkspaceId = (await this.workspaceService.getOrCreateForBranch(targetBranch)).id;
2619
+ if (!(await this.git.remoteBranchExists(targetWorkspaceId, sourceBranch))) {
2620
+ throw new WorkflowValidationError(`No branch named "${sourceBranch}" on the shared remote.`);
2621
+ }
2622
+ const result = await this.git.mergeChangeRequest(targetWorkspaceId, sourceBranch, targetBranch, {
2623
+ subject: `Merge ${sourceBranch} into ${targetBranch}`,
2624
+ body: `Merged via Bevel by ${user.name} <${user.email}>`,
2625
+ }, user, {
2626
+ // The merge resets that workspace to the published target tip, which
2627
+ // would throw away edits still on their way out. Refuse until they
2628
+ // are shared — asked inside the merge's own reservation, so a save
2629
+ // cannot land between the question and the reset.
2630
+ requireCleanTarget: true,
2631
+ // Into a protected branch, a merge is a commit of every file it
2632
+ // changes: allowed only for a caller who could commit each of them
2633
+ // directly (the same at-HEAD rule the write lock applies). The hook
2634
+ // runs inside that reservation too, against the target tip this
2635
+ // merge is built on rather than a workspace HEAD that may be behind
2636
+ // it — so the rules read are the ones the merge is about to change.
2637
+ //
2638
+ // `changedPaths` is the authorizing set: roles.yaml kept (unlike a
2639
+ // change request's merge, nothing strips it here — so its presence
2640
+ // is what the hook refuses on) and both sides of a rename, since the
2641
+ // old name is a file this merge deletes.
2642
+ authorize: isProtectedBranch(targetBranch)
2643
+ ? async ({ sha, changedPaths }) => {
2644
+ if (changedPaths.length === 0)
2645
+ return;
2646
+ // roles.yaml never changes through a merge. A change request's
2647
+ // merge restores the target's copy on the source first
2648
+ // (`preserveBaseRolesYaml`): a draft is a free-for-all, and the
2649
+ // merge is what lands its content on a protected branch. This
2650
+ // path lands draft content the same way, so it refuses rather
2651
+ // than authorizing — even an admin, who could write the file
2652
+ // directly, gets the app's roles surface for that, where the
2653
+ // file is validated (parsable, groups that exist); the agent's
2654
+ // own write tools refuse it under the same guard. A merged
2655
+ // roles.yaml would bypass both, on the one file the access
2656
+ // model cannot survive being wrong.
2657
+ if (changedPaths.includes('roles.yaml')) {
2658
+ throw new WorkflowDomainError(`"${targetBranch}" is protected and this merge would change its roles.yaml. ` +
2659
+ `Roles never change through a merge: restore roles.yaml on "${sourceBranch}" to the version on ` +
2660
+ `"${targetBranch}" and merge again, and ask the user to change roles in the app.`, 403, { kind: 'protected-merge-changes-roles', targetBranch, sourceBranch });
2661
+ }
2662
+ const allowed = await this.accessControl.canWriteBatchAtRef(targetWorkspaceId, sha, user.email, changedPaths);
2663
+ const denied = allowed ? changedPaths.filter((p) => !allowed.get(p)) : [];
2664
+ if (denied.length === 0)
2665
+ return;
2666
+ throw new WorkflowDomainError(`"${targetBranch}" is protected and you cannot commit directly to it: ` +
2667
+ `${denied.length} changed file(s) are outside your write access (${denied.join(', ')}). ` +
2668
+ `Open a change request from "${sourceBranch}" into "${targetBranch}" and ask the user to review it in the app.`, 403, { kind: 'protected-merge-target', targetBranch, deniedPaths: denied });
2669
+ }
2670
+ : undefined,
2671
+ });
2672
+ if (result.kind === 'conflicts') {
2673
+ return { kind: 'conflicts-need-resolution', conflictedPaths: result.paths };
2674
+ }
2675
+ await this.pullMergeTarget(targetBranch, user);
2676
+ return { kind: 'merged', sha: result.sha };
2677
+ });
2678
+ }
2679
+ /**
2680
+ * Start an apply attempt on `number` and return its token. Only the latest
2681
+ * attempt may record a refusal: when two people apply the same request at
2682
+ * once, an older attempt that finishes last must not overwrite the newer
2683
+ * one's verdict. In-process, like the detail and list caches this reads with.
2684
+ * Every attempt must be ended with `endApplyAttempt`.
2685
+ */
2686
+ beginApplyAttempt(number) {
2687
+ const attempt = ++this.applyAttemptSeq;
2688
+ this.applyAttempts.set(number, attempt);
2689
+ return attempt;
2690
+ }
2691
+ /**
2692
+ * An attempt finished. Its entry leaves only if no newer attempt replaced it;
2693
+ * an older attempt ending later then finds no entry and records nothing,
2694
+ * because its token can never match one issued again.
2695
+ */
2696
+ endApplyAttempt(number, attempt) {
2697
+ if (this.applyAttempts.get(number) === attempt)
2698
+ this.applyAttempts.delete(number);
2699
+ }
2700
+ /**
2701
+ * Persist why an apply did not land and tell every session. The apply route
2702
+ * already answers the clicker directly (user-scoped `merge-failed`); this is
2703
+ * for everyone else who can see the still-open request — above all its
2704
+ * author, who otherwise sees it pending forever with no word of the refusal.
2705
+ * The event carries only the number: the reason is read back through the
2706
+ * list and detail endpoints, so a session that misses the event gets the
2707
+ * same answer on its next fetch.
2708
+ *
2709
+ * Returns false, writing and announcing nothing, when a newer attempt has
2710
+ * started since `attempt` or the request is no longer open (a concurrent
2711
+ * apply landed it) — a refusal nobody can act on must not reach anyone.
2712
+ */
2713
+ async recordApplyFailure(number, failure, user, attempt) {
2714
+ if (this.applyAttempts.get(number) !== attempt)
2715
+ return false;
2716
+ const at = failure.at ?? new Date();
2717
+ const updated = await this.db
2718
+ .update(changeRequests)
2719
+ .set({
2720
+ applyFailureReason: sanitizeError(failure.reason, { maxLen: APPLY_FAILURE_MAX_LEN }),
2721
+ applyFailureConflicts: failure.kind === 'conflicts',
2722
+ applyFailureKind: failure.kind,
2723
+ applyFailedAt: at,
2724
+ applyFailedByName: user.name,
2725
+ })
2726
+ // The write itself refuses to go backwards: the attempt map above is an
2727
+ // early exit within this process, but an older UPDATE still in flight (or
2728
+ // one from another replica) must never replace a newer refusal.
2729
+ .where(and(eq(changeRequests.number, number), eq(changeRequests.state, 'open'), or(isNull(changeRequests.applyFailedAt), lt(changeRequests.applyFailedAt, at))))
2730
+ .returning({ number: changeRequests.number });
2731
+ if (updated.length === 0)
2732
+ return false;
1757
2733
  this.prs.invalidateDetailCache(number);
1758
- this.events?.emit({ kind: 'change-request-merged', number });
1759
- // AFTER the event: the applying UI is waiting on `change-request-merged`,
1760
- // and branch retirement is git IO it must never wait behind.
1761
- await this.retireMergedSourceBranch(number, baseBranch, user);
1762
- return { kind: 'merged', result };
2734
+ this.events?.emit({ kind: 'change-request-apply-failed', number });
2735
+ return true;
2736
+ }
2737
+ /**
2738
+ * Forget a request's recorded refusal because a change made it obsolete.
2739
+ * Otherwise the request keeps saying "<name> could not apply this: Waiting on
2740
+ * approval…" after the approval arrived, with nothing dating it.
2741
+ *
2742
+ * WHICH refusals a change makes obsolete is `kinds`: an approval answers only
2743
+ * the gate — a conflict or a git error is untouched by it and must keep
2744
+ * saying so — while a moved source head replaces the revision every kind of
2745
+ * refusal described (omit `kinds` to clear any). A row with no recorded kind
2746
+ * is only cleared by the latter. `recordedBefore` is taken before the change
2747
+ * starts: a refusal recorded after it is newer than the change and stays. Announced on the same event a new refusal
2748
+ * uses — every list and open dialog re-reads the request. Best-effort: the
2749
+ * mutation that triggered it already happened and must not fail on this.
2750
+ */
2751
+ /**
2752
+ * Whether the merge gate would now let an apply through without bypass — no
2753
+ * hard reason, no warning. Part of the best-effort clear: an evaluation that
2754
+ * fails answers "no" (the refusal stays) instead of failing the approval.
2755
+ */
2756
+ gateWouldPass(number, approvals) {
2757
+ try {
2758
+ const gate = this.reviewWorkflow.evaluateMergeGate({ prNumber: number, state: 'open', approvals });
2759
+ return gate?.mergeable === true && gate.warnings.length === 0;
2760
+ }
2761
+ catch (err) {
2762
+ crLog.warn(`could not re-evaluate the merge gate of change request #${number}; keeping its refusal:`, { err });
2763
+ return false;
2764
+ }
2765
+ }
2766
+ async clearApplyFailure(number, scope) {
2767
+ const { kinds, recordedBefore } = scope;
2768
+ try {
2769
+ const cleared = await this.db
2770
+ .update(changeRequests)
2771
+ .set({
2772
+ applyFailureReason: null,
2773
+ applyFailureConflicts: null,
2774
+ applyFailedAt: null,
2775
+ applyFailedByName: null,
2776
+ applyFailureKind: null,
2777
+ })
2778
+ .where(and(eq(changeRequests.number, number), isNotNull(changeRequests.applyFailedAt),
2779
+ // Only a refusal that PREDATES the change: one an apply recorded
2780
+ // while the change was landing may already have seen it, and is
2781
+ // the newer verdict — never erase it.
2782
+ lt(changeRequests.applyFailedAt, recordedBefore), kinds ? inArray(changeRequests.applyFailureKind, [...kinds]) : undefined))
2783
+ .returning({ number: changeRequests.number });
2784
+ if (cleared.length === 0)
2785
+ return;
2786
+ this.prs.invalidateDetailCache(number);
2787
+ this.events?.emit({ kind: 'change-request-apply-failed', number });
2788
+ }
2789
+ catch (err) {
2790
+ crLog.warn(`could not clear the recorded apply failure of change request #${number}:`, { err });
2791
+ }
1763
2792
  }
1764
2793
  /**
1765
2794
  * Best-effort retirement of a merged change request's source branch — the
@@ -1806,11 +2835,13 @@ export class WorkflowService {
1806
2835
  // "does origin still have it?" probe skips the remote delete.
1807
2836
  await this.workspaceService.ensureRemotesFetched(targetWs.id).catch(() => { });
1808
2837
  await this.deleteBranchUnlocked(targetWs.id, sourceBranch, user, { systemCleanup: true });
1809
- console.log(`[merge] retired merged source branch "${sourceBranch}"`);
2838
+ mergeLog.info(`retired merged source branch "${sourceBranch}"`);
1810
2839
  });
1811
2840
  }
1812
2841
  catch (err) {
1813
- console.warn(`[merge] could not retire the merged source branch of change request #${number} (non-fatal):`, err);
2842
+ mergeLog.warn(`could not retire the merged source branch of change request #${number} (non-fatal):`, {
2843
+ err,
2844
+ });
1814
2845
  }
1815
2846
  }
1816
2847
  /**
@@ -1916,7 +2947,7 @@ export class WorkflowService {
1916
2947
  const detail = err instanceof Error ? err.message : String(err);
1917
2948
  // Log the raw git/push detail server-side; the thrown error keeps it OFF
1918
2949
  // the client-facing 502 message (see RolesYamlPreservationError).
1919
- console.warn(`[merge] roles.yaml preservation failed for #${number}:`, detail);
2950
+ mergeLog.warn(`roles.yaml preservation failed for #${number}:`, { detail });
1920
2951
  throw new RolesYamlPreservationError(detail);
1921
2952
  }
1922
2953
  }
@@ -1933,7 +2964,9 @@ export class WorkflowService {
1933
2964
  return stdout.split('\n').map((s) => s.trim()).filter(Boolean);
1934
2965
  }
1935
2966
  catch (err) {
1936
- console.warn('[workflow] listChangedPathsBetweenBranches failed:', redactTokens(err instanceof Error ? err.message : String(err)));
2967
+ log.warn('listChangedPathsBetweenBranches failed:', {
2968
+ detail: redactTokens(err instanceof Error ? err.message : String(err)),
2969
+ });
1937
2970
  return [];
1938
2971
  }
1939
2972
  }
@@ -1958,7 +2991,7 @@ export class WorkflowService {
1958
2991
  resolved = await this.accessControl.eligibleWritersForPathsAtRef(workspaceId, `origin/${targetBranch}`, paths);
1959
2992
  }
1960
2993
  catch (err) {
1961
- console.warn('[workflow] eligibleWritersForPathsAtRef failed:', err instanceof Error ? err.message : String(err));
2994
+ log.warn('eligibleWritersForPathsAtRef failed:', { err });
1962
2995
  }
1963
2996
  if (!resolved || resolved.size === 0)
1964
2997
  return '';