@bevel-software/platform-core-backend 0.1.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 (720) hide show
  1. package/LICENSE +202 -0
  2. package/dist/assets.d.ts +5 -0
  3. package/dist/assets.d.ts.map +1 -0
  4. package/dist/assets.js +23 -0
  5. package/dist/assets.js.map +1 -0
  6. package/dist/core/core-ports.d.ts +83 -0
  7. package/dist/core/core-ports.d.ts.map +1 -0
  8. package/dist/core/core-ports.js +12 -0
  9. package/dist/core/core-ports.js.map +1 -0
  10. package/dist/core/create-core-server.d.ts +62 -0
  11. package/dist/core/create-core-server.d.ts.map +1 -0
  12. package/dist/core/create-core-server.js +204 -0
  13. package/dist/core/create-core-server.js.map +1 -0
  14. package/dist/core/create-core-services.d.ts +95 -0
  15. package/dist/core/create-core-services.d.ts.map +1 -0
  16. package/dist/core/create-core-services.js +352 -0
  17. package/dist/core/create-core-services.js.map +1 -0
  18. package/dist/core-config.d.ts +119 -0
  19. package/dist/core-config.d.ts.map +1 -0
  20. package/dist/core-config.js +240 -0
  21. package/dist/core-config.js.map +1 -0
  22. package/dist/index.d.ts +36 -0
  23. package/dist/index.d.ts.map +1 -0
  24. package/dist/index.js +36 -0
  25. package/dist/index.js.map +1 -0
  26. package/dist/modules/access/access-control.interface.d.ts +348 -0
  27. package/dist/modules/access/access-control.interface.d.ts.map +1 -0
  28. package/dist/modules/access/access-control.interface.js +2 -0
  29. package/dist/modules/access/access-control.interface.js.map +1 -0
  30. package/dist/modules/access/access-control.service.d.ts +323 -0
  31. package/dist/modules/access/access-control.service.d.ts.map +1 -0
  32. package/dist/modules/access/access-control.service.js +1431 -0
  33. package/dist/modules/access/access-control.service.js.map +1 -0
  34. package/dist/modules/access/access-errors.d.ts +34 -0
  35. package/dist/modules/access/access-errors.d.ts.map +1 -0
  36. package/dist/modules/access/access-errors.js +40 -0
  37. package/dist/modules/access/access-errors.js.map +1 -0
  38. package/dist/modules/access/access-mutation.service.d.ts +176 -0
  39. package/dist/modules/access/access-mutation.service.d.ts.map +1 -0
  40. package/dist/modules/access/access-mutation.service.js +307 -0
  41. package/dist/modules/access/access-mutation.service.js.map +1 -0
  42. package/dist/modules/access/access-splice.d.ts +98 -0
  43. package/dist/modules/access/access-splice.d.ts.map +1 -0
  44. package/dist/modules/access/access-splice.js +346 -0
  45. package/dist/modules/access/access-splice.js.map +1 -0
  46. package/dist/modules/access/access.routes.d.ts +10 -0
  47. package/dist/modules/access/access.routes.d.ts.map +1 -0
  48. package/dist/modules/access/access.routes.js +757 -0
  49. package/dist/modules/access/access.routes.js.map +1 -0
  50. package/dist/modules/access/creator-access.d.ts +113 -0
  51. package/dist/modules/access/creator-access.d.ts.map +1 -0
  52. package/dist/modules/access/creator-access.js +212 -0
  53. package/dist/modules/access/creator-access.js.map +1 -0
  54. package/dist/modules/access/kb-read-filter.d.ts +41 -0
  55. package/dist/modules/access/kb-read-filter.d.ts.map +1 -0
  56. package/dist/modules/access/kb-read-filter.js +60 -0
  57. package/dist/modules/access/kb-read-filter.js.map +1 -0
  58. package/dist/modules/access/roles-admin.service.d.ts +240 -0
  59. package/dist/modules/access/roles-admin.service.d.ts.map +1 -0
  60. package/dist/modules/access/roles-admin.service.js +630 -0
  61. package/dist/modules/access/roles-admin.service.js.map +1 -0
  62. package/dist/modules/access/roles-edit.d.ts +91 -0
  63. package/dist/modules/access/roles-edit.d.ts.map +1 -0
  64. package/dist/modules/access/roles-edit.js +232 -0
  65. package/dist/modules/access/roles-edit.js.map +1 -0
  66. package/dist/modules/access/roles-yaml-guard.d.ts +56 -0
  67. package/dist/modules/access/roles-yaml-guard.d.ts.map +1 -0
  68. package/dist/modules/access/roles-yaml-guard.js +79 -0
  69. package/dist/modules/access/roles-yaml-guard.js.map +1 -0
  70. package/dist/modules/admin/admin-access.routes.d.ts +18 -0
  71. package/dist/modules/admin/admin-access.routes.d.ts.map +1 -0
  72. package/dist/modules/admin/admin-access.routes.js +26 -0
  73. package/dist/modules/admin/admin-access.routes.js.map +1 -0
  74. package/dist/modules/admin/admin-access.service.d.ts +24 -0
  75. package/dist/modules/admin/admin-access.service.d.ts.map +1 -0
  76. package/dist/modules/admin/admin-access.service.js +41 -0
  77. package/dist/modules/admin/admin-access.service.js.map +1 -0
  78. package/dist/modules/admin/admin.interface.d.ts +11 -0
  79. package/dist/modules/admin/admin.interface.d.ts.map +1 -0
  80. package/dist/modules/admin/admin.interface.js +2 -0
  81. package/dist/modules/admin/admin.interface.js.map +1 -0
  82. package/dist/modules/auth/account-erasure.service.d.ts +80 -0
  83. package/dist/modules/auth/account-erasure.service.d.ts.map +1 -0
  84. package/dist/modules/auth/account-erasure.service.js +91 -0
  85. package/dist/modules/auth/account-erasure.service.js.map +1 -0
  86. package/dist/modules/auth/auth.middleware.d.ts +23 -0
  87. package/dist/modules/auth/auth.middleware.d.ts.map +1 -0
  88. package/dist/modules/auth/auth.middleware.js +68 -0
  89. package/dist/modules/auth/auth.middleware.js.map +1 -0
  90. package/dist/modules/auth/auth.routes.d.ts +25 -0
  91. package/dist/modules/auth/auth.routes.d.ts.map +1 -0
  92. package/dist/modules/auth/auth.routes.js +72 -0
  93. package/dist/modules/auth/auth.routes.js.map +1 -0
  94. package/dist/modules/auth/auth.service.d.ts +76 -0
  95. package/dist/modules/auth/auth.service.d.ts.map +1 -0
  96. package/dist/modules/auth/auth.service.js +168 -0
  97. package/dist/modules/auth/auth.service.js.map +1 -0
  98. package/dist/modules/code-mode/code-mode-names.d.ts +16 -0
  99. package/dist/modules/code-mode/code-mode-names.d.ts.map +1 -0
  100. package/dist/modules/code-mode/code-mode-names.js +30 -0
  101. package/dist/modules/code-mode/code-mode-names.js.map +1 -0
  102. package/dist/modules/code-mode/code-mode.tool.d.ts +13 -0
  103. package/dist/modules/code-mode/code-mode.tool.d.ts.map +1 -0
  104. package/dist/modules/code-mode/code-mode.tool.js +112 -0
  105. package/dist/modules/code-mode/code-mode.tool.js.map +1 -0
  106. package/dist/modules/code-mode/index.d.ts +3 -0
  107. package/dist/modules/code-mode/index.d.ts.map +1 -0
  108. package/dist/modules/code-mode/index.js +3 -0
  109. package/dist/modules/code-mode/index.js.map +1 -0
  110. package/dist/modules/database/connection.d.ts +6 -0
  111. package/dist/modules/database/connection.d.ts.map +1 -0
  112. package/dist/modules/database/connection.js +12 -0
  113. package/dist/modules/database/connection.js.map +1 -0
  114. package/dist/modules/database/core-schema.d.ts +2273 -0
  115. package/dist/modules/database/core-schema.d.ts.map +1 -0
  116. package/dist/modules/database/core-schema.js +413 -0
  117. package/dist/modules/database/core-schema.js.map +1 -0
  118. package/dist/modules/database/migrate.d.ts +10 -0
  119. package/dist/modules/database/migrate.d.ts.map +1 -0
  120. package/dist/modules/database/migrate.js +40 -0
  121. package/dist/modules/database/migrate.js.map +1 -0
  122. package/dist/modules/database/schema.d.ts +12 -0
  123. package/dist/modules/database/schema.d.ts.map +1 -0
  124. package/dist/modules/database/schema.js +12 -0
  125. package/dist/modules/database/schema.js.map +1 -0
  126. package/dist/modules/diff/diff-paths.d.ts +2 -0
  127. package/dist/modules/diff/diff-paths.d.ts.map +1 -0
  128. package/dist/modules/diff/diff-paths.js +9 -0
  129. package/dist/modules/diff/diff-paths.js.map +1 -0
  130. package/dist/modules/diff/diff.config.d.ts +8 -0
  131. package/dist/modules/diff/diff.config.d.ts.map +1 -0
  132. package/dist/modules/diff/diff.config.js +13 -0
  133. package/dist/modules/diff/diff.config.js.map +1 -0
  134. package/dist/modules/diff/diff.interface.d.ts +73 -0
  135. package/dist/modules/diff/diff.interface.d.ts.map +1 -0
  136. package/dist/modules/diff/diff.interface.js +2 -0
  137. package/dist/modules/diff/diff.interface.js.map +1 -0
  138. package/dist/modules/diff/diff.routes.d.ts +35 -0
  139. package/dist/modules/diff/diff.routes.d.ts.map +1 -0
  140. package/dist/modules/diff/diff.routes.js +228 -0
  141. package/dist/modules/diff/diff.routes.js.map +1 -0
  142. package/dist/modules/diff/diff.service.d.ts +51 -0
  143. package/dist/modules/diff/diff.service.d.ts.map +1 -0
  144. package/dist/modules/diff/diff.service.js +489 -0
  145. package/dist/modules/diff/diff.service.js.map +1 -0
  146. package/dist/modules/diff/line-diff.d.ts +16 -0
  147. package/dist/modules/diff/line-diff.d.ts.map +1 -0
  148. package/dist/modules/diff/line-diff.js +67 -0
  149. package/dist/modules/diff/line-diff.js.map +1 -0
  150. package/dist/modules/mcp/mcp-auth.middleware.d.ts +27 -0
  151. package/dist/modules/mcp/mcp-auth.middleware.d.ts.map +1 -0
  152. package/dist/modules/mcp/mcp-auth.middleware.js +116 -0
  153. package/dist/modules/mcp/mcp-auth.middleware.js.map +1 -0
  154. package/dist/modules/mcp/mcp-session-store.d.ts +74 -0
  155. package/dist/modules/mcp/mcp-session-store.d.ts.map +1 -0
  156. package/dist/modules/mcp/mcp-session-store.js +131 -0
  157. package/dist/modules/mcp/mcp-session-store.js.map +1 -0
  158. package/dist/modules/mcp/mcp.routes.d.ts +24 -0
  159. package/dist/modules/mcp/mcp.routes.d.ts.map +1 -0
  160. package/dist/modules/mcp/mcp.routes.js +229 -0
  161. package/dist/modules/mcp/mcp.routes.js.map +1 -0
  162. package/dist/modules/mcp/mcp.service.d.ts +198 -0
  163. package/dist/modules/mcp/mcp.service.d.ts.map +1 -0
  164. package/dist/modules/mcp/mcp.service.js +840 -0
  165. package/dist/modules/mcp/mcp.service.js.map +1 -0
  166. package/dist/modules/mcp/oauth/bevel-oauth-provider.d.ts +76 -0
  167. package/dist/modules/mcp/oauth/bevel-oauth-provider.d.ts.map +1 -0
  168. package/dist/modules/mcp/oauth/bevel-oauth-provider.js +291 -0
  169. package/dist/modules/mcp/oauth/bevel-oauth-provider.js.map +1 -0
  170. package/dist/modules/mcp/oauth/oauth-consent.routes.d.ts +18 -0
  171. package/dist/modules/mcp/oauth/oauth-consent.routes.d.ts.map +1 -0
  172. package/dist/modules/mcp/oauth/oauth-consent.routes.js +52 -0
  173. package/dist/modules/mcp/oauth/oauth-consent.routes.js.map +1 -0
  174. package/dist/modules/mcp/oauth/oauth-state.d.ts +32 -0
  175. package/dist/modules/mcp/oauth/oauth-state.d.ts.map +1 -0
  176. package/dist/modules/mcp/oauth/oauth-state.js +34 -0
  177. package/dist/modules/mcp/oauth/oauth-state.js.map +1 -0
  178. package/dist/modules/secrets-vault/db-secrets-vault.service.d.ts +92 -0
  179. package/dist/modules/secrets-vault/db-secrets-vault.service.d.ts.map +1 -0
  180. package/dist/modules/secrets-vault/db-secrets-vault.service.js +728 -0
  181. package/dist/modules/secrets-vault/db-secrets-vault.service.js.map +1 -0
  182. package/dist/modules/secrets-vault/index.d.ts +7 -0
  183. package/dist/modules/secrets-vault/index.d.ts.map +1 -0
  184. package/dist/modules/secrets-vault/index.js +6 -0
  185. package/dist/modules/secrets-vault/index.js.map +1 -0
  186. package/dist/modules/secrets-vault/mcp-oauth-discovery.service.d.ts +63 -0
  187. package/dist/modules/secrets-vault/mcp-oauth-discovery.service.d.ts.map +1 -0
  188. package/dist/modules/secrets-vault/mcp-oauth-discovery.service.js +269 -0
  189. package/dist/modules/secrets-vault/mcp-oauth-discovery.service.js.map +1 -0
  190. package/dist/modules/secrets-vault/secrets-variable-loader.d.ts +46 -0
  191. package/dist/modules/secrets-vault/secrets-variable-loader.d.ts.map +1 -0
  192. package/dist/modules/secrets-vault/secrets-variable-loader.js +87 -0
  193. package/dist/modules/secrets-vault/secrets-variable-loader.js.map +1 -0
  194. package/dist/modules/secrets-vault/secrets-vault.contract.d.ts +210 -0
  195. package/dist/modules/secrets-vault/secrets-vault.contract.d.ts.map +1 -0
  196. package/dist/modules/secrets-vault/secrets-vault.contract.js +90 -0
  197. package/dist/modules/secrets-vault/secrets-vault.contract.js.map +1 -0
  198. package/dist/modules/secrets-vault/secrets-vault.routes.d.ts +30 -0
  199. package/dist/modules/secrets-vault/secrets-vault.routes.d.ts.map +1 -0
  200. package/dist/modules/secrets-vault/secrets-vault.routes.js +490 -0
  201. package/dist/modules/secrets-vault/secrets-vault.routes.js.map +1 -0
  202. package/dist/modules/skills/index.d.ts +5 -0
  203. package/dist/modules/skills/index.d.ts.map +1 -0
  204. package/dist/modules/skills/index.js +4 -0
  205. package/dist/modules/skills/index.js.map +1 -0
  206. package/dist/modules/skills/skills.contract.d.ts +58 -0
  207. package/dist/modules/skills/skills.contract.d.ts.map +1 -0
  208. package/dist/modules/skills/skills.contract.js +10 -0
  209. package/dist/modules/skills/skills.contract.js.map +1 -0
  210. package/dist/modules/skills/skills.routes.d.ts +15 -0
  211. package/dist/modules/skills/skills.routes.d.ts.map +1 -0
  212. package/dist/modules/skills/skills.routes.js +34 -0
  213. package/dist/modules/skills/skills.routes.js.map +1 -0
  214. package/dist/modules/skills/skills.service.d.ts +21 -0
  215. package/dist/modules/skills/skills.service.d.ts.map +1 -0
  216. package/dist/modules/skills/skills.service.js +202 -0
  217. package/dist/modules/skills/skills.service.js.map +1 -0
  218. package/dist/modules/skills/skills.tools.d.ts +15 -0
  219. package/dist/modules/skills/skills.tools.d.ts.map +1 -0
  220. package/dist/modules/skills/skills.tools.js +96 -0
  221. package/dist/modules/skills/skills.tools.js.map +1 -0
  222. package/dist/modules/tool-auth/external-api-key.errors.d.ts +14 -0
  223. package/dist/modules/tool-auth/external-api-key.errors.d.ts.map +1 -0
  224. package/dist/modules/tool-auth/external-api-key.errors.js +23 -0
  225. package/dist/modules/tool-auth/external-api-key.errors.js.map +1 -0
  226. package/dist/modules/tool-auth/external-api-key.interface.d.ts +90 -0
  227. package/dist/modules/tool-auth/external-api-key.interface.d.ts.map +1 -0
  228. package/dist/modules/tool-auth/external-api-key.interface.js +2 -0
  229. package/dist/modules/tool-auth/external-api-key.interface.js.map +1 -0
  230. package/dist/modules/tool-auth/external-api-key.service.d.ts +25 -0
  231. package/dist/modules/tool-auth/external-api-key.service.d.ts.map +1 -0
  232. package/dist/modules/tool-auth/external-api-key.service.js +164 -0
  233. package/dist/modules/tool-auth/external-api-key.service.js.map +1 -0
  234. package/dist/modules/tool-auth/internal-token.service.d.ts +62 -0
  235. package/dist/modules/tool-auth/internal-token.service.d.ts.map +1 -0
  236. package/dist/modules/tool-auth/internal-token.service.js +85 -0
  237. package/dist/modules/tool-auth/internal-token.service.js.map +1 -0
  238. package/dist/modules/tool-auth/llm-usage-meter.d.ts +16 -0
  239. package/dist/modules/tool-auth/llm-usage-meter.d.ts.map +1 -0
  240. package/dist/modules/tool-auth/llm-usage-meter.js +6 -0
  241. package/dist/modules/tool-auth/llm-usage-meter.js.map +1 -0
  242. package/dist/modules/tool-auth/tool-auth.middleware.d.ts +101 -0
  243. package/dist/modules/tool-auth/tool-auth.middleware.d.ts.map +1 -0
  244. package/dist/modules/tool-auth/tool-auth.middleware.js +166 -0
  245. package/dist/modules/tool-auth/tool-auth.middleware.js.map +1 -0
  246. package/dist/modules/tool-helpers/index.d.ts +19 -0
  247. package/dist/modules/tool-helpers/index.d.ts.map +1 -0
  248. package/dist/modules/tool-helpers/index.js +18 -0
  249. package/dist/modules/tool-helpers/index.js.map +1 -0
  250. package/dist/modules/tool-helpers/tool-context.d.ts +35 -0
  251. package/dist/modules/tool-helpers/tool-context.d.ts.map +1 -0
  252. package/dist/modules/tool-helpers/tool-context.js +66 -0
  253. package/dist/modules/tool-helpers/tool-context.js.map +1 -0
  254. package/dist/modules/tool-helpers/tool-def.d.ts +33 -0
  255. package/dist/modules/tool-helpers/tool-def.d.ts.map +1 -0
  256. package/dist/modules/tool-helpers/tool-def.js +57 -0
  257. package/dist/modules/tool-helpers/tool-def.js.map +1 -0
  258. package/dist/modules/tool-helpers/tool-handler.d.ts +19 -0
  259. package/dist/modules/tool-helpers/tool-handler.d.ts.map +1 -0
  260. package/dist/modules/tool-helpers/tool-handler.js +84 -0
  261. package/dist/modules/tool-helpers/tool-handler.js.map +1 -0
  262. package/dist/modules/tool-helpers/tool.contract.d.ts +64 -0
  263. package/dist/modules/tool-helpers/tool.contract.d.ts.map +1 -0
  264. package/dist/modules/tool-helpers/tool.contract.js +24 -0
  265. package/dist/modules/tool-helpers/tool.contract.js.map +1 -0
  266. package/dist/modules/tool-helpers/validate-token.d.ts +31 -0
  267. package/dist/modules/tool-helpers/validate-token.d.ts.map +1 -0
  268. package/dist/modules/tool-helpers/validate-token.js +20 -0
  269. package/dist/modules/tool-helpers/validate-token.js.map +1 -0
  270. package/dist/modules/tool-manuals/index.d.ts +5 -0
  271. package/dist/modules/tool-manuals/index.d.ts.map +1 -0
  272. package/dist/modules/tool-manuals/index.js +5 -0
  273. package/dist/modules/tool-manuals/index.js.map +1 -0
  274. package/dist/modules/tool-manuals/tool-manuals.contract.d.ts +182 -0
  275. package/dist/modules/tool-manuals/tool-manuals.contract.d.ts.map +1 -0
  276. package/dist/modules/tool-manuals/tool-manuals.contract.js +19 -0
  277. package/dist/modules/tool-manuals/tool-manuals.contract.js.map +1 -0
  278. package/dist/modules/tool-manuals/tool-manuals.routes.d.ts +24 -0
  279. package/dist/modules/tool-manuals/tool-manuals.routes.d.ts.map +1 -0
  280. package/dist/modules/tool-manuals/tool-manuals.routes.js +102 -0
  281. package/dist/modules/tool-manuals/tool-manuals.routes.js.map +1 -0
  282. package/dist/modules/tool-manuals/tool-manuals.service.d.ts +108 -0
  283. package/dist/modules/tool-manuals/tool-manuals.service.d.ts.map +1 -0
  284. package/dist/modules/tool-manuals/tool-manuals.service.js +710 -0
  285. package/dist/modules/tool-manuals/tool-manuals.service.js.map +1 -0
  286. package/dist/modules/tool-manuals/tool-manuals.tools.d.ts +35 -0
  287. package/dist/modules/tool-manuals/tool-manuals.tools.d.ts.map +1 -0
  288. package/dist/modules/tool-manuals/tool-manuals.tools.js +157 -0
  289. package/dist/modules/tool-manuals/tool-manuals.tools.js.map +1 -0
  290. package/dist/modules/tool-registry/manual.routes.d.ts +6 -0
  291. package/dist/modules/tool-registry/manual.routes.d.ts.map +1 -0
  292. package/dist/modules/tool-registry/manual.routes.js +76 -0
  293. package/dist/modules/tool-registry/manual.routes.js.map +1 -0
  294. package/dist/modules/tool-registry/tool-registry.d.ts +23 -0
  295. package/dist/modules/tool-registry/tool-registry.d.ts.map +1 -0
  296. package/dist/modules/tool-registry/tool-registry.js +46 -0
  297. package/dist/modules/tool-registry/tool-registry.js.map +1 -0
  298. package/dist/modules/tool-registry/tool.contract.d.ts +48 -0
  299. package/dist/modules/tool-registry/tool.contract.d.ts.map +1 -0
  300. package/dist/modules/tool-registry/tool.contract.js +2 -0
  301. package/dist/modules/tool-registry/tool.contract.js.map +1 -0
  302. package/dist/modules/workflow/agent-tools/workflow.tools.d.ts +14 -0
  303. package/dist/modules/workflow/agent-tools/workflow.tools.d.ts.map +1 -0
  304. package/dist/modules/workflow/agent-tools/workflow.tools.js +437 -0
  305. package/dist/modules/workflow/agent-tools/workflow.tools.js.map +1 -0
  306. package/dist/modules/workflow/event-bus.d.ts +107 -0
  307. package/dist/modules/workflow/event-bus.d.ts.map +1 -0
  308. package/dist/modules/workflow/event-bus.js +224 -0
  309. package/dist/modules/workflow/event-bus.js.map +1 -0
  310. package/dist/modules/workflow/events.routes.d.ts +80 -0
  311. package/dist/modules/workflow/events.routes.d.ts.map +1 -0
  312. package/dist/modules/workflow/events.routes.js +216 -0
  313. package/dist/modules/workflow/events.routes.js.map +1 -0
  314. package/dist/modules/workflow/file-change-notifier.d.ts +38 -0
  315. package/dist/modules/workflow/file-change-notifier.d.ts.map +1 -0
  316. package/dist/modules/workflow/file-change-notifier.js +22 -0
  317. package/dist/modules/workflow/file-change-notifier.js.map +1 -0
  318. package/dist/modules/workflow/file-lock.service.d.ts +57 -0
  319. package/dist/modules/workflow/file-lock.service.d.ts.map +1 -0
  320. package/dist/modules/workflow/file-lock.service.js +202 -0
  321. package/dist/modules/workflow/file-lock.service.js.map +1 -0
  322. package/dist/modules/workflow/git/branch-name.d.ts +10 -0
  323. package/dist/modules/workflow/git/branch-name.d.ts.map +1 -0
  324. package/dist/modules/workflow/git/branch-name.js +76 -0
  325. package/dist/modules/workflow/git/branch-name.js.map +1 -0
  326. package/dist/modules/workflow/git/clone-config.d.ts +61 -0
  327. package/dist/modules/workflow/git/clone-config.d.ts.map +1 -0
  328. package/dist/modules/workflow/git/clone-config.js +69 -0
  329. package/dist/modules/workflow/git/clone-config.js.map +1 -0
  330. package/dist/modules/workflow/git/git-version.d.ts +34 -0
  331. package/dist/modules/workflow/git/git-version.d.ts.map +1 -0
  332. package/dist/modules/workflow/git/git-version.js +63 -0
  333. package/dist/modules/workflow/git/git-version.js.map +1 -0
  334. package/dist/modules/workflow/git/git.service.d.ts +427 -0
  335. package/dist/modules/workflow/git/git.service.d.ts.map +1 -0
  336. package/dist/modules/workflow/git/git.service.js +1987 -0
  337. package/dist/modules/workflow/git/git.service.js.map +1 -0
  338. package/dist/modules/workflow/git/mutex.d.ts +11 -0
  339. package/dist/modules/workflow/git/mutex.d.ts.map +1 -0
  340. package/dist/modules/workflow/git/mutex.js +23 -0
  341. package/dist/modules/workflow/git/mutex.js.map +1 -0
  342. package/dist/modules/workflow/git/pull-request.service.d.ts +124 -0
  343. package/dist/modules/workflow/git/pull-request.service.d.ts.map +1 -0
  344. package/dist/modules/workflow/git/pull-request.service.js +328 -0
  345. package/dist/modules/workflow/git/pull-request.service.js.map +1 -0
  346. package/dist/modules/workflow/locking-filesystem.d.ts +133 -0
  347. package/dist/modules/workflow/locking-filesystem.d.ts.map +1 -0
  348. package/dist/modules/workflow/locking-filesystem.js +400 -0
  349. package/dist/modules/workflow/locking-filesystem.js.map +1 -0
  350. package/dist/modules/workflow/pending-commits.service.d.ts +221 -0
  351. package/dist/modules/workflow/pending-commits.service.d.ts.map +1 -0
  352. package/dist/modules/workflow/pending-commits.service.js +364 -0
  353. package/dist/modules/workflow/pending-commits.service.js.map +1 -0
  354. package/dist/modules/workflow/pending-commits.worker.d.ts +148 -0
  355. package/dist/modules/workflow/pending-commits.worker.d.ts.map +1 -0
  356. package/dist/modules/workflow/pending-commits.worker.js +245 -0
  357. package/dist/modules/workflow/pending-commits.worker.js.map +1 -0
  358. package/dist/modules/workflow/read-only-filesystem.d.ts +24 -0
  359. package/dist/modules/workflow/read-only-filesystem.d.ts.map +1 -0
  360. package/dist/modules/workflow/read-only-filesystem.js +39 -0
  361. package/dist/modules/workflow/read-only-filesystem.js.map +1 -0
  362. package/dist/modules/workflow/recovery-bot.d.ts +31 -0
  363. package/dist/modules/workflow/recovery-bot.d.ts.map +1 -0
  364. package/dist/modules/workflow/recovery-bot.js +64 -0
  365. package/dist/modules/workflow/recovery-bot.js.map +1 -0
  366. package/dist/modules/workflow/review-workflow/review-workflow.interface.d.ts +137 -0
  367. package/dist/modules/workflow/review-workflow/review-workflow.interface.d.ts.map +1 -0
  368. package/dist/modules/workflow/review-workflow/review-workflow.interface.js +2 -0
  369. package/dist/modules/workflow/review-workflow/review-workflow.interface.js.map +1 -0
  370. package/dist/modules/workflow/review-workflow/review-workflow.service.d.ts +26 -0
  371. package/dist/modules/workflow/review-workflow/review-workflow.service.d.ts.map +1 -0
  372. package/dist/modules/workflow/review-workflow/review-workflow.service.js +636 -0
  373. package/dist/modules/workflow/review-workflow/review-workflow.service.js.map +1 -0
  374. package/dist/modules/workflow/sanitize-error.d.ts +29 -0
  375. package/dist/modules/workflow/sanitize-error.d.ts.map +1 -0
  376. package/dist/modules/workflow/sanitize-error.js +60 -0
  377. package/dist/modules/workflow/sanitize-error.js.map +1 -0
  378. package/dist/modules/workflow/session-ontology.policy.d.ts +52 -0
  379. package/dist/modules/workflow/session-ontology.policy.d.ts.map +1 -0
  380. package/dist/modules/workflow/session-ontology.policy.js +62 -0
  381. package/dist/modules/workflow/session-ontology.policy.js.map +1 -0
  382. package/dist/modules/workflow/session-ontology.service.d.ts +100 -0
  383. package/dist/modules/workflow/session-ontology.service.d.ts.map +1 -0
  384. package/dist/modules/workflow/session-ontology.service.js +143 -0
  385. package/dist/modules/workflow/session-ontology.service.js.map +1 -0
  386. package/dist/modules/workflow/workflow-hooks.d.ts +87 -0
  387. package/dist/modules/workflow/workflow-hooks.d.ts.map +1 -0
  388. package/dist/modules/workflow/workflow-hooks.js +35 -0
  389. package/dist/modules/workflow/workflow-hooks.js.map +1 -0
  390. package/dist/modules/workflow/workflow.errors.d.ts +171 -0
  391. package/dist/modules/workflow/workflow.errors.d.ts.map +1 -0
  392. package/dist/modules/workflow/workflow.errors.js +260 -0
  393. package/dist/modules/workflow/workflow.errors.js.map +1 -0
  394. package/dist/modules/workflow/workflow.routes.d.ts +26 -0
  395. package/dist/modules/workflow/workflow.routes.d.ts.map +1 -0
  396. package/dist/modules/workflow/workflow.routes.js +804 -0
  397. package/dist/modules/workflow/workflow.routes.js.map +1 -0
  398. package/dist/modules/workflow/workflow.service.d.ts +385 -0
  399. package/dist/modules/workflow/workflow.service.d.ts.map +1 -0
  400. package/dist/modules/workflow/workflow.service.js +1155 -0
  401. package/dist/modules/workflow/workflow.service.js.map +1 -0
  402. package/dist/modules/workspace/bevel-ignore.d.ts +25 -0
  403. package/dist/modules/workspace/bevel-ignore.d.ts.map +1 -0
  404. package/dist/modules/workspace/bevel-ignore.js +57 -0
  405. package/dist/modules/workspace/bevel-ignore.js.map +1 -0
  406. package/dist/modules/workspace/kb-seed.interface.d.ts +36 -0
  407. package/dist/modules/workspace/kb-seed.interface.d.ts.map +1 -0
  408. package/dist/modules/workspace/kb-seed.interface.js +2 -0
  409. package/dist/modules/workspace/kb-seed.interface.js.map +1 -0
  410. package/dist/modules/workspace/kb-seed.service.d.ts +70 -0
  411. package/dist/modules/workspace/kb-seed.service.d.ts.map +1 -0
  412. package/dist/modules/workspace/kb-seed.service.js +325 -0
  413. package/dist/modules/workspace/kb-seed.service.js.map +1 -0
  414. package/dist/modules/workspace/routine-write-policy.d.ts +49 -0
  415. package/dist/modules/workspace/routine-write-policy.d.ts.map +1 -0
  416. package/dist/modules/workspace/routine-write-policy.js +46 -0
  417. package/dist/modules/workspace/routine-write-policy.js.map +1 -0
  418. package/dist/modules/workspace/session-ontology.gate.d.ts +112 -0
  419. package/dist/modules/workspace/session-ontology.gate.d.ts.map +1 -0
  420. package/dist/modules/workspace/session-ontology.gate.js +161 -0
  421. package/dist/modules/workspace/session-ontology.gate.js.map +1 -0
  422. package/dist/modules/workspace/session-sink.d.ts +25 -0
  423. package/dist/modules/workspace/session-sink.d.ts.map +1 -0
  424. package/dist/modules/workspace/session-sink.js +8 -0
  425. package/dist/modules/workspace/session-sink.js.map +1 -0
  426. package/dist/modules/workspace/spill-store.d.ts +24 -0
  427. package/dist/modules/workspace/spill-store.d.ts.map +1 -0
  428. package/dist/modules/workspace/spill-store.js +87 -0
  429. package/dist/modules/workspace/spill-store.js.map +1 -0
  430. package/dist/modules/workspace/workspace.routes.d.ts +25 -0
  431. package/dist/modules/workspace/workspace.routes.d.ts.map +1 -0
  432. package/dist/modules/workspace/workspace.routes.js +1046 -0
  433. package/dist/modules/workspace/workspace.routes.js.map +1 -0
  434. package/dist/modules/workspace/workspace.service.d.ts +339 -0
  435. package/dist/modules/workspace/workspace.service.d.ts.map +1 -0
  436. package/dist/modules/workspace/workspace.service.js +1221 -0
  437. package/dist/modules/workspace/workspace.service.js.map +1 -0
  438. package/dist/modules/workspace/workspace.tools.d.ts +19 -0
  439. package/dist/modules/workspace/workspace.tools.d.ts.map +1 -0
  440. package/dist/modules/workspace/workspace.tools.js +767 -0
  441. package/dist/modules/workspace/workspace.tools.js.map +1 -0
  442. package/dist/shared/frontmatter-id.d.ts +35 -0
  443. package/dist/shared/frontmatter-id.d.ts.map +1 -0
  444. package/dist/shared/frontmatter-id.js +68 -0
  445. package/dist/shared/frontmatter-id.js.map +1 -0
  446. package/dist/shared/fs-walk.d.ts +9 -0
  447. package/dist/shared/fs-walk.d.ts.map +1 -0
  448. package/dist/shared/fs-walk.js +34 -0
  449. package/dist/shared/fs-walk.js.map +1 -0
  450. package/dist/shared/hash-email.d.ts +11 -0
  451. package/dist/shared/hash-email.d.ts.map +1 -0
  452. package/dist/shared/hash-email.js +14 -0
  453. package/dist/shared/hash-email.js.map +1 -0
  454. package/dist/shared/kb-layout.d.ts +38 -0
  455. package/dist/shared/kb-layout.d.ts.map +1 -0
  456. package/dist/shared/kb-layout.js +102 -0
  457. package/dist/shared/kb-layout.js.map +1 -0
  458. package/dist/shared/kb-layout.test.d.ts +2 -0
  459. package/dist/shared/kb-layout.test.d.ts.map +1 -0
  460. package/dist/shared/kb-layout.test.js +67 -0
  461. package/dist/shared/kb-layout.test.js.map +1 -0
  462. package/dist/shared/ssrf.d.ts +20 -0
  463. package/dist/shared/ssrf.d.ts.map +1 -0
  464. package/dist/shared/ssrf.js +89 -0
  465. package/dist/shared/ssrf.js.map +1 -0
  466. package/dist/shared/token-crypto.d.ts +16 -0
  467. package/dist/shared/token-crypto.d.ts.map +1 -0
  468. package/dist/shared/token-crypto.js +49 -0
  469. package/dist/shared/token-crypto.js.map +1 -0
  470. package/dist/shared/ttl-cache.d.ts +16 -0
  471. package/dist/shared/ttl-cache.d.ts.map +1 -0
  472. package/dist/shared/ttl-cache.js +27 -0
  473. package/dist/shared/ttl-cache.js.map +1 -0
  474. package/dist/shared/utcp-namespace.d.ts +46 -0
  475. package/dist/shared/utcp-namespace.d.ts.map +1 -0
  476. package/dist/shared/utcp-namespace.js +82 -0
  477. package/dist/shared/utcp-namespace.js.map +1 -0
  478. package/dist/version.d.ts +17 -0
  479. package/dist/version.d.ts.map +1 -0
  480. package/dist/version.js +19 -0
  481. package/dist/version.js.map +1 -0
  482. package/kb-template/.bevelignore +21 -0
  483. package/kb-template/Agents/.gitkeep +0 -0
  484. package/kb-template/Agents/README.md +38 -0
  485. package/kb-template/CLAUDE.md +419 -0
  486. package/kb-template/Data/.gitkeep +0 -0
  487. package/kb-template/Data/README.md +39 -0
  488. package/kb-template/KnowledgeBase/.gitkeep +0 -0
  489. package/kb-template/KnowledgeBase/README.md +37 -0
  490. package/kb-template/Pipelines/.gitkeep +0 -0
  491. package/kb-template/Pipelines/README.md +38 -0
  492. package/kb-template/Skills/.gitkeep +0 -0
  493. package/kb-template/Skills/README.md +32 -0
  494. package/kb-template/Tools/.gitkeep +0 -0
  495. package/kb-template/Tools/README.md +42 -0
  496. package/kb-template/access.md +36 -0
  497. package/migrations/0000_core_init.sql +243 -0
  498. package/migrations/meta/0000_snapshot.json +1480 -0
  499. package/migrations/meta/_journal.json +13 -0
  500. package/package.json +80 -0
  501. package/src/assets.ts +25 -0
  502. package/src/core/core-ports.ts +91 -0
  503. package/src/core/create-core-server.ts +338 -0
  504. package/src/core/create-core-services.ts +501 -0
  505. package/src/core-config.ts +257 -0
  506. package/src/index.ts +69 -0
  507. package/src/modules/access/__tests__/access-control.atref-batch.test.ts +98 -0
  508. package/src/modules/access/__tests__/access-control.read-batch.test.ts +132 -0
  509. package/src/modules/access/__tests__/access-control.service.test.ts +1011 -0
  510. package/src/modules/access/__tests__/access-mutation.service.test.ts +439 -0
  511. package/src/modules/access/__tests__/access-splice.test.ts +247 -0
  512. package/src/modules/access/__tests__/access.routes.revoke.test.ts +407 -0
  513. package/src/modules/access/__tests__/creator-access.test.ts +217 -0
  514. package/src/modules/access/__tests__/grant-sources.test.ts +204 -0
  515. package/src/modules/access/__tests__/kb-read-filter.test.ts +70 -0
  516. package/src/modules/access/__tests__/roles-admin.service.test.ts +469 -0
  517. package/src/modules/access/__tests__/roles-edit.test.ts +138 -0
  518. package/src/modules/access/__tests__/roles-yaml-guard.test.ts +70 -0
  519. package/src/modules/access/__tests__/roles.routes.test.ts +234 -0
  520. package/src/modules/access/access-control.interface.ts +383 -0
  521. package/src/modules/access/access-control.service.ts +1794 -0
  522. package/src/modules/access/access-errors.ts +60 -0
  523. package/src/modules/access/access-mutation.service.ts +369 -0
  524. package/src/modules/access/access-splice.ts +413 -0
  525. package/src/modules/access/access.routes.ts +894 -0
  526. package/src/modules/access/creator-access.ts +270 -0
  527. package/src/modules/access/kb-read-filter.ts +76 -0
  528. package/src/modules/access/roles-admin.service.ts +741 -0
  529. package/src/modules/access/roles-edit.ts +248 -0
  530. package/src/modules/access/roles-yaml-guard.ts +89 -0
  531. package/src/modules/admin/admin-access.routes.ts +29 -0
  532. package/src/modules/admin/admin-access.service.ts +38 -0
  533. package/src/modules/admin/admin.interface.ts +10 -0
  534. package/src/modules/auth/account-erasure.service.ts +183 -0
  535. package/src/modules/auth/auth.middleware.ts +78 -0
  536. package/src/modules/auth/auth.routes.ts +97 -0
  537. package/src/modules/auth/auth.service.ts +199 -0
  538. package/src/modules/code-mode/code-mode-names.ts +38 -0
  539. package/src/modules/code-mode/code-mode.tool.ts +117 -0
  540. package/src/modules/code-mode/index.ts +6 -0
  541. package/src/modules/database/connection.ts +15 -0
  542. package/src/modules/database/core-schema.ts +431 -0
  543. package/src/modules/database/migrate.ts +43 -0
  544. package/src/modules/database/schema.ts +11 -0
  545. package/src/modules/diff/__tests__/diff.routes.read-gate.test.ts +87 -0
  546. package/src/modules/diff/__tests__/diff.routes.rejectPathsLocked.test.ts +150 -0
  547. package/src/modules/diff/__tests__/diff.service.seed-atomicity.test.ts +184 -0
  548. package/src/modules/diff/__tests__/diff.service.test.ts +202 -0
  549. package/src/modules/diff/diff-paths.ts +9 -0
  550. package/src/modules/diff/diff.config.ts +12 -0
  551. package/src/modules/diff/diff.interface.ts +78 -0
  552. package/src/modules/diff/diff.routes.ts +277 -0
  553. package/src/modules/diff/diff.service.ts +508 -0
  554. package/src/modules/diff/line-diff.ts +79 -0
  555. package/src/modules/mcp/__tests__/bevel-oauth-provider.test.ts +287 -0
  556. package/src/modules/mcp/__tests__/mcp-auth.middleware.test.ts +249 -0
  557. package/src/modules/mcp/__tests__/mcp-proxy-helpers.test.ts +253 -0
  558. package/src/modules/mcp/__tests__/mcp-session-store.test.ts +151 -0
  559. package/src/modules/mcp/__tests__/mcp.e2e.test.ts +150 -0
  560. package/src/modules/mcp/__tests__/mcp.routes.delete.test.ts +92 -0
  561. package/src/modules/mcp/__tests__/mcp.service.test.ts +414 -0
  562. package/src/modules/mcp/mcp-auth.middleware.ts +126 -0
  563. package/src/modules/mcp/mcp-session-store.ts +162 -0
  564. package/src/modules/mcp/mcp.routes.ts +263 -0
  565. package/src/modules/mcp/mcp.service.ts +973 -0
  566. package/src/modules/mcp/oauth/bevel-oauth-provider.ts +401 -0
  567. package/src/modules/mcp/oauth/oauth-consent.routes.ts +60 -0
  568. package/src/modules/mcp/oauth/oauth-state.ts +61 -0
  569. package/src/modules/secrets-vault/__tests__/connect-pending.route.test.ts +277 -0
  570. package/src/modules/secrets-vault/__tests__/db-secrets-vault.oauth.test.ts +206 -0
  571. package/src/modules/secrets-vault/__tests__/mcp-oauth-discovery.service.test.ts +178 -0
  572. package/src/modules/secrets-vault/__tests__/scopes-covered.test.ts +101 -0
  573. package/src/modules/secrets-vault/__tests__/secrets-variable-loader.test.ts +54 -0
  574. package/src/modules/secrets-vault/__tests__/tool-owner-gate.route.test.ts +143 -0
  575. package/src/modules/secrets-vault/db-secrets-vault.service.ts +792 -0
  576. package/src/modules/secrets-vault/index.ts +25 -0
  577. package/src/modules/secrets-vault/mcp-oauth-discovery.service.ts +289 -0
  578. package/src/modules/secrets-vault/secrets-variable-loader.ts +104 -0
  579. package/src/modules/secrets-vault/secrets-vault.contract.ts +270 -0
  580. package/src/modules/secrets-vault/secrets-vault.routes.ts +526 -0
  581. package/src/modules/skills/__tests__/skills.service.test.ts +176 -0
  582. package/src/modules/skills/index.ts +10 -0
  583. package/src/modules/skills/skills.contract.ts +54 -0
  584. package/src/modules/skills/skills.routes.ts +38 -0
  585. package/src/modules/skills/skills.service.ts +245 -0
  586. package/src/modules/skills/skills.tools.ts +121 -0
  587. package/src/modules/tool-auth/__tests__/external-api-key.service.test.ts +358 -0
  588. package/src/modules/tool-auth/__tests__/internal-token.service.test.ts +79 -0
  589. package/src/modules/tool-auth/__tests__/manual-auth.middleware.test.ts +88 -0
  590. package/src/modules/tool-auth/external-api-key.errors.ts +25 -0
  591. package/src/modules/tool-auth/external-api-key.interface.ts +99 -0
  592. package/src/modules/tool-auth/external-api-key.service.ts +209 -0
  593. package/src/modules/tool-auth/internal-token.service.ts +124 -0
  594. package/src/modules/tool-auth/llm-usage-meter.ts +19 -0
  595. package/src/modules/tool-auth/tool-auth.middleware.ts +220 -0
  596. package/src/modules/tool-helpers/__tests__/phase0-loopback-spike.test.ts +129 -0
  597. package/src/modules/tool-helpers/__tests__/phase4-tools.test.ts +95 -0
  598. package/src/modules/tool-helpers/__tests__/tools-surface.test.ts +146 -0
  599. package/src/modules/tool-helpers/__tests__/validate-token.test.ts +75 -0
  600. package/src/modules/tool-helpers/index.ts +18 -0
  601. package/src/modules/tool-helpers/tool-context.ts +98 -0
  602. package/src/modules/tool-helpers/tool-def.ts +73 -0
  603. package/src/modules/tool-helpers/tool-handler.ts +91 -0
  604. package/src/modules/tool-helpers/tool.contract.ts +85 -0
  605. package/src/modules/tool-helpers/validate-token.ts +50 -0
  606. package/src/modules/tool-manuals/__tests__/tool-manuals.mcp-oauth.test.ts +200 -0
  607. package/src/modules/tool-manuals/__tests__/tool-manuals.service.test.ts +499 -0
  608. package/src/modules/tool-manuals/__tests__/tool-manuals.tools.test.ts +156 -0
  609. package/src/modules/tool-manuals/index.ts +12 -0
  610. package/src/modules/tool-manuals/tool-manuals.contract.ts +183 -0
  611. package/src/modules/tool-manuals/tool-manuals.routes.ts +110 -0
  612. package/src/modules/tool-manuals/tool-manuals.service.ts +781 -0
  613. package/src/modules/tool-manuals/tool-manuals.tools.ts +202 -0
  614. package/src/modules/tool-registry/__tests__/tool-registry.test.ts +64 -0
  615. package/src/modules/tool-registry/manual.routes.ts +87 -0
  616. package/src/modules/tool-registry/tool-registry.ts +55 -0
  617. package/src/modules/tool-registry/tool.contract.ts +52 -0
  618. package/src/modules/workflow/__tests__/event-bus.test.ts +322 -0
  619. package/src/modules/workflow/__tests__/file-change-notifier.test.ts +51 -0
  620. package/src/modules/workflow/__tests__/format-affected-owners.test.ts +67 -0
  621. package/src/modules/workflow/__tests__/locking-filesystem.test.ts +445 -0
  622. package/src/modules/workflow/__tests__/pending-commits.canonical.test.ts +36 -0
  623. package/src/modules/workflow/__tests__/pending-commits.worker.test.ts +243 -0
  624. package/src/modules/workflow/__tests__/preserve-roles-yaml.test.ts +308 -0
  625. package/src/modules/workflow/__tests__/sanitize-error.test.ts +67 -0
  626. package/src/modules/workflow/__tests__/session-ontology.policy.test.ts +62 -0
  627. package/src/modules/workflow/__tests__/session-ontology.service.test.ts +200 -0
  628. package/src/modules/workflow/__tests__/workflow.service.commitFileWhileLocked.test.ts +232 -0
  629. package/src/modules/workflow/__tests__/workflow.service.facade.test.ts +380 -0
  630. package/src/modules/workflow/__tests__/workflow.service.releaseLock.test.ts +340 -0
  631. package/src/modules/workflow/agent-tools/__tests__/workflow.tools.test.ts +192 -0
  632. package/src/modules/workflow/agent-tools/workflow.tools.ts +502 -0
  633. package/src/modules/workflow/event-bus.ts +262 -0
  634. package/src/modules/workflow/events.routes.ts +266 -0
  635. package/src/modules/workflow/file-change-notifier.ts +54 -0
  636. package/src/modules/workflow/file-lock.service.ts +269 -0
  637. package/src/modules/workflow/git/__tests__/branch-name.test.ts +86 -0
  638. package/src/modules/workflow/git/__tests__/branchAuthor.test.ts +80 -0
  639. package/src/modules/workflow/git/__tests__/clone-config.test.ts +15 -0
  640. package/src/modules/workflow/git/__tests__/git-test-helpers.ts +47 -0
  641. package/src/modules/workflow/git/__tests__/git-version.failure.test.ts +24 -0
  642. package/src/modules/workflow/git/__tests__/git-version.test.ts +49 -0
  643. package/src/modules/workflow/git/__tests__/git.service.accessGating.test.ts +389 -0
  644. package/src/modules/workflow/git/__tests__/git.service.changedFilesForPr.test.ts +124 -0
  645. package/src/modules/workflow/git/__tests__/git.service.commitChanges.test.ts +105 -0
  646. package/src/modules/workflow/git/__tests__/git.service.commitFile.test.ts +211 -0
  647. package/src/modules/workflow/git/__tests__/git.service.createBranch.test.ts +154 -0
  648. package/src/modules/workflow/git/__tests__/git.service.deleteBranch.test.ts +320 -0
  649. package/src/modules/workflow/git/__tests__/git.service.diffFileAtCommit.test.ts +202 -0
  650. package/src/modules/workflow/git/__tests__/git.service.diffFileBetweenBranches.test.ts +453 -0
  651. package/src/modules/workflow/git/__tests__/git.service.list-branches.test.ts +198 -0
  652. package/src/modules/workflow/git/__tests__/git.service.listTrackedFiles.test.ts +125 -0
  653. package/src/modules/workflow/git/__tests__/git.service.logForFile.test.ts +219 -0
  654. package/src/modules/workflow/git/__tests__/git.service.mergeChangeRequest.test.ts +145 -0
  655. package/src/modules/workflow/git/__tests__/git.service.pendingChanges.test.ts +240 -0
  656. package/src/modules/workflow/git/__tests__/git.service.postMergePull.test.ts +272 -0
  657. package/src/modules/workflow/git/__tests__/git.service.pull.test.ts +232 -0
  658. package/src/modules/workflow/git/__tests__/git.service.readFileAtRef.test.ts +101 -0
  659. package/src/modules/workflow/git/__tests__/git.service.status.test.ts +127 -0
  660. package/src/modules/workflow/git/__tests__/mutex.test.ts +78 -0
  661. package/src/modules/workflow/git/__tests__/pull-request.service.test.ts +225 -0
  662. package/src/modules/workflow/git/__tests__/pull-request.service.viewer-can-cancel.test.ts +101 -0
  663. package/src/modules/workflow/git/branch-name.ts +70 -0
  664. package/src/modules/workflow/git/clone-config.ts +71 -0
  665. package/src/modules/workflow/git/git-version.ts +70 -0
  666. package/src/modules/workflow/git/git.service.ts +2248 -0
  667. package/src/modules/workflow/git/mutex.ts +22 -0
  668. package/src/modules/workflow/git/pull-request.service.ts +447 -0
  669. package/src/modules/workflow/locking-filesystem.ts +489 -0
  670. package/src/modules/workflow/pending-commits.service.ts +444 -0
  671. package/src/modules/workflow/pending-commits.worker.ts +361 -0
  672. package/src/modules/workflow/read-only-filesystem.ts +54 -0
  673. package/src/modules/workflow/recovery-bot.ts +73 -0
  674. package/src/modules/workflow/review-workflow/__tests__/approval-states.test.ts +560 -0
  675. package/src/modules/workflow/review-workflow/__tests__/cancel-pr.test.ts +176 -0
  676. package/src/modules/workflow/review-workflow/__tests__/merge-gate.test.ts +258 -0
  677. package/src/modules/workflow/review-workflow/review-workflow.interface.ts +199 -0
  678. package/src/modules/workflow/review-workflow/review-workflow.service.ts +806 -0
  679. package/src/modules/workflow/sanitize-error.ts +62 -0
  680. package/src/modules/workflow/session-ontology.policy.ts +70 -0
  681. package/src/modules/workflow/session-ontology.service.ts +178 -0
  682. package/src/modules/workflow/workflow-hooks.ts +101 -0
  683. package/src/modules/workflow/workflow.errors.ts +285 -0
  684. package/src/modules/workflow/workflow.routes.ts +858 -0
  685. package/src/modules/workflow/workflow.service.ts +1555 -0
  686. package/src/modules/workspace/__tests__/kb-seed.service.test.ts +213 -0
  687. package/src/modules/workspace/__tests__/routine-write-policy.test.ts +68 -0
  688. package/src/modules/workspace/__tests__/session-ontology.gate.test.ts +237 -0
  689. package/src/modules/workspace/__tests__/spill-store.test.ts +43 -0
  690. package/src/modules/workspace/__tests__/workspace.routes.create-grant.test.ts +233 -0
  691. package/src/modules/workspace/__tests__/workspace.routes.delete.test.ts +176 -0
  692. package/src/modules/workspace/__tests__/workspace.routes.download.test.ts +303 -0
  693. package/src/modules/workspace/__tests__/workspace.routes.read-gate.test.ts +276 -0
  694. package/src/modules/workspace/__tests__/workspace.service.read-filter.test.ts +120 -0
  695. package/src/modules/workspace/__tests__/workspace.service.test.ts +565 -0
  696. package/src/modules/workspace/__tests__/workspace.tools.test.ts +504 -0
  697. package/src/modules/workspace/bevel-ignore.ts +62 -0
  698. package/src/modules/workspace/kb-seed.interface.ts +36 -0
  699. package/src/modules/workspace/kb-seed.service.ts +361 -0
  700. package/src/modules/workspace/routine-write-policy.ts +79 -0
  701. package/src/modules/workspace/session-ontology.gate.ts +189 -0
  702. package/src/modules/workspace/session-sink.ts +25 -0
  703. package/src/modules/workspace/spill-store.ts +87 -0
  704. package/src/modules/workspace/workspace.routes.ts +1114 -0
  705. package/src/modules/workspace/workspace.service.ts +1343 -0
  706. package/src/modules/workspace/workspace.tools.ts +885 -0
  707. package/src/shared/__tests__/frontmatter-id.test.ts +88 -0
  708. package/src/shared/__tests__/fs-walk.test.ts +42 -0
  709. package/src/shared/__tests__/ssrf.test.ts +53 -0
  710. package/src/shared/__tests__/utcp-namespace.test.ts +19 -0
  711. package/src/shared/frontmatter-id.ts +72 -0
  712. package/src/shared/fs-walk.ts +30 -0
  713. package/src/shared/hash-email.ts +14 -0
  714. package/src/shared/kb-layout.test.ts +88 -0
  715. package/src/shared/kb-layout.ts +101 -0
  716. package/src/shared/ssrf.ts +78 -0
  717. package/src/shared/token-crypto.ts +54 -0
  718. package/src/shared/ttl-cache.ts +27 -0
  719. package/src/shared/utcp-namespace.ts +88 -0
  720. package/src/version.ts +19 -0
@@ -0,0 +1,1343 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { execFile } from 'node:child_process';
4
+ import { promisify } from 'node:util';
5
+ import AdmZip from 'adm-zip';
6
+ import type { AuthUser, IWorkspaceService, WorkspaceInfo, FileTreeEntry } from '@bevel-software/platform-shared';
7
+ import { assertValidRelativePath, validateFilename, DEFAULT_BRANCH } from '@bevel-software/platform-shared';
8
+ import { BevelIgnoreStack } from './bevel-ignore.js';
9
+ import { assertValidBranchName } from '../workflow/git/branch-name.js';
10
+ import { cloneTrackingConfigArgs, SAFE_IMPLICIT_FETCH_ARGS } from '../workflow/git/clone-config.js';
11
+ import type { IDiffService } from '../diff/diff.interface.js';
12
+ import type { IKbSeedService } from './kb-seed.interface.js';
13
+
14
+ /**
15
+ * Workspaces are per-branch, not per-user (PLAN §3). One on-disk clone per
16
+ * branch name, shared by every user editing that branch — coordination of
17
+ * concurrent edits lives in the file-lock service, not in giving each user
18
+ * their own clone.
19
+ *
20
+ * Workspace identity is the URL-encoded branch name. The mapping is
21
+ * deterministic: every caller (backend, frontend, agent) can compute the
22
+ * workspace id from a branch name and vice versa without a DB round-trip.
23
+ *
24
+ * Directory layout on disk:
25
+ * <workspacesRoot>/
26
+ * <encodeURIComponent(branch)>/
27
+ * knowledge-base/ # the git clone checked out to <branch>
28
+ *
29
+ * Bootstrap is lazy: the first call to `getOrCreateForBranch(branch)` clones
30
+ * the repo with `-b <branch>`. Subsequent callers — whether the same user or
31
+ * a different one — share the same directory and the same git operations are
32
+ * serialised through the workspace mutex elsewhere in the codebase.
33
+ */
34
+
35
+ export interface UnzipResult {
36
+ /** Workspace-relative directory the archive was extracted into. */
37
+ destination: string;
38
+ /** Workspace-relative paths of files that were written. */
39
+ extracted: string[];
40
+ /** Workspace-relative entry paths from the archive that were intentionally skipped (e.g. macOS metadata, invalid names). */
41
+ skipped: { path: string; reason: string }[];
42
+ }
43
+
44
+ // Zip-bomb guardrails. The upload cap is 50 MB compressed; even a generous
45
+ // 100× expansion ratio still fits inside these limits, so a well-behaved
46
+ // archive will never trip them. They exist so a hostile or malformed zip
47
+ // can't run the container out of disk by inflating to gigabytes.
48
+ const UNZIP_MAX_ENTRIES = 10_000;
49
+ const UNZIP_MAX_ENTRY_BYTES = 100 * 1024 * 1024; // 100 MB per file
50
+ const UNZIP_MAX_TOTAL_BYTES = 500 * 1024 * 1024; // 500 MB across the whole archive
51
+
52
+ // Folder-download cap. The whole zip is built in memory by adm-zip (no
53
+ // streaming) so the cap also bounds peak heap usage for one download.
54
+ // Reuses the unzip total as a single "fits in a workspace" budget.
55
+ const ZIP_DOWNLOAD_MAX_BYTES = UNZIP_MAX_TOTAL_BYTES;
56
+
57
+ /**
58
+ * Thrown by `createFolderZip` when the cumulative uncompressed bytes exceed
59
+ * `ZIP_DOWNLOAD_MAX_BYTES`. Carries a 413-ready message; the route layer
60
+ * checks `instanceof` and maps it accordingly.
61
+ */
62
+ export class FolderTooLargeError extends Error {
63
+ constructor(public readonly limitBytes: number) {
64
+ super(`Folder exceeds the ${limitBytes}-byte download size limit`);
65
+ this.name = 'FolderTooLargeError';
66
+ }
67
+ }
68
+
69
+ const execFileAsync = promisify(execFile);
70
+
71
+ /**
72
+ * Identity stamped on the per-branch clone's git config. Every workflow
73
+ * commit overrides this with `--author=…` so the human shows up in
74
+ * `git log`; this is just the fallback committer identity so plain
75
+ * git operations have something to attribute to.
76
+ */
77
+ const BOT_NAME = 'Bevel Workflow';
78
+ const BOT_EMAIL = 'bevel-workflow@bevel.software';
79
+
80
+ /** Redact any token that might leak into an error message. */
81
+ function redactError(err: unknown): string {
82
+ const token = process.env.GITHUB_TOKEN;
83
+ const msg = err instanceof Error ? err.message : String(err);
84
+ return token ? msg.replaceAll(token, '***') : msg;
85
+ }
86
+
87
+ const FETCH_CACHE_TTL_MS = 30_000;
88
+
89
+ /**
90
+ * Map a branch name to the directory that contains its workspace clone.
91
+ * `encodeURIComponent` handles `/` in branch names (e.g. `alice/foo`) by
92
+ * encoding it to `%2F` — single-segment-safe on every supported filesystem.
93
+ */
94
+ export function workspaceIdForBranch(branch: string): string {
95
+ return encodeURIComponent(branch);
96
+ }
97
+
98
+ export function branchForWorkspaceId(workspaceId: string): string {
99
+ return decodeURIComponent(workspaceId);
100
+ }
101
+
102
+ /**
103
+ * Per-directory read filter for the file tree. Given a batch of
104
+ * workspace-relative entry paths (e.g. `staging-repo/Product/Knowledge/x.md`),
105
+ * returns a verdict map keyed by those paths (`path → readable`). Injected by
106
+ * the route from the access service so `WorkspaceService` stays access-agnostic
107
+ * (it gets a function, not the access module). See `modules/access/kb-read-filter.ts`.
108
+ */
109
+ export type ReadTreeFilter = (wsRelPaths: string[]) => Promise<Map<string, boolean>>;
110
+
111
+ export class WorkspaceService implements IWorkspaceService {
112
+ /** Maps branch → absolute directory path. Lazily populated. */
113
+ private readonly branchDirs = new Map<string, string>();
114
+
115
+ /**
116
+ * In-flight bootstrap promises keyed by branch — concurrent callers for
117
+ * the same branch share a single clone instead of racing to overwrite
118
+ * each other's working tree.
119
+ */
120
+ private readonly inFlightBootstraps = new Map<string, Promise<void>>();
121
+
122
+ /** Per-workspace-dir `git fetch origin` timestamp + in-flight tracker. */
123
+ private readonly lastFetchAt = new Map<string, number>();
124
+ private readonly inFlightFetches = new Map<string, Promise<void>>();
125
+
126
+ /**
127
+ * Late-bound by the composition root — DiffService depends on
128
+ * WorkspaceService for path resolution.
129
+ */
130
+ private diffService: IDiffService | null = null;
131
+
132
+ /**
133
+ * Late-bound by the composition root. Awaited once before the first per-branch
134
+ * clone so the KB remote is guaranteed to carry the protected branches + base
135
+ * scaffolding — a fresh (or partially-populated) remote is seeded rather than
136
+ * assumed. Null in tests / setups that pre-provision the remote themselves.
137
+ */
138
+ private seedService: IKbSeedService | null = null;
139
+
140
+ /**
141
+ * Late-bound by the composition root: notified with the workspace id
142
+ * right after a branch's clone is created. The git layer uses this to
143
+ * skip the otherwise-redundant implicit `git fetch` on the first
144
+ * `listBranches` — a fresh clone has already downloaded every ref.
145
+ */
146
+ private onWorkspaceCloned: ((workspaceId: string) => void) | null = null;
147
+
148
+ constructor(
149
+ private readonly workspacesRoot: string,
150
+ private readonly kbRepoUrl: string,
151
+ private readonly kbDirName: string,
152
+ private readonly gitUsername: string = 'x-access-token',
153
+ ) {}
154
+
155
+ setDiffService(diffService: IDiffService): void {
156
+ this.diffService = diffService;
157
+ }
158
+
159
+ setSeedService(seedService: IKbSeedService): void {
160
+ this.seedService = seedService;
161
+ }
162
+
163
+ setWorkspaceClonedListener(listener: (workspaceId: string) => void): void {
164
+ this.onWorkspaceCloned = listener;
165
+ }
166
+
167
+ /** Build git clone args that inject credentials via -c headers so the token never appears in the repo URL. */
168
+ private gitCloneArgs(targetDir: string, branch: string, referenceRepo?: string): string[] {
169
+ const token = process.env.GITHUB_TOKEN;
170
+ // core.longpaths=true: KB has paths >260 chars; without this the clone silently drops those files on Windows.
171
+ const args = ['clone', '-c', 'core.longpaths=true', '-b', branch];
172
+ // Borrow the object database of an existing sibling clone. Every commit
173
+ // in the KB is already on disk locally, so object transfer drops to
174
+ // ~zero and a cold branch switch pays only the ref-negotiation
175
+ // round-trip instead of a full download. `--dissociate` copies the
176
+ // borrowed objects into the new repo and drops the alternates link, so
177
+ // the new clone stays independent even if the sibling is later swept
178
+ // or gc'd. Still a real clone of the real remote — all `refs/remotes/
179
+ // origin/*` are populated as usual.
180
+ if (referenceRepo) {
181
+ args.push('--reference', referenceRepo, '--dissociate');
182
+ }
183
+ if (token) {
184
+ // The helper script reads from GITHUB_TOKEN at runtime — the token value never appears in args.
185
+ // Username is provider-specific (GitHub `x-access-token`, GitLab `oauth2`, …); the token is
186
+ // always the Basic-auth password, which every major host accepts.
187
+ args.push(
188
+ '-c', `credential.helper=!f() { echo "username=${this.gitUsername}"; echo "password=$GITHUB_TOKEN"; }; f`,
189
+ );
190
+ }
191
+ args.push(this.kbRepoUrl, targetDir);
192
+ return args;
193
+ }
194
+
195
+ /**
196
+ * Find an existing workspace clone on disk to use as a `--reference` for
197
+ * a new clone. Any clone of the KB works — they all share the same object
198
+ * history. Returns the absolute path to the inner repo dir, or null when
199
+ * no sibling exists (the first branch after a cold start). Scans disk
200
+ * rather than the in-memory `branchDirs` map so clones that survived a
201
+ * process restart are still found.
202
+ */
203
+ private async findSiblingRepo(excludeBranch: string): Promise<string | null> {
204
+ const excludeId = workspaceIdForBranch(excludeBranch);
205
+ let entries: Array<{ name: string; isDirectory: () => boolean }>;
206
+ try {
207
+ entries = await fs.readdir(this.workspacesRoot, { withFileTypes: true });
208
+ } catch {
209
+ return null;
210
+ }
211
+ for (const entry of entries) {
212
+ if (!entry.isDirectory() || entry.name === excludeId) continue;
213
+ const repoDir = path.join(this.workspacesRoot, entry.name, this.kbDirName);
214
+ try {
215
+ await fs.access(path.join(repoDir, '.git'));
216
+ return repoDir;
217
+ } catch {
218
+ // No `.git` — half-built or unrelated dir; keep looking.
219
+ }
220
+ }
221
+ return null;
222
+ }
223
+
224
+ /**
225
+ * Return the id of any workspace already cloned on disk, or null if none
226
+ * exists. Every clone tracks the same origin, so for a *repo-global* operation
227
+ * that doesn't act on a particular draft — listing branches, reading or
228
+ * merging a change request by number — any existing clone is interchangeable.
229
+ * Reusing one that's already on disk avoids cloning a branch (or failing when
230
+ * a conventional default branch isn't on the remote) just to run a global op.
231
+ * Scans disk rather than the in-memory `branchDirs` map so clones that
232
+ * survived a process restart are still found.
233
+ */
234
+ async findAnyWorkspaceId(): Promise<string | null> {
235
+ let entries: Array<{ name: string; isDirectory: () => boolean }>;
236
+ try {
237
+ entries = await fs.readdir(this.workspacesRoot, { withFileTypes: true });
238
+ } catch {
239
+ return null;
240
+ }
241
+ for (const entry of entries) {
242
+ if (!entry.isDirectory()) continue;
243
+ try {
244
+ await fs.access(path.join(this.workspacesRoot, entry.name, this.kbDirName, '.git'));
245
+ return entry.name;
246
+ } catch {
247
+ // No `.git` — half-built or unrelated dir; keep looking.
248
+ }
249
+ }
250
+ return null;
251
+ }
252
+
253
+ /**
254
+ * Run the clone. When a `referenceRepo` is supplied and the referenced
255
+ * clone fails (e.g. the sibling's object store is corrupt or mid-write),
256
+ * fall back once to a plain network clone so a bad sibling can never
257
+ * block a new branch from bootstrapping.
258
+ */
259
+ private async runClone(
260
+ targetDir: string,
261
+ branch: string,
262
+ referenceRepo: string | null,
263
+ ): Promise<void> {
264
+ if (referenceRepo) {
265
+ try {
266
+ await execFileAsync('git', this.gitCloneArgs(targetDir, branch, referenceRepo), {
267
+ env: { ...process.env },
268
+ });
269
+ return;
270
+ } catch (err) {
271
+ console.warn(
272
+ `[workspace] referenced clone for "${branch}" failed, retrying without reference:`,
273
+ redactError(err),
274
+ );
275
+ // Clear any partial output so the retry clones into a clean dir.
276
+ await fs.rm(targetDir, { recursive: true, force: true }).catch(() => {});
277
+ }
278
+ }
279
+ await execFileAsync('git', this.gitCloneArgs(targetDir, branch), {
280
+ env: { ...process.env },
281
+ });
282
+ }
283
+
284
+ /**
285
+ * Get-or-create the per-branch workspace. Idempotent — repeat calls for
286
+ * the same branch reuse the existing clone. Concurrent first-callers
287
+ * share one bootstrap via `inFlightBootstraps`.
288
+ */
289
+ async getOrCreateForBranch(branch: string): Promise<WorkspaceInfo> {
290
+ assertValidBranchName(branch);
291
+ const id = workspaceIdForBranch(branch);
292
+ const workspaceDir = path.join(this.workspacesRoot, id);
293
+ const repoDir = path.join(workspaceDir, this.kbDirName);
294
+
295
+ // Fast path: we've already bootstrapped this branch in this process.
296
+ // Wait for any in-flight bootstrap before returning so we never hand
297
+ // back a half-cloned workspace.
298
+ if (this.branchDirs.has(branch)) {
299
+ await this.awaitBootstrapOrEvict(branch);
300
+ return this.buildWorkspaceInfo(branch, workspaceDir);
301
+ }
302
+
303
+ // If a bootstrap is in flight for this branch, wait it out before
304
+ // trusting the on-disk check below. `git clone` creates `.git` within
305
+ // the first few hundred ms of a multi-second clone, well before the
306
+ // working tree (roles.yaml, access.md, every tracked file) is checked
307
+ // out — so a naive fs.access(.git) on a mid-clone dir happily hands a
308
+ // half-cloned workspace to the caller, and the next loadModel /
309
+ // readFile faceplants with ENOENT and 500s. This mirrors the
310
+ // single-flight wait the cached fast path already uses.
311
+ await this.awaitBootstrapOrEvict(branch);
312
+ if (this.branchDirs.has(branch)) {
313
+ return this.buildWorkspaceInfo(branch, workspaceDir);
314
+ }
315
+
316
+ // Check disk — the clone may have been bootstrapped by a previous
317
+ // process and survived a restart.
318
+ try {
319
+ await fs.access(path.join(repoDir, '.git'));
320
+ // Migration for clones already on disk: re-stamp the tracking config the
321
+ // first time this process opens them, so a clone whose config drifted
322
+ // (duplicate fetch refspec / merge ref) is repaired before anything
323
+ // pulls it. Once per branch per process — the cached paths above return
324
+ // before reaching here.
325
+ await this.normalizeCloneTracking(repoDir, branch);
326
+ this.branchDirs.set(branch, workspaceDir);
327
+ return this.buildWorkspaceInfo(branch, workspaceDir);
328
+ } catch {
329
+ // Not on disk — bootstrap below.
330
+ }
331
+
332
+ // Single-flight bootstrap. Concurrent callers for the same branch
333
+ // share the first one's promise instead of stacking clones.
334
+ let resolveBootstrap!: () => void;
335
+ let rejectBootstrap!: (err: unknown) => void;
336
+ const bootstrap = new Promise<void>((res, rej) => {
337
+ resolveBootstrap = res;
338
+ rejectBootstrap = rej;
339
+ });
340
+ bootstrap.catch(() => undefined);
341
+
342
+ const existingBootstrap = this.inFlightBootstraps.get(branch);
343
+ if (existingBootstrap) {
344
+ await existingBootstrap;
345
+ this.branchDirs.set(branch, workspaceDir);
346
+ return this.buildWorkspaceInfo(branch, workspaceDir);
347
+ }
348
+ this.inFlightBootstraps.set(branch, bootstrap);
349
+
350
+ try {
351
+ // Ensure the remote is seeded before the first clone of any branch. A
352
+ // clone with `-b <branch>` fails outright on an empty remote, so seeding
353
+ // must happen ahead of it. The seed service is single-flight, so this is
354
+ // a cheap already-resolved await on every clone after the first.
355
+ if (this.seedService) {
356
+ await this.seedService.ensureRemoteSeeded();
357
+ }
358
+ await fs.mkdir(workspaceDir, { recursive: true });
359
+ await this.cloneProcessMapForBranch(workspaceDir, branch);
360
+ this.branchDirs.set(branch, workspaceDir);
361
+ resolveBootstrap();
362
+ } catch (err) {
363
+ this.branchDirs.delete(branch);
364
+ rejectBootstrap(err);
365
+ throw err;
366
+ } finally {
367
+ if (this.inFlightBootstraps.get(branch) === bootstrap) {
368
+ this.inFlightBootstraps.delete(branch);
369
+ }
370
+ }
371
+
372
+ return this.buildWorkspaceInfo(branch, workspaceDir);
373
+ }
374
+
375
+ /**
376
+ * Convenience: open-or-create the workspace using the caller's user
377
+ * record only for typing / auditing context. Used by route handlers
378
+ * that don't know the target branch up-front (e.g. legacy bootstrap
379
+ * paths). Always resolves to the default protected branch unless
380
+ * `branch` is passed.
381
+ */
382
+ async getOrCreateForUser(_user: AuthUser, branch?: string): Promise<WorkspaceInfo> {
383
+ return this.getOrCreateForBranch(branch ?? DEFAULT_BRANCH);
384
+ }
385
+
386
+ private buildWorkspaceInfo(branch: string, workspaceDir: string): WorkspaceInfo {
387
+ return {
388
+ id: workspaceIdForBranch(branch),
389
+ name: `${branch}`,
390
+ absolutePath: workspaceDir,
391
+ createdAt: new Date(0).toISOString(),
392
+ kbDirName: this.kbDirName,
393
+ };
394
+ }
395
+
396
+ /**
397
+ * Await the in-flight bootstrap for a branch (if any). On reject —
398
+ * meaning the clone rolled back — drop the branch→dir mapping so the
399
+ * next caller re-bootstraps.
400
+ */
401
+ private async awaitBootstrapOrEvict(branch: string): Promise<void> {
402
+ const pending = this.inFlightBootstraps.get(branch);
403
+ if (!pending) return;
404
+ try {
405
+ await pending;
406
+ } catch (err) {
407
+ this.branchDirs.delete(branch);
408
+ throw err;
409
+ }
410
+ }
411
+
412
+ /**
413
+ * Collapse the clone's tracking config to a single fetch refspec and a single
414
+ * upstream ref for `branch` (see `workflow/git/clone-config.ts`). A clone that
415
+ * accumulated a second `remote.origin.fetch` refspec or `branch.<b>.merge`
416
+ * value makes git refuse to refresh it — "Cannot rebase onto multiple
417
+ * branches" — which is how a post-merge pull of the target branch fails.
418
+ *
419
+ * Never throws: it only ever touches `.git/config`, so a failure leaves the
420
+ * workspace exactly as usable as it was, and the git layer self-heals the
421
+ * same keys before it pulls.
422
+ */
423
+ private async normalizeCloneTracking(repoDir: string, branch: string): Promise<void> {
424
+ try {
425
+ for (const args of cloneTrackingConfigArgs(branch)) {
426
+ await execFileAsync('git', ['-C', repoDir, ...args]);
427
+ }
428
+ } catch (err) {
429
+ // One line per branch, not per key: every key writes to the same
430
+ // `.git/config`, so what fails for one fails for all.
431
+ console.warn(
432
+ `[workspace] could not normalize the tracking config of the "${branch}" clone:`,
433
+ redactError(err),
434
+ );
435
+ }
436
+ }
437
+
438
+ private async cloneProcessMapForBranch(workspaceDir: string, branch: string): Promise<void> {
439
+ const targetDir = path.join(workspaceDir, this.kbDirName);
440
+
441
+ // Probe whether `targetDir` exists. We split the existence check from
442
+ // the inner `.git` validation so unexpected errors from EITHER probe
443
+ // propagate to the caller instead of getting silently swallowed by an
444
+ // outer catch (the previous shape let an inner rethrow fall through
445
+ // to a clone-into-non-empty-dir, producing a confusing 500).
446
+ let targetExists = true;
447
+ try {
448
+ await fs.access(targetDir);
449
+ } catch (err) {
450
+ const code = (err as NodeJS.ErrnoException | null)?.code;
451
+ if (code === 'ENOENT' || code === 'ENOTDIR') {
452
+ targetExists = false;
453
+ } else {
454
+ throw err;
455
+ }
456
+ }
457
+
458
+ if (targetExists) {
459
+ // The directory exists, but a previous bootstrap may have crashed
460
+ // mid-clone — leaving a directory shell without `.git`. Treat
461
+ // anything missing `.git` as not-cloned and re-clone. (Can't be
462
+ // an active workspace either: every cloned repo has `.git` from
463
+ // step one.)
464
+ let alreadyCloned = false;
465
+ try {
466
+ const dotGit = await fs.stat(path.join(targetDir, '.git'));
467
+ if (dotGit.isDirectory() || dotGit.isFile()) {
468
+ alreadyCloned = true;
469
+ }
470
+ } catch (err) {
471
+ // Only treat "does-not-exist" as "half-built dir, wipe and
472
+ // re-clone". A transient EACCES / EIO / EBUSY must NOT delete
473
+ // what might be a perfectly valid repo whose `.git` we couldn't
474
+ // read this moment — surface the error so the caller can retry.
475
+ const code = (err as NodeJS.ErrnoException | null)?.code;
476
+ if (code !== 'ENOENT' && code !== 'ENOTDIR') {
477
+ throw err;
478
+ }
479
+ }
480
+ if (alreadyCloned) {
481
+ // We don't auto-pull because that could clobber another user's
482
+ // in-progress lock-held edits. The tracking config IS re-stamped —
483
+ // it never touches the working tree, and a drifted config is what
484
+ // breaks the next pull.
485
+ await this.normalizeCloneTracking(targetDir, branch);
486
+ return;
487
+ }
488
+ // Half-built dir — wipe and re-clone.
489
+ await fs.rm(targetDir, { recursive: true, force: true });
490
+ }
491
+
492
+ // Borrow an existing sibling clone's objects to skip the bulk download.
493
+ // Resolved before the clone so a failure mid-clone still rolls back cleanly.
494
+ const reference = await this.findSiblingRepo(branch);
495
+ try {
496
+ await this.runClone(targetDir, branch, reference);
497
+ // Persist longpaths in the cloned repo's config so subsequent
498
+ // checkouts also honor it.
499
+ await execFileAsync('git', ['-C', targetDir, 'config', 'core.longpaths', 'true']);
500
+ // Generic bot identity for the clone — every workflow commit
501
+ // overrides via `--author=…` so the real human shows up in
502
+ // `git log`. This is purely the fallback committer.
503
+ await execFileAsync('git', ['-C', targetDir, 'config', 'user.name', BOT_NAME]);
504
+ await execFileAsync('git', ['-C', targetDir, 'config', 'user.email', BOT_EMAIL]);
505
+ // Pin the clone to exactly one fetch refspec and one upstream ref for
506
+ // this branch. `git clone -b` already produces that shape; stamping it
507
+ // explicitly means the shape is asserted rather than assumed, and the
508
+ // same call is what repairs an existing clone that drifted.
509
+ await this.normalizeCloneTracking(targetDir, branch);
510
+ console.log(
511
+ `[workspace] Cloned ${this.kbDirName} for branch "${branch}"` +
512
+ (reference ? ' (referenced a sibling clone)' : ''),
513
+ );
514
+ // A fresh clone has already downloaded every ref — tell the git layer
515
+ // so the first `listBranches` skips the redundant implicit `git fetch`.
516
+ // Isolated from the clone-rollback `catch` below: a misbehaving listener
517
+ // must not cause a successful clone to be wiped.
518
+ try {
519
+ this.onWorkspaceCloned?.(workspaceIdForBranch(branch));
520
+ } catch (listenerErr) {
521
+ console.error(
522
+ `[workspace] onWorkspaceCloned listener failed for branch "${branch}":`,
523
+ redactError(listenerErr),
524
+ );
525
+ }
526
+ // Fill in any base scaffolding this branch is missing, now that it's
527
+ // freshly checked out. Runs on whichever branch the user loaded (not just
528
+ // protected ones), only on a fresh clone (this block). Isolated in its own
529
+ // try/catch — like the onWorkspaceCloned listener above — so even a future
530
+ // change that lets it throw can never roll back an otherwise-good clone.
531
+ if (this.seedService) {
532
+ try {
533
+ await this.seedService.topUpWorkspace(targetDir, branch);
534
+ } catch (topUpErr) {
535
+ console.error(
536
+ `[workspace] scaffolding top-up failed for branch "${branch}" (clone kept):`,
537
+ redactError(topUpErr),
538
+ );
539
+ }
540
+ }
541
+ } catch (err) {
542
+ const redacted = redactError(err);
543
+ console.error(`[workspace] Failed to clone for branch "${branch}":`, redacted);
544
+ // Roll back partial state so the next bootstrap retries cleanly.
545
+ await fs.rm(targetDir, { recursive: true, force: true }).catch(() => {});
546
+ throw new Error(`Failed to clone process map: ${redacted}`);
547
+ }
548
+ }
549
+
550
+ async getMetadata(workspaceId: string): Promise<WorkspaceInfo | null> {
551
+ try {
552
+ const workspaceDir = await this.resolveWorkspaceDir(workspaceId);
553
+ return this.buildWorkspaceInfo(branchForWorkspaceId(workspaceId), workspaceDir);
554
+ } catch {
555
+ return null;
556
+ }
557
+ }
558
+
559
+ /**
560
+ * Delete a branch's workspace directory entirely. Used when a branch is
561
+ * deleted upstream — frees the disk used by its clone. The branch's git
562
+ * history on origin is unaffected.
563
+ */
564
+ async deleteWorkspace(workspaceId: string): Promise<void> {
565
+ const workspaceDir = await this.resolveWorkspaceDir(workspaceId);
566
+ await fs.rm(workspaceDir, { recursive: true, force: true });
567
+ const branch = branchForWorkspaceId(workspaceId);
568
+ this.branchDirs.delete(branch);
569
+ }
570
+
571
+ async listFiles(workspaceId: string, readFilter?: ReadTreeFilter): Promise<FileTreeEntry> {
572
+ const workspaceDir = await this.resolveWorkspaceDir(workspaceId);
573
+ return this.buildFileTree(workspaceDir, workspaceDir, BevelIgnoreStack.empty(), readFilter);
574
+ }
575
+
576
+ async readFile(workspaceId: string, relativePath: string): Promise<string> {
577
+ const workspaceDir = await this.resolveWorkspaceDir(workspaceId);
578
+ const absolutePath = path.resolve(workspaceDir, relativePath);
579
+ this.assertWithinWorkspace(absolutePath, workspaceDir);
580
+ return fs.readFile(absolutePath, 'utf-8');
581
+ }
582
+
583
+ async readFileBinary(workspaceId: string, relativePath: string): Promise<Buffer> {
584
+ const workspaceDir = await this.resolveWorkspaceDir(workspaceId);
585
+ const absolutePath = path.resolve(workspaceDir, relativePath);
586
+ this.assertWithinWorkspace(absolutePath, workspaceDir);
587
+ return fs.readFile(absolutePath);
588
+ }
589
+
590
+ /**
591
+ * Read every `.md` file under the KB repo in one disk pass, keyed by
592
+ * repo-root-relative POSIX path (e.g. `Product/NodeTypes/ServiceCommitment.md`)
593
+ * — the form `parseGraph` keys its file map on. Respects `.git/` and
594
+ * `.bevelignore` like the file tree, so the iframe's `buildGraph()` sees the
595
+ * same set the explorer shows. This exists so the HTML renderer can fetch the
596
+ * whole KB in a single request instead of one HTTP round-trip per file (the
597
+ * KB is thousands of files; per-file fetching exhausts the browser's
598
+ * connection pool).
599
+ */
600
+ async readAllKbFiles(workspaceId: string): Promise<Record<string, string>> {
601
+ const workspaceDir = await this.resolveWorkspaceDir(workspaceId);
602
+ const repoRoot = path.join(workspaceDir, this.kbDirName);
603
+ const files: Record<string, string> = {};
604
+ const walk = async (dir: string, ignoreStack: BevelIgnoreStack): Promise<void> => {
605
+ const nextStack = await ignoreStack.extendedWith(dir);
606
+ const entries = await fs.readdir(dir, { withFileTypes: true });
607
+ for (const entry of entries) {
608
+ if (entry.name === '.git' && entry.isDirectory()) continue;
609
+ const childAbs = path.join(dir, entry.name);
610
+ if (nextStack.isIgnored(childAbs, entry.isDirectory())) continue;
611
+ if (entry.isDirectory()) {
612
+ await walk(childAbs, nextStack);
613
+ continue;
614
+ }
615
+ if (!entry.isFile() || !entry.name.endsWith('.md')) continue;
616
+ const repoPath = path.relative(repoRoot, childAbs).replace(/\\/g, '/');
617
+ files[repoPath] = await fs.readFile(childAbs, 'utf-8');
618
+ }
619
+ };
620
+ await walk(repoRoot, BevelIgnoreStack.empty());
621
+ return files;
622
+ }
623
+
624
+ /**
625
+ * Build a zip archive of the folder at `relativePath` and return it as a
626
+ * single buffer. Mirrors the file-tree's skip rules — `.git/`, `.gitkeep`,
627
+ * and `.bevelignore`-excluded paths are omitted so the zip matches what
628
+ * the user sees in the explorer (and so we don't leak the entire git
629
+ * object database into a download).
630
+ *
631
+ * Entry names inside the zip are prefixed with the folder's own name so
632
+ * unzipping yields `<folderName>/...` rather than spilling files into the
633
+ * caller's cwd. Throws `FolderTooLargeError` if cumulative uncompressed
634
+ * size crosses `ZIP_DOWNLOAD_MAX_BYTES`. Buffered in memory (adm-zip has
635
+ * no streaming API); the cap therefore doubles as a peak-heap bound.
636
+ */
637
+ async createFolderZip(workspaceId: string, relativePath: string): Promise<Buffer> {
638
+ const workspaceDir = await this.resolveWorkspaceDir(workspaceId);
639
+ const absoluteRoot = path.resolve(workspaceDir, relativePath);
640
+ this.assertWithinWorkspace(absoluteRoot, workspaceDir);
641
+ const stat = await fs.stat(absoluteRoot);
642
+ if (!stat.isDirectory()) {
643
+ throw new Error('Not a directory');
644
+ }
645
+
646
+ const zipRoot = path.basename(absoluteRoot) || 'folder';
647
+ const zip = new AdmZip();
648
+ let totalBytes = 0;
649
+
650
+ const walk = async (dir: string, ignoreStack: BevelIgnoreStack): Promise<void> => {
651
+ const nextStack = await ignoreStack.extendedWith(dir);
652
+ const entries = await fs.readdir(dir, { withFileTypes: true });
653
+ entries.sort((a, b) => a.name.localeCompare(b.name));
654
+ for (const entry of entries) {
655
+ if (entry.name === '.git' && entry.isDirectory()) continue;
656
+ if (entry.name === '.gitkeep' && entry.isFile()) continue;
657
+ const childAbs = path.join(dir, entry.name);
658
+ if (nextStack.isIgnored(childAbs, entry.isDirectory())) continue;
659
+ if (entry.isDirectory()) {
660
+ await walk(childAbs, nextStack);
661
+ continue;
662
+ }
663
+ if (!entry.isFile()) continue; // skip sockets, symlinks, etc.
664
+ // Check the size cap BEFORE reading the file into memory. Reading
665
+ // first would let a single hostile 2 GB file allocate the whole
666
+ // buffer before the throw — defeating the cap as a peak-heap
667
+ // bound. `stat.size` is an upper bound that we re-verify after
668
+ // the read in case the file grew between stat and readFile.
669
+ const stat = await fs.stat(childAbs);
670
+ if (totalBytes + stat.size > ZIP_DOWNLOAD_MAX_BYTES) {
671
+ throw new FolderTooLargeError(ZIP_DOWNLOAD_MAX_BYTES);
672
+ }
673
+ const data = await fs.readFile(childAbs);
674
+ if (totalBytes + data.byteLength > ZIP_DOWNLOAD_MAX_BYTES) {
675
+ throw new FolderTooLargeError(ZIP_DOWNLOAD_MAX_BYTES);
676
+ }
677
+ totalBytes += data.byteLength;
678
+ // Path inside the archive: <folderName>/<relPathUnderFolder>, POSIX
679
+ // separators regardless of host OS so the zip extracts cleanly
680
+ // on Windows/Mac/Linux alike.
681
+ const relInside = path
682
+ .relative(absoluteRoot, childAbs)
683
+ .replace(/\\/g, '/');
684
+ zip.addFile(`${zipRoot}/${relInside}`, data);
685
+ }
686
+ };
687
+
688
+ await walk(absoluteRoot, BevelIgnoreStack.empty());
689
+ return zip.toBuffer();
690
+ }
691
+
692
+ /**
693
+ * Read a file's contents as it exists on a specific git ref in the
694
+ * branch's clone. Returns null when the ref or path doesn't exist on that
695
+ * ref. `relativePath` is relative to the repo root (e.g. `Knowledge/Foo.md`),
696
+ * NOT to the workspace dir — the repo sits at `<workspaceDir>/knowledge-base`.
697
+ */
698
+ async readFileAtRef(
699
+ workspaceId: string,
700
+ ref: string,
701
+ relativePath: string,
702
+ ): Promise<string | null> {
703
+ this.assertValidGitRef(ref);
704
+ this.assertValidRepoRelativePath(relativePath);
705
+ const workspaceDir = await this.resolveWorkspaceDir(workspaceId);
706
+ const repoDir = path.join(workspaceDir, this.kbDirName);
707
+
708
+ const candidates = [ref];
709
+ if (!ref.startsWith('origin/') && !ref.startsWith('refs/')) {
710
+ candidates.push(`origin/${ref}`);
711
+ }
712
+
713
+ for (const candidate of candidates) {
714
+ try {
715
+ const { stdout } = await execFileAsync(
716
+ 'git',
717
+ ['-C', repoDir, 'show', `${candidate}:${relativePath}`],
718
+ { encoding: 'utf-8', maxBuffer: 16 * 1024 * 1024 },
719
+ );
720
+ return stdout;
721
+ } catch {
722
+ // Try next candidate.
723
+ }
724
+ }
725
+ return null;
726
+ }
727
+
728
+ /**
729
+ * Run `git fetch --prune origin` for this branch's clone. Results are
730
+ * cached for `FETCH_CACHE_TTL_MS`; concurrent callers share the same
731
+ * in-flight fetch to avoid fetch storms when e.g. the CR list poll fans out.
732
+ */
733
+ async ensureRemotesFetched(workspaceId: string): Promise<void> {
734
+ const workspaceDir = await this.resolveWorkspaceDir(workspaceId);
735
+ const repoDir = path.join(workspaceDir, this.kbDirName);
736
+ const now = Date.now();
737
+ const last = this.lastFetchAt.get(repoDir) ?? 0;
738
+ if (now - last < FETCH_CACHE_TTL_MS) return;
739
+
740
+ const inFlight = this.inFlightFetches.get(repoDir);
741
+ if (inFlight) return inFlight;
742
+
743
+ // This driver runs outside the git layer's per-workspace mutex, so it may
744
+ // only run the safe implicit-fetch shape — see `SAFE_IMPLICIT_FETCH_ARGS`
745
+ // in `workflow/git/clone-config.ts` for the full rationale.
746
+ const promise = execFileAsync(
747
+ 'git',
748
+ ['-C', repoDir, ...SAFE_IMPLICIT_FETCH_ARGS],
749
+ { env: { ...process.env } },
750
+ )
751
+ .then(() => {
752
+ this.lastFetchAt.set(repoDir, Date.now());
753
+ })
754
+ .catch((err) => {
755
+ console.warn('[workspace] git fetch origin failed:', redactError(err));
756
+ })
757
+ .finally(() => {
758
+ if (this.inFlightFetches.get(repoDir) === promise) {
759
+ this.inFlightFetches.delete(repoDir);
760
+ }
761
+ });
762
+ this.inFlightFetches.set(repoDir, promise);
763
+ return promise;
764
+ }
765
+
766
+ private assertValidGitRef(ref: string): void {
767
+ if (!ref) throw new Error('Git ref is required');
768
+ if (ref.startsWith('-') || /[\s\0]/.test(ref)) {
769
+ throw new Error('Invalid git ref');
770
+ }
771
+ }
772
+
773
+ private assertValidRepoRelativePath(relativePath: string): void {
774
+ if (!relativePath) throw new Error('Repo-relative path is required');
775
+ if (relativePath.startsWith('-') || relativePath.includes('\0')) {
776
+ throw new Error('Invalid repo-relative path');
777
+ }
778
+ }
779
+
780
+ async deleteFile(workspaceId: string, relativePath: string): Promise<void> {
781
+ const workspaceDir = await this.resolveWorkspaceDir(workspaceId);
782
+ const absolutePath = path.resolve(workspaceDir, relativePath);
783
+ this.assertWithinWorkspace(absolutePath, workspaceDir);
784
+ await fs.rm(absolutePath, { recursive: true, force: true });
785
+ await this.diffService?.markUserDeleted(workspaceId, relativePath);
786
+ }
787
+
788
+ /**
789
+ * Remove the workspace's `tmp/` directory — transient run artifacts (code-mode
790
+ * tool-chain spills, staged SharePoint docs) that live at the workspace root,
791
+ * outside the KB git repo, and are never committed. Computes the path directly
792
+ * from `workspacesRoot` (no bootstrapping) and force-removes, so it's safe to
793
+ * call after a run even if the workspace was already torn down — a missing
794
+ * directory is ignored.
795
+ */
796
+ async clearTmp(workspaceId: string): Promise<void> {
797
+ // Guard against a malicious/odd workspaceId (e.g. "..") escaping the root
798
+ // and force-removing an unintended directory.
799
+ const root = path.resolve(this.workspacesRoot);
800
+ const tmpDir = path.resolve(root, workspaceId, 'tmp');
801
+ if (!tmpDir.startsWith(root + path.sep)) {
802
+ throw new Error(`clearTmp: refusing to remove a path outside the workspaces root (workspaceId="${workspaceId}").`);
803
+ }
804
+ await fs.rm(tmpDir, { recursive: true, force: true });
805
+ }
806
+
807
+ async moveEntry(workspaceId: string, oldRelativePath: string, newRelativePath: string): Promise<void> {
808
+ assertValidRelativePath(newRelativePath);
809
+ const workspaceDir = await this.resolveWorkspaceDir(workspaceId);
810
+ const oldAbsolute = path.resolve(workspaceDir, oldRelativePath);
811
+ const newAbsolute = path.resolve(workspaceDir, newRelativePath);
812
+ this.assertWithinWorkspace(oldAbsolute, workspaceDir);
813
+ this.assertWithinWorkspace(newAbsolute, workspaceDir);
814
+ await fs.mkdir(path.dirname(newAbsolute), { recursive: true });
815
+ await fs.rename(oldAbsolute, newAbsolute);
816
+ if (this.diffService) {
817
+ await this.diffService.markUserDeleted(workspaceId, oldRelativePath);
818
+ await this.diffService.syncFromDisk(workspaceId, newRelativePath);
819
+ }
820
+ }
821
+
822
+ async writeFile(workspaceId: string, relativePath: string, content: string): Promise<void> {
823
+ assertValidRelativePath(relativePath);
824
+ const workspaceDir = await this.resolveWorkspaceDir(workspaceId);
825
+ const absolutePath = path.resolve(workspaceDir, relativePath);
826
+ this.assertWithinWorkspace(absolutePath, workspaceDir);
827
+ await fs.mkdir(path.dirname(absolutePath), { recursive: true });
828
+ await fs.writeFile(absolutePath, content, 'utf-8');
829
+ await this.diffService?.syncFromDisk(workspaceId, relativePath);
830
+ }
831
+
832
+ async createDirectory(workspaceId: string, relativePath: string): Promise<void> {
833
+ assertValidRelativePath(relativePath);
834
+ const workspaceDir = await this.resolveWorkspaceDir(workspaceId);
835
+ const absolutePath = path.resolve(workspaceDir, relativePath);
836
+ this.assertWithinWorkspace(absolutePath, workspaceDir);
837
+ await fs.mkdir(absolutePath, { recursive: true });
838
+ const entries = await fs.readdir(absolutePath);
839
+ if (entries.length === 0) {
840
+ await fs.writeFile(path.join(absolutePath, '.gitkeep'), '', 'utf-8');
841
+ }
842
+ }
843
+
844
+ async writeFileBinary(workspaceId: string, relativePath: string, data: Uint8Array): Promise<void> {
845
+ assertValidRelativePath(relativePath);
846
+ const workspaceDir = await this.resolveWorkspaceDir(workspaceId);
847
+ const absolutePath = path.resolve(workspaceDir, relativePath);
848
+ this.assertWithinWorkspace(absolutePath, workspaceDir);
849
+ await fs.mkdir(path.dirname(absolutePath), { recursive: true });
850
+ await fs.writeFile(absolutePath, data);
851
+ await this.diffService?.syncFromDisk(workspaceId, relativePath);
852
+ }
853
+
854
+ /**
855
+ * Extract a `.zip` file already present in the workspace into a destination
856
+ * directory (defaults to the archive's parent directory — "unzip here").
857
+ * Validation + zip-bomb guards are identical to the per-user version that
858
+ * preceded the workspace-per-branch migration.
859
+ */
860
+ async unzipFile(
861
+ workspaceId: string,
862
+ zipRelativePath: string,
863
+ destDirRelativePath?: string,
864
+ /**
865
+ * Per-extracted-path write guard (ontology-session boundary). Called with
866
+ * each entry's workspace-relative target before it is written; if it throws,
867
+ * the entry is skipped (with the thrown message as the reason) rather than
868
+ * aborting the whole extraction. Omitted by non-agent / human callers.
869
+ */
870
+ guardWrite?: (wsRelativePath: string) => Promise<void>,
871
+ ): Promise<UnzipResult> {
872
+ const workspaceDir = await this.resolveWorkspaceDir(workspaceId);
873
+ const zipAbsolute = path.resolve(workspaceDir, zipRelativePath);
874
+ this.assertWithinWorkspace(zipAbsolute, workspaceDir);
875
+ if (!zipRelativePath.toLowerCase().endsWith('.zip')) {
876
+ throw new Error('Only .zip files can be extracted');
877
+ }
878
+
879
+ const inferredDest = (() => {
880
+ const d = path.posix.dirname(zipRelativePath.replace(/\\/g, '/'));
881
+ return d === '.' ? '' : d;
882
+ })();
883
+ const destRel = destDirRelativePath ?? inferredDest;
884
+ if (destRel) assertValidRelativePath(destRel);
885
+ const destAbsolute = destRel ? path.resolve(workspaceDir, destRel) : workspaceDir;
886
+ this.assertWithinWorkspace(destAbsolute, workspaceDir);
887
+
888
+ let zip: AdmZip;
889
+ try {
890
+ zip = new AdmZip(zipAbsolute);
891
+ } catch (err) {
892
+ const msg = err instanceof Error ? err.message : String(err);
893
+ throw new Error(`Could not read zip file: ${msg}`);
894
+ }
895
+
896
+ // The destination directory is created on demand by the first allowed entry
897
+ // (each guarded path mkdirs its parents with `recursive: true`). Creating it
898
+ // up front would leave it behind even when every entry is write-blocked.
899
+ const extracted: string[] = [];
900
+ const skipped: UnzipResult['skipped'] = [];
901
+ let processedCount = 0;
902
+ let totalUncompressed = 0;
903
+
904
+ for (const entry of zip.getEntries()) {
905
+ if (processedCount >= UNZIP_MAX_ENTRIES) {
906
+ skipped.push({
907
+ path: entry.entryName,
908
+ reason: `Archive exceeds ${UNZIP_MAX_ENTRIES} entries`,
909
+ });
910
+ continue;
911
+ }
912
+ processedCount++;
913
+ const rawName = entry.entryName.replace(/\\/g, '/');
914
+
915
+ if (
916
+ rawName.startsWith('__MACOSX/') ||
917
+ rawName === '__MACOSX' ||
918
+ rawName.endsWith('/.DS_Store') ||
919
+ rawName === '.DS_Store' ||
920
+ /(^|\/)\._/.test(rawName)
921
+ ) {
922
+ continue;
923
+ }
924
+
925
+ if (!rawName || rawName.startsWith('/') || /(^|\/)\.\.($|\/)/.test(rawName)) {
926
+ skipped.push({ path: rawName || '(empty)', reason: 'Invalid path' });
927
+ continue;
928
+ }
929
+
930
+ const trimmed = rawName.replace(/\/+$/, '');
931
+ const segments = trimmed.split('/').filter((s) => s.length > 0);
932
+ let invalidReason: string | null = null;
933
+ for (const segment of segments) {
934
+ const reason = validateFilename(segment);
935
+ if (reason) {
936
+ invalidReason = reason;
937
+ break;
938
+ }
939
+ }
940
+ if (invalidReason) {
941
+ skipped.push({ path: rawName, reason: invalidReason });
942
+ continue;
943
+ }
944
+
945
+ const targetAbsolute = path.resolve(destAbsolute, ...segments);
946
+ const destRoot = path.resolve(destAbsolute);
947
+ if (targetAbsolute !== destRoot && !targetAbsolute.startsWith(destRoot + path.sep)) {
948
+ skipped.push({ path: rawName, reason: 'Path escapes destination' });
949
+ continue;
950
+ }
951
+
952
+ const relForReport = path
953
+ .relative(workspaceDir, targetAbsolute)
954
+ .replace(/\\/g, '/');
955
+ // Ontology-session boundary: a write-blocked (or cross-ontology) entry is
956
+ // skipped, not extracted, so an archive can't be a write path around it.
957
+ // Runs before any filesystem side effect — including directory creation —
958
+ // so a blocked entry leaves nothing behind on disk.
959
+ const allowEntry = async (): Promise<boolean> => {
960
+ if (!guardWrite) return true;
961
+ try {
962
+ await guardWrite(relForReport);
963
+ return true;
964
+ } catch (err) {
965
+ skipped.push({ path: rawName, reason: err instanceof Error ? err.message : 'Blocked by the ontology-session boundary' });
966
+ return false;
967
+ }
968
+ };
969
+
970
+ if (entry.isDirectory) {
971
+ if (!(await allowEntry())) continue;
972
+ await fs.mkdir(targetAbsolute, { recursive: true });
973
+ continue;
974
+ }
975
+
976
+ const declaredSize = entry.header?.size ?? 0;
977
+ if (declaredSize > UNZIP_MAX_ENTRY_BYTES) {
978
+ skipped.push({
979
+ path: rawName,
980
+ reason: `Entry exceeds ${UNZIP_MAX_ENTRY_BYTES} byte per-file limit`,
981
+ });
982
+ continue;
983
+ }
984
+ if (totalUncompressed + declaredSize > UNZIP_MAX_TOTAL_BYTES) {
985
+ skipped.push({
986
+ path: rawName,
987
+ reason: `Archive exceeds ${UNZIP_MAX_TOTAL_BYTES} byte total uncompressed limit`,
988
+ });
989
+ continue;
990
+ }
991
+
992
+ const data = entry.getData();
993
+ if (data.byteLength > UNZIP_MAX_ENTRY_BYTES) {
994
+ skipped.push({
995
+ path: rawName,
996
+ reason: `Entry exceeds ${UNZIP_MAX_ENTRY_BYTES} byte per-file limit`,
997
+ });
998
+ continue;
999
+ }
1000
+ if (totalUncompressed + data.byteLength > UNZIP_MAX_TOTAL_BYTES) {
1001
+ skipped.push({
1002
+ path: rawName,
1003
+ reason: `Archive exceeds ${UNZIP_MAX_TOTAL_BYTES} byte total uncompressed limit`,
1004
+ });
1005
+ continue;
1006
+ }
1007
+ if (!(await allowEntry())) continue;
1008
+ await fs.mkdir(path.dirname(targetAbsolute), { recursive: true });
1009
+ await fs.writeFile(targetAbsolute, data);
1010
+ totalUncompressed += data.byteLength;
1011
+ extracted.push(relForReport);
1012
+ await this.diffService?.syncFromDisk(workspaceId, relForReport);
1013
+ }
1014
+
1015
+ return {
1016
+ destination: path.relative(workspaceDir, destAbsolute).replace(/\\/g, '/') || '.',
1017
+ extracted,
1018
+ skipped,
1019
+ };
1020
+ }
1021
+
1022
+ async getWorkspacePath(workspaceId: string): Promise<string> {
1023
+ return this.resolveWorkspaceDir(workspaceId);
1024
+ }
1025
+
1026
+ /**
1027
+ * True when `workspaceId` already has a bootstrapped clone — either in the
1028
+ * in-process cache or persisted on disk — WITHOUT triggering the lazy
1029
+ * bootstrap that `resolveWorkspaceDir` does. Lets best-effort callers
1030
+ * (e.g. chat-upload staging) refuse to clone an arbitrary branch supplied
1031
+ * in a request body. Per-branch access itself is open by design, so this is
1032
+ * an IO guard, not an ownership check.
1033
+ */
1034
+ async hasBootstrappedWorkspace(workspaceId: string): Promise<boolean> {
1035
+ if (!workspaceId) return false;
1036
+ let branch: string;
1037
+ try {
1038
+ branch = branchForWorkspaceId(workspaceId);
1039
+ } catch {
1040
+ return false;
1041
+ }
1042
+ if (this.branchDirs.has(branch)) return true;
1043
+ let workspaceDir: string;
1044
+ try {
1045
+ workspaceDir = this.workspaceDirWithinRoot(workspaceId);
1046
+ } catch {
1047
+ return false;
1048
+ }
1049
+ try {
1050
+ await fs.access(path.join(workspaceDir, this.kbDirName, '.git'));
1051
+ return true;
1052
+ } catch {
1053
+ return false;
1054
+ }
1055
+ }
1056
+
1057
+ /**
1058
+ * Enumerate every branch this process has bootstrapped a workspace for.
1059
+ * Used by the `PendingCommitsWorker` to know which workspaces to poll
1060
+ * and by `startupReconcile` to know which workspaces to scan for
1061
+ * orphan files. The returned descriptors match the worker's
1062
+ * `WorkspaceProvider` contract — id + branch in one struct, no need
1063
+ * for the caller to re-encode.
1064
+ *
1065
+ * The cache is populated lazily as `getOrCreateForBranch` resolves, so
1066
+ * a freshly-touched branch shows up here within the same poll cycle
1067
+ * that resolved its bootstrap.
1068
+ */
1069
+ knownWorkspaces(): Array<{ id: string; branch: string }> {
1070
+ return Array.from(this.branchDirs.keys()).map((branch) => ({
1071
+ id: workspaceIdForBranch(branch),
1072
+ branch,
1073
+ }));
1074
+ }
1075
+
1076
+ /**
1077
+ * Walk the working tree of `workspaceId`'s clone and return every
1078
+ * path `git status --porcelain=v1` reports as dirty (untracked,
1079
+ * modified, or staged). Used by `PendingCommitsService.startupReconcile`
1080
+ * to enqueue commits for files left orphaned by a prior process
1081
+ * crash — under save=share + the pending-commits queue the working
1082
+ * tree should never be dirty in steady state, so anything reported
1083
+ * here is recovery material.
1084
+ *
1085
+ * Paths are workspace-relative (`<kbDirName>/<repo-relative>`), matching
1086
+ * the form `releaseLock` expects.
1087
+ *
1088
+ * Returns an empty array if the workspace dir isn't yet bootstrapped
1089
+ * or the repo dir is missing; the worker treats that as "nothing to
1090
+ * recover here" and moves on.
1091
+ */
1092
+ async scanOrphanedPaths(
1093
+ workspaceId: string,
1094
+ ): Promise<Array<{ path: string }>> {
1095
+ let repoDir: string;
1096
+ try {
1097
+ const workspaceDir = await this.resolveWorkspaceDir(workspaceId);
1098
+ repoDir = path.join(workspaceDir, this.kbDirName);
1099
+ // Confirm the inner repo dir actually exists — getOrCreateForBranch
1100
+ // may have failed mid-bootstrap and we'd otherwise hit a confusing
1101
+ // git error here.
1102
+ await fs.access(path.join(repoDir, '.git'));
1103
+ } catch {
1104
+ return [];
1105
+ }
1106
+ let stdout: string;
1107
+ try {
1108
+ ({ stdout } = await execFileAsync('git', ['-C', repoDir, 'status', '--porcelain=v1', '-z'], {
1109
+ encoding: 'utf-8',
1110
+ maxBuffer: 16 * 1024 * 1024,
1111
+ }));
1112
+ } catch (err) {
1113
+ console.warn(
1114
+ `[workspace] orphan scan via git status failed for ${workspaceId}:`,
1115
+ err instanceof Error ? err.message : err,
1116
+ );
1117
+ return [];
1118
+ }
1119
+ if (!stdout.trim()) return [];
1120
+ // `-z` separates entries with NUL and skips quoting; renames use a
1121
+ // second NUL-terminated field for the original path which we ignore.
1122
+ const entries = stdout.split('\0').filter((s) => s.length > 0);
1123
+ const out: Array<{ path: string }> = [];
1124
+ for (let i = 0; i < entries.length; i++) {
1125
+ const entry = entries[i];
1126
+ // First 3 chars are status codes; the rest is the path.
1127
+ if (entry.length < 4) continue;
1128
+ const code = entry.slice(0, 2);
1129
+ const repoRelative = entry.slice(3);
1130
+ // Renames + copies carry the original path as the next NUL-separated
1131
+ // field. Skip it — we want the new path only.
1132
+ if (code[0] === 'R' || code[0] === 'C') {
1133
+ i += 1;
1134
+ }
1135
+ out.push({ path: `${this.kbDirName}/${repoRelative}` });
1136
+ }
1137
+ return out;
1138
+ }
1139
+
1140
+ /**
1141
+ * Remove on-disk workspace dirs whose branches aren't in `knownBranches`.
1142
+ * Handles two drift sources:
1143
+ * - Branch deleted on origin while the backend was offline (no live
1144
+ * `deleteBranch` call ran, so the clone wasn't cleaned up).
1145
+ * - Local-only experimentation (a branch that never made it to origin).
1146
+ *
1147
+ * `knownBranches` is the set of branch names the caller considers
1148
+ * authoritative — typically the union of local + remote branches from
1149
+ * a recent `git for-each-ref` against one of the live clones.
1150
+ *
1151
+ * Best-effort: removal failures are logged and swallowed so one stuck
1152
+ * directory doesn't poison the whole sweep. Returns the set of removed
1153
+ * workspace ids so callers can log / report cleanup activity.
1154
+ */
1155
+ async sweepOrphanedWorkspaces(
1156
+ knownBranches: Iterable<string>,
1157
+ ): Promise<{ removed: string[] }> {
1158
+ const validIds = new Set<string>();
1159
+ for (const b of knownBranches) validIds.add(workspaceIdForBranch(b));
1160
+
1161
+ let entries: Array<{ name: string; isDirectory: () => boolean }>;
1162
+ try {
1163
+ entries = await fs.readdir(this.workspacesRoot, { withFileTypes: true });
1164
+ } catch {
1165
+ return { removed: [] };
1166
+ }
1167
+
1168
+ const removed: string[] = [];
1169
+ for (const entry of entries) {
1170
+ if (!entry.isDirectory()) continue;
1171
+ if (validIds.has(entry.name)) continue;
1172
+ // Skip dirs that don't look like an encoded workspace id — third-
1173
+ // party tooling sometimes drops scratch dirs alongside ours.
1174
+ try {
1175
+ decodeURIComponent(entry.name);
1176
+ } catch {
1177
+ continue;
1178
+ }
1179
+ const dir = path.join(this.workspacesRoot, entry.name);
1180
+ try {
1181
+ await fs.rm(dir, { recursive: true, force: true });
1182
+ this.branchDirs.delete(branchForWorkspaceId(entry.name));
1183
+ removed.push(entry.name);
1184
+ } catch (err) {
1185
+ console.warn(
1186
+ `[workspace] sweep failed for ${entry.name}:`,
1187
+ err instanceof Error ? err.message : err,
1188
+ );
1189
+ }
1190
+ }
1191
+ return { removed };
1192
+ }
1193
+
1194
+ /**
1195
+ * Resolve a workspace id to its on-disk directory. `workspaceId` is
1196
+ * `encodeURIComponent(branch)` — the decoded branch is the in-memory
1197
+ * cache key, the encoded form is the disk directory name.
1198
+ *
1199
+ * Resolution order:
1200
+ * 1. In-process cache (`branchDirs`) — bootstrap completed earlier this
1201
+ * run, or a prior `resolveWorkspaceDir` populated it from disk.
1202
+ * 2. On-disk lookup — the directory may persist across container
1203
+ * restarts; if `<id>/<kbDirName>/.git` is there, register and return.
1204
+ * 3. **Lazy bootstrap** — clone the branch on demand via
1205
+ * `getOrCreateForBranch`. Without this, any frontend route that
1206
+ * hits `/api/workspace/:id/workflow/...` for a branch the user
1207
+ * hasn't explicitly bootstrapped via `GET /api/workspace?branch=…`
1208
+ * would 500 with "Invalid workspace ID" (real symptom in dev:
1209
+ * `listBranches` / `branch-status` failing right after container
1210
+ * start because the frontend's parallel calls race the initial
1211
+ * bootstrap). Bootstrap is single-flight so concurrent racers
1212
+ * share one clone. A bogus branch name surfaces the underlying
1213
+ * `BranchNameError` / clone failure verbatim — much more useful
1214
+ * than the old generic "Invalid workspace ID".
1215
+ *
1216
+ * Cold-cache cost: a mid-request clone is multi-second. Subsequent
1217
+ * requests use the cache. Acceptable since it only happens once per
1218
+ * branch per container lifetime.
1219
+ */
1220
+ /**
1221
+ * Resolve `<workspacesRoot>/<workspaceId>` and assert it stays under the
1222
+ * root. `workspaceId` is expected to be `encodeURIComponent(branch)`, but a
1223
+ * malformed caller value (e.g. `..`) could otherwise escape the root before
1224
+ * the `.git` probe runs. Mirrors `clearTmp`'s containment guard.
1225
+ */
1226
+ private workspaceDirWithinRoot(workspaceId: string): string {
1227
+ const root = path.resolve(this.workspacesRoot);
1228
+ const dir = path.resolve(root, workspaceId);
1229
+ if (dir !== root && !dir.startsWith(root + path.sep)) {
1230
+ throw new Error(`Invalid workspace ID "${workspaceId}": path escapes the workspaces root.`);
1231
+ }
1232
+ return dir;
1233
+ }
1234
+
1235
+ private async resolveWorkspaceDir(workspaceId: string): Promise<string> {
1236
+ if (!workspaceId) throw new Error('Invalid workspace ID');
1237
+ const branch = branchForWorkspaceId(workspaceId);
1238
+ if (this.branchDirs.has(branch)) {
1239
+ return this.branchDirs.get(branch)!;
1240
+ }
1241
+ // Wait for any in-flight bootstrap before trusting the on-disk check.
1242
+ // `git clone` creates `.git` early in a multi-second clone, before the
1243
+ // working tree files (roles.yaml, etc.) are checked out — so a naive
1244
+ // fs.access(.git) on a mid-clone dir returns "ready" and the caller's
1245
+ // next readFile faceplants with ENOENT and a 500. Mirrors the
1246
+ // single-flight wait inside `getOrCreateForBranch`.
1247
+ await this.awaitBootstrapOrEvict(branch).catch(() => undefined);
1248
+ if (this.branchDirs.has(branch)) {
1249
+ return this.branchDirs.get(branch)!;
1250
+ }
1251
+ const workspaceDir = this.workspaceDirWithinRoot(workspaceId);
1252
+ try {
1253
+ await fs.access(path.join(workspaceDir, this.kbDirName, '.git'));
1254
+ this.branchDirs.set(branch, workspaceDir);
1255
+ return workspaceDir;
1256
+ } catch {
1257
+ // Fall through to lazy bootstrap.
1258
+ }
1259
+ try {
1260
+ await this.getOrCreateForBranch(branch);
1261
+ const dir = this.branchDirs.get(branch);
1262
+ if (dir) return dir;
1263
+ // Bootstrap reported success but didn't register — defensive fallback.
1264
+ return workspaceDir;
1265
+ } catch (err) {
1266
+ const reason = err instanceof Error ? err.message : String(err);
1267
+ throw new Error(`Invalid workspace ID "${workspaceId}": ${reason}`);
1268
+ }
1269
+ }
1270
+
1271
+ private assertWithinWorkspace(absolutePath: string, workspaceDir: string): void {
1272
+ const resolved = path.resolve(absolutePath);
1273
+ const root = path.resolve(workspaceDir);
1274
+ if (!resolved.startsWith(root + path.sep) && resolved !== root) {
1275
+ throw new Error('Path traversal detected');
1276
+ }
1277
+ }
1278
+
1279
+ private async buildFileTree(
1280
+ dir: string,
1281
+ workspaceRoot: string,
1282
+ parentIgnore: BevelIgnoreStack,
1283
+ readFilter?: ReadTreeFilter,
1284
+ ): Promise<FileTreeEntry> {
1285
+ const name = path.basename(dir);
1286
+ const relativePath = path.relative(workspaceRoot, dir).replace(/\\/g, '/');
1287
+ const entries = await fs.readdir(dir, { withFileTypes: true });
1288
+
1289
+ const ignoreStack = await parentIgnore.extendedWith(dir);
1290
+
1291
+ // First pass: the entries surviving `.git`/`.gitkeep` + `.bevelignore`.
1292
+ const candidates: { entry: (typeof entries)[number]; entryPath: string; rel: string }[] = [];
1293
+ for (const entry of entries) {
1294
+ // `.git/` is never user content and listing it would blow up the
1295
+ // tree response. `.gitkeep` is the empty-folder placeholder — not
1296
+ // worth surfacing in the file list.
1297
+ if (entry.name === '.git' && entry.isDirectory()) continue;
1298
+ if (entry.name === '.gitkeep' && entry.isFile()) continue;
1299
+
1300
+ const entryPath = path.join(dir, entry.name);
1301
+ if (ignoreStack.isIgnored(entryPath, entry.isDirectory())) continue;
1302
+
1303
+ candidates.push({
1304
+ entry,
1305
+ entryPath,
1306
+ rel: path.relative(workspaceRoot, entryPath).replace(/\\/g, '/'),
1307
+ });
1308
+ }
1309
+
1310
+ // Read-permission filter: ONE batched check per directory. Drops files AND
1311
+ // directories the caller can't read; a hidden directory's subtree is never
1312
+ // walked (skip-and-don't-recurse). A readable directory left empty after
1313
+ // filtering stays visible (the folder itself is readable). Fail-closed:
1314
+ // anything not explicitly readable is dropped. No filter → identical to the
1315
+ // pre-feature tree (regression-safe).
1316
+ let visible = candidates;
1317
+ if (readFilter && candidates.length > 0) {
1318
+ const verdict = await readFilter(candidates.map((c) => c.rel));
1319
+ visible = candidates.filter((c) => verdict.get(c.rel) === true);
1320
+ }
1321
+
1322
+ const children: FileTreeEntry[] = [];
1323
+ for (const { entry, entryPath, rel } of visible) {
1324
+ if (entry.isDirectory()) {
1325
+ children.push(await this.buildFileTree(entryPath, workspaceRoot, ignoreStack, readFilter));
1326
+ } else {
1327
+ children.push({ name: entry.name, relativePath: rel, type: 'file' });
1328
+ }
1329
+ }
1330
+
1331
+ children.sort((a, b) => {
1332
+ if (a.type !== b.type) return a.type === 'directory' ? -1 : 1;
1333
+ return a.name.localeCompare(b.name);
1334
+ });
1335
+
1336
+ return {
1337
+ name,
1338
+ relativePath: relativePath || '.',
1339
+ type: 'directory',
1340
+ children,
1341
+ };
1342
+ }
1343
+ }