@bevel-software/platform-core-backend 0.26.0 → 0.27.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 (613) hide show
  1. package/agent-guide/access-control.md +17 -0
  2. package/agent-guide/conventions.md +8 -0
  3. package/agent-guide/html-views.md +83 -0
  4. package/agent-guide/new-knowledge-base.md +24 -0
  5. package/agent-guide/tool-manuals.md +10 -0
  6. package/dist/assets.d.ts +19 -0
  7. package/dist/assets.d.ts.map +1 -1
  8. package/dist/assets.js +26 -2
  9. package/dist/assets.js.map +1 -1
  10. package/dist/core/catalog-revision.d.ts +19 -1
  11. package/dist/core/catalog-revision.d.ts.map +1 -1
  12. package/dist/core/catalog-revision.js +14 -3
  13. package/dist/core/catalog-revision.js.map +1 -1
  14. package/dist/core/create-core-server.d.ts +5 -0
  15. package/dist/core/create-core-server.d.ts.map +1 -1
  16. package/dist/core/create-core-server.js +54 -3
  17. package/dist/core/create-core-server.js.map +1 -1
  18. package/dist/core/create-core-services.d.ts +26 -0
  19. package/dist/core/create-core-services.d.ts.map +1 -1
  20. package/dist/core/create-core-services.js +100 -8
  21. package/dist/core/create-core-services.js.map +1 -1
  22. package/dist/core/lifecycle.d.ts +16 -1
  23. package/dist/core/lifecycle.d.ts.map +1 -1
  24. package/dist/core/lifecycle.js +18 -1
  25. package/dist/core/lifecycle.js.map +1 -1
  26. package/dist/core-config.d.ts +21 -0
  27. package/dist/core-config.d.ts.map +1 -1
  28. package/dist/core-config.js +22 -1
  29. package/dist/core-config.js.map +1 -1
  30. package/dist/index.d.ts +1 -1
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js +2 -2
  33. package/dist/index.js.map +1 -1
  34. package/dist/modules/access/access-control.interface.d.ts +8 -0
  35. package/dist/modules/access/access-control.interface.d.ts.map +1 -1
  36. package/dist/modules/access/access-control.service.d.ts +1 -0
  37. package/dist/modules/access/access-control.service.d.ts.map +1 -1
  38. package/dist/modules/access/access-control.service.js +11 -0
  39. package/dist/modules/access/access-control.service.js.map +1 -1
  40. package/dist/modules/access/directory-sync-bot.js +1 -1
  41. package/dist/modules/access/directory-sync-bot.js.map +1 -1
  42. package/dist/modules/agent-guide/agent-guide.d.ts +6 -0
  43. package/dist/modules/agent-guide/agent-guide.d.ts.map +1 -1
  44. package/dist/modules/agent-guide/agent-guide.js +8 -0
  45. package/dist/modules/agent-guide/agent-guide.js.map +1 -1
  46. package/dist/modules/agent-guide/index.d.ts +1 -1
  47. package/dist/modules/agent-guide/index.d.ts.map +1 -1
  48. package/dist/modules/agent-guide/index.js +1 -1
  49. package/dist/modules/agent-guide/index.js.map +1 -1
  50. package/dist/modules/agent-instructions/compose.d.ts +5 -0
  51. package/dist/modules/agent-instructions/compose.d.ts.map +1 -1
  52. package/dist/modules/agent-instructions/compose.js +19 -2
  53. package/dist/modules/agent-instructions/compose.js.map +1 -1
  54. package/dist/modules/agent-instructions/shared-file-rules.js +19 -19
  55. package/dist/modules/agent-instructions/shared-file-rules.js.map +1 -1
  56. package/dist/modules/audit/agent-audit.service.d.ts +21 -2
  57. package/dist/modules/audit/agent-audit.service.d.ts.map +1 -1
  58. package/dist/modules/audit/agent-audit.service.js +43 -2
  59. package/dist/modules/audit/agent-audit.service.js.map +1 -1
  60. package/dist/modules/audit/agent-connection.routes.d.ts +34 -0
  61. package/dist/modules/audit/agent-connection.routes.d.ts.map +1 -0
  62. package/dist/modules/audit/agent-connection.routes.js +38 -0
  63. package/dist/modules/audit/agent-connection.routes.js.map +1 -0
  64. package/dist/modules/audit/audit.contract.d.ts +24 -0
  65. package/dist/modules/audit/audit.contract.d.ts.map +1 -1
  66. package/dist/modules/audit/audit.contract.js.map +1 -1
  67. package/dist/modules/audit/index.d.ts +1 -0
  68. package/dist/modules/audit/index.d.ts.map +1 -1
  69. package/dist/modules/audit/index.js +1 -0
  70. package/dist/modules/audit/index.js.map +1 -1
  71. package/dist/modules/auth/account.routes.d.ts.map +1 -1
  72. package/dist/modules/auth/account.routes.js +10 -3
  73. package/dist/modules/auth/account.routes.js.map +1 -1
  74. package/dist/modules/auth/auth.middleware.d.ts +7 -0
  75. package/dist/modules/auth/auth.middleware.d.ts.map +1 -1
  76. package/dist/modules/auth/auth.middleware.js +1 -1
  77. package/dist/modules/auth/auth.middleware.js.map +1 -1
  78. package/dist/modules/code-mode/code-mode.tool.d.ts.map +1 -1
  79. package/dist/modules/code-mode/code-mode.tool.js +11 -3
  80. package/dist/modules/code-mode/code-mode.tool.js.map +1 -1
  81. package/dist/modules/database/core-schema.d.ts +96 -0
  82. package/dist/modules/database/core-schema.d.ts.map +1 -1
  83. package/dist/modules/database/core-schema.js +32 -3
  84. package/dist/modules/database/core-schema.js.map +1 -1
  85. package/dist/modules/embed/account-link.service.d.ts +42 -0
  86. package/dist/modules/embed/account-link.service.d.ts.map +1 -0
  87. package/dist/modules/embed/account-link.service.js +50 -0
  88. package/dist/modules/embed/account-link.service.js.map +1 -0
  89. package/dist/modules/embed/embed-link.d.ts +58 -0
  90. package/dist/modules/embed/embed-link.d.ts.map +1 -0
  91. package/dist/modules/embed/embed-link.js +160 -0
  92. package/dist/modules/embed/embed-link.js.map +1 -0
  93. package/dist/modules/embed/embed.errors.d.ts +30 -0
  94. package/dist/modules/embed/embed.errors.d.ts.map +1 -0
  95. package/dist/modules/embed/embed.errors.js +42 -0
  96. package/dist/modules/embed/embed.errors.js.map +1 -0
  97. package/dist/modules/embed/embed.interface.d.ts +204 -0
  98. package/dist/modules/embed/embed.interface.d.ts.map +1 -0
  99. package/dist/modules/embed/embed.interface.js +2 -0
  100. package/dist/modules/embed/embed.interface.js.map +1 -0
  101. package/dist/modules/embed/embed.routes.d.ts +59 -0
  102. package/dist/modules/embed/embed.routes.d.ts.map +1 -0
  103. package/dist/modules/embed/embed.routes.js +343 -0
  104. package/dist/modules/embed/embed.routes.js.map +1 -0
  105. package/dist/modules/embed/embed.service.d.ts +169 -0
  106. package/dist/modules/embed/embed.service.d.ts.map +1 -0
  107. package/dist/modules/embed/embed.service.js +627 -0
  108. package/dist/modules/embed/embed.service.js.map +1 -0
  109. package/dist/modules/embed/embed.tools.d.ts +51 -0
  110. package/dist/modules/embed/embed.tools.d.ts.map +1 -0
  111. package/dist/modules/embed/embed.tools.js +170 -0
  112. package/dist/modules/embed/embed.tools.js.map +1 -0
  113. package/dist/modules/embed/mcp-app.d.ts +64 -0
  114. package/dist/modules/embed/mcp-app.d.ts.map +1 -0
  115. package/dist/modules/embed/mcp-app.js +147 -0
  116. package/dist/modules/embed/mcp-app.js.map +1 -0
  117. package/dist/modules/embed/mcp-app.routes.d.ts +21 -0
  118. package/dist/modules/embed/mcp-app.routes.d.ts.map +1 -0
  119. package/dist/modules/embed/mcp-app.routes.js +36 -0
  120. package/dist/modules/embed/mcp-app.routes.js.map +1 -0
  121. package/dist/modules/github-app/github-app.connection.d.ts +15 -0
  122. package/dist/modules/github-app/github-app.connection.d.ts.map +1 -1
  123. package/dist/modules/github-app/github-app.connection.js +34 -3
  124. package/dist/modules/github-app/github-app.connection.js.map +1 -1
  125. package/dist/modules/kb-fs/locking-filesystem.d.ts +65 -0
  126. package/dist/modules/kb-fs/locking-filesystem.d.ts.map +1 -1
  127. package/dist/modules/kb-fs/locking-filesystem.js +300 -0
  128. package/dist/modules/kb-fs/locking-filesystem.js.map +1 -1
  129. package/dist/modules/kb-sync/kb-sync.interface.d.ts +5 -0
  130. package/dist/modules/kb-sync/kb-sync.interface.d.ts.map +1 -1
  131. package/dist/modules/kb-sync/kb-sync.service.d.ts.map +1 -1
  132. package/dist/modules/kb-sync/kb-sync.service.js +6 -0
  133. package/dist/modules/kb-sync/kb-sync.service.js.map +1 -1
  134. package/dist/modules/mcp/mcp-auth.middleware.d.ts.map +1 -1
  135. package/dist/modules/mcp/mcp-auth.middleware.js +2 -1
  136. package/dist/modules/mcp/mcp-auth.middleware.js.map +1 -1
  137. package/dist/modules/mcp/mcp.service.d.ts +28 -1
  138. package/dist/modules/mcp/mcp.service.d.ts.map +1 -1
  139. package/dist/modules/mcp/mcp.service.js +64 -8
  140. package/dist/modules/mcp/mcp.service.js.map +1 -1
  141. package/dist/modules/mcp/oauth/bevel-oauth-provider.d.ts +24 -0
  142. package/dist/modules/mcp/oauth/bevel-oauth-provider.d.ts.map +1 -1
  143. package/dist/modules/mcp/oauth/bevel-oauth-provider.js +32 -4
  144. package/dist/modules/mcp/oauth/bevel-oauth-provider.js.map +1 -1
  145. package/dist/modules/onboarding/onboarding.contract.d.ts +80 -0
  146. package/dist/modules/onboarding/onboarding.contract.d.ts.map +1 -0
  147. package/dist/modules/onboarding/onboarding.contract.js +7 -0
  148. package/dist/modules/onboarding/onboarding.contract.js.map +1 -0
  149. package/dist/modules/onboarding/onboarding.routes.d.ts +18 -0
  150. package/dist/modules/onboarding/onboarding.routes.d.ts.map +1 -0
  151. package/dist/modules/onboarding/onboarding.routes.js +81 -0
  152. package/dist/modules/onboarding/onboarding.routes.js.map +1 -0
  153. package/dist/modules/onboarding/starter-pack.service.d.ts +213 -0
  154. package/dist/modules/onboarding/starter-pack.service.d.ts.map +1 -0
  155. package/dist/modules/onboarding/starter-pack.service.js +492 -0
  156. package/dist/modules/onboarding/starter-pack.service.js.map +1 -0
  157. package/dist/modules/onboarding/starter-packs.d.ts +78 -0
  158. package/dist/modules/onboarding/starter-packs.d.ts.map +1 -0
  159. package/dist/modules/onboarding/starter-packs.js +156 -0
  160. package/dist/modules/onboarding/starter-packs.js.map +1 -0
  161. package/dist/modules/plugins/index.d.ts +1 -1
  162. package/dist/modules/plugins/index.d.ts.map +1 -1
  163. package/dist/modules/plugins/index.js +1 -1
  164. package/dist/modules/plugins/index.js.map +1 -1
  165. package/dist/modules/plugins/plugin-provision.service.d.ts +19 -0
  166. package/dist/modules/plugins/plugin-provision.service.d.ts.map +1 -1
  167. package/dist/modules/plugins/plugin-provision.service.js +18 -1
  168. package/dist/modules/plugins/plugin-provision.service.js.map +1 -1
  169. package/dist/modules/plugins/plugins.routes.d.ts +0 -36
  170. package/dist/modules/plugins/plugins.routes.d.ts.map +1 -1
  171. package/dist/modules/plugins/plugins.routes.js +31 -0
  172. package/dist/modules/plugins/plugins.routes.js.map +1 -1
  173. package/dist/modules/settings/deployment-settings.service.d.ts +34 -0
  174. package/dist/modules/settings/deployment-settings.service.d.ts.map +1 -1
  175. package/dist/modules/settings/deployment-settings.service.js +134 -1
  176. package/dist/modules/settings/deployment-settings.service.js.map +1 -1
  177. package/dist/modules/settings/repository-source.d.ts +1 -0
  178. package/dist/modules/settings/repository-source.d.ts.map +1 -1
  179. package/dist/modules/settings/repository-source.js +3 -3
  180. package/dist/modules/settings/repository-source.js.map +1 -1
  181. package/dist/modules/skills/skills.tools.d.ts.map +1 -1
  182. package/dist/modules/skills/skills.tools.js +50 -32
  183. package/dist/modules/skills/skills.tools.js.map +1 -1
  184. package/dist/modules/tool-auth/connection-key-rejection.d.ts +10 -1
  185. package/dist/modules/tool-auth/connection-key-rejection.d.ts.map +1 -1
  186. package/dist/modules/tool-auth/connection-key-rejection.js +11 -3
  187. package/dist/modules/tool-auth/connection-key-rejection.js.map +1 -1
  188. package/dist/modules/tool-auth/external-api-key.interface.d.ts +8 -0
  189. package/dist/modules/tool-auth/external-api-key.interface.d.ts.map +1 -1
  190. package/dist/modules/tool-auth/external-api-key.service.d.ts +24 -2
  191. package/dist/modules/tool-auth/external-api-key.service.d.ts.map +1 -1
  192. package/dist/modules/tool-auth/external-api-key.service.js +59 -2
  193. package/dist/modules/tool-auth/external-api-key.service.js.map +1 -1
  194. package/dist/modules/tool-auth/tool-auth.middleware.d.ts.map +1 -1
  195. package/dist/modules/tool-auth/tool-auth.middleware.js +9 -4
  196. package/dist/modules/tool-auth/tool-auth.middleware.js.map +1 -1
  197. package/dist/modules/tool-helpers/index.d.ts +28 -1
  198. package/dist/modules/tool-helpers/index.d.ts.map +1 -1
  199. package/dist/modules/tool-helpers/index.js +28 -1
  200. package/dist/modules/tool-helpers/index.js.map +1 -1
  201. package/dist/modules/tool-helpers/route-argument-check.d.ts +10 -0
  202. package/dist/modules/tool-helpers/route-argument-check.d.ts.map +1 -0
  203. package/dist/modules/tool-helpers/route-argument-check.js +132 -0
  204. package/dist/modules/tool-helpers/route-argument-check.js.map +1 -0
  205. package/dist/modules/tool-helpers/route-tool-schemas.d.ts +89 -0
  206. package/dist/modules/tool-helpers/route-tool-schemas.d.ts.map +1 -0
  207. package/dist/modules/tool-helpers/route-tool-schemas.js +113 -0
  208. package/dist/modules/tool-helpers/route-tool-schemas.js.map +1 -0
  209. package/dist/modules/tool-helpers/tool-def.d.ts +71 -0
  210. package/dist/modules/tool-helpers/tool-def.d.ts.map +1 -1
  211. package/dist/modules/tool-helpers/tool-def.js +70 -6
  212. package/dist/modules/tool-helpers/tool-def.js.map +1 -1
  213. package/dist/modules/tool-helpers/tool-handler.d.ts +46 -6
  214. package/dist/modules/tool-helpers/tool-handler.d.ts.map +1 -1
  215. package/dist/modules/tool-helpers/tool-handler.js +157 -8
  216. package/dist/modules/tool-helpers/tool-handler.js.map +1 -1
  217. package/dist/modules/tool-helpers/tool.contract.d.ts +9 -0
  218. package/dist/modules/tool-helpers/tool.contract.d.ts.map +1 -1
  219. package/dist/modules/tool-helpers/tool.contract.js.map +1 -1
  220. package/dist/modules/tool-manuals/tool-manuals.tools.d.ts.map +1 -1
  221. package/dist/modules/tool-manuals/tool-manuals.tools.js +14 -5
  222. package/dist/modules/tool-manuals/tool-manuals.tools.js.map +1 -1
  223. package/dist/modules/tool-registry/description-length.d.ts +6 -2
  224. package/dist/modules/tool-registry/description-length.d.ts.map +1 -1
  225. package/dist/modules/tool-registry/description-length.js +27 -5
  226. package/dist/modules/tool-registry/description-length.js.map +1 -1
  227. package/dist/modules/tool-registry/guide-first.d.ts +4 -0
  228. package/dist/modules/tool-registry/guide-first.d.ts.map +1 -1
  229. package/dist/modules/tool-registry/guide-first.js +9 -1
  230. package/dist/modules/tool-registry/guide-first.js.map +1 -1
  231. package/dist/modules/workflow/agent-tools/change-request-read.tools.d.ts +53 -0
  232. package/dist/modules/workflow/agent-tools/change-request-read.tools.d.ts.map +1 -1
  233. package/dist/modules/workflow/agent-tools/change-request-read.tools.js +97 -68
  234. package/dist/modules/workflow/agent-tools/change-request-read.tools.js.map +1 -1
  235. package/dist/modules/workflow/agent-tools/workflow.tools.d.ts +8 -1
  236. package/dist/modules/workflow/agent-tools/workflow.tools.d.ts.map +1 -1
  237. package/dist/modules/workflow/agent-tools/workflow.tools.js +124 -37
  238. package/dist/modules/workflow/agent-tools/workflow.tools.js.map +1 -1
  239. package/dist/modules/workflow/file-lock.service.d.ts +29 -5
  240. package/dist/modules/workflow/file-lock.service.d.ts.map +1 -1
  241. package/dist/modules/workflow/file-lock.service.js +62 -5
  242. package/dist/modules/workflow/file-lock.service.js.map +1 -1
  243. package/dist/modules/workflow/git/git.service.d.ts +72 -1
  244. package/dist/modules/workflow/git/git.service.d.ts.map +1 -1
  245. package/dist/modules/workflow/git/git.service.js +172 -36
  246. package/dist/modules/workflow/git/git.service.js.map +1 -1
  247. package/dist/modules/workflow/git/node-git-runner.d.ts +15 -0
  248. package/dist/modules/workflow/git/node-git-runner.d.ts.map +1 -1
  249. package/dist/modules/workflow/git/node-git-runner.js +78 -6
  250. package/dist/modules/workflow/git/node-git-runner.js.map +1 -1
  251. package/dist/modules/workflow/pending-commits.service.d.ts +15 -0
  252. package/dist/modules/workflow/pending-commits.service.d.ts.map +1 -1
  253. package/dist/modules/workflow/pending-commits.service.js +25 -1
  254. package/dist/modules/workflow/pending-commits.service.js.map +1 -1
  255. package/dist/modules/workflow/pending-commits.worker.d.ts +6 -1
  256. package/dist/modules/workflow/pending-commits.worker.d.ts.map +1 -1
  257. package/dist/modules/workflow/pending-commits.worker.js +23 -6
  258. package/dist/modules/workflow/pending-commits.worker.js.map +1 -1
  259. package/dist/modules/workflow/recovery-bot.js +1 -1
  260. package/dist/modules/workflow/recovery-bot.js.map +1 -1
  261. package/dist/modules/workflow/review-workflow/review-workflow.service.d.ts.map +1 -1
  262. package/dist/modules/workflow/review-workflow/review-workflow.service.js +2 -1
  263. package/dist/modules/workflow/review-workflow/review-workflow.service.js.map +1 -1
  264. package/dist/modules/workflow/sanitize-error.d.ts +16 -0
  265. package/dist/modules/workflow/sanitize-error.d.ts.map +1 -1
  266. package/dist/modules/workflow/sanitize-error.js +45 -0
  267. package/dist/modules/workflow/sanitize-error.js.map +1 -1
  268. package/dist/modules/workflow/workflow.service.d.ts +183 -15
  269. package/dist/modules/workflow/workflow.service.d.ts.map +1 -1
  270. package/dist/modules/workflow/workflow.service.js +805 -106
  271. package/dist/modules/workflow/workflow.service.js.map +1 -1
  272. package/dist/modules/workspace/agent-download.builder.d.ts +105 -0
  273. package/dist/modules/workspace/agent-download.builder.d.ts.map +1 -0
  274. package/dist/modules/workspace/agent-download.builder.js +238 -0
  275. package/dist/modules/workspace/agent-download.builder.js.map +1 -0
  276. package/dist/modules/workspace/agent-download.routes.d.ts +77 -0
  277. package/dist/modules/workspace/agent-download.routes.d.ts.map +1 -0
  278. package/dist/modules/workspace/agent-download.routes.js +156 -0
  279. package/dist/modules/workspace/agent-download.routes.js.map +1 -0
  280. package/dist/modules/workspace/agent-download.store.d.ts +225 -0
  281. package/dist/modules/workspace/agent-download.store.d.ts.map +1 -0
  282. package/dist/modules/workspace/agent-download.store.js +423 -0
  283. package/dist/modules/workspace/agent-download.store.js.map +1 -0
  284. package/dist/modules/workspace/agent-upload.store.d.ts +10 -1
  285. package/dist/modules/workspace/agent-upload.store.d.ts.map +1 -1
  286. package/dist/modules/workspace/agent-upload.store.js +16 -6
  287. package/dist/modules/workspace/agent-upload.store.js.map +1 -1
  288. package/dist/modules/workspace/file-readers/raw-mime.d.ts +6 -0
  289. package/dist/modules/workspace/file-readers/raw-mime.d.ts.map +1 -0
  290. package/dist/modules/workspace/file-readers/raw-mime.js +38 -0
  291. package/dist/modules/workspace/file-readers/raw-mime.js.map +1 -0
  292. package/dist/modules/workspace/file-readers/text-reader.d.ts +5 -5
  293. package/dist/modules/workspace/file-readers/text-reader.d.ts.map +1 -1
  294. package/dist/modules/workspace/file-readers/text-reader.js +5 -8
  295. package/dist/modules/workspace/file-readers/text-reader.js.map +1 -1
  296. package/dist/modules/workspace/first-run.d.ts +111 -0
  297. package/dist/modules/workspace/first-run.d.ts.map +1 -0
  298. package/dist/modules/workspace/first-run.js +181 -0
  299. package/dist/modules/workspace/first-run.js.map +1 -0
  300. package/dist/modules/workspace/move-links.d.ts +92 -0
  301. package/dist/modules/workspace/move-links.d.ts.map +1 -0
  302. package/dist/modules/workspace/move-links.js +173 -0
  303. package/dist/modules/workspace/move-links.js.map +1 -0
  304. package/dist/modules/workspace/startup/steps/seed-tree.d.ts.map +1 -1
  305. package/dist/modules/workspace/startup/steps/seed-tree.js +3 -13
  306. package/dist/modules/workspace/startup/steps/seed-tree.js.map +1 -1
  307. package/dist/modules/workspace/workspace.routes.d.ts.map +1 -1
  308. package/dist/modules/workspace/workspace.routes.js +63 -22
  309. package/dist/modules/workspace/workspace.routes.js.map +1 -1
  310. package/dist/modules/workspace/workspace.service.d.ts +75 -2
  311. package/dist/modules/workspace/workspace.service.d.ts.map +1 -1
  312. package/dist/modules/workspace/workspace.service.js +197 -24
  313. package/dist/modules/workspace/workspace.service.js.map +1 -1
  314. package/dist/modules/workspace/workspace.tools.d.ts +59 -1
  315. package/dist/modules/workspace/workspace.tools.d.ts.map +1 -1
  316. package/dist/modules/workspace/workspace.tools.js +443 -65
  317. package/dist/modules/workspace/workspace.tools.js.map +1 -1
  318. package/dist/modules/write-access/write-access.d.ts.map +1 -1
  319. package/dist/modules/write-access/write-access.js +9 -0
  320. package/dist/modules/write-access/write-access.js.map +1 -1
  321. package/dist/shared/domain-errors.d.ts +73 -1
  322. package/dist/shared/domain-errors.d.ts.map +1 -1
  323. package/dist/shared/domain-errors.js +86 -12
  324. package/dist/shared/domain-errors.js.map +1 -1
  325. package/dist/shared/git.contract.d.ts +10 -1
  326. package/dist/shared/git.contract.d.ts.map +1 -1
  327. package/dist/shared/git.contract.js.map +1 -1
  328. package/dist/shared/utf8-text.d.ts +15 -0
  329. package/dist/shared/utf8-text.d.ts.map +1 -0
  330. package/dist/shared/utf8-text.js +20 -0
  331. package/dist/shared/utf8-text.js.map +1 -0
  332. package/dist/tenancy/static-tenant-source.d.ts +2 -1
  333. package/dist/tenancy/static-tenant-source.d.ts.map +1 -1
  334. package/dist/tenancy/static-tenant-source.js +9 -2
  335. package/dist/tenancy/static-tenant-source.js.map +1 -1
  336. package/mcp-app/page.html +349 -0
  337. package/migrations/0017_atlassian_account_links.sql +37 -0
  338. package/migrations/meta/0017_snapshot.json +2396 -0
  339. package/migrations/meta/_journal.json +8 -1
  340. package/package.json +5 -3
  341. package/src/__tests__/packaged-assets-ship.test.ts +16 -4
  342. package/src/assets.ts +28 -2
  343. package/src/core/__tests__/catalog-revision.test.ts +40 -0
  344. package/src/core/__tests__/gated-boot.test.ts +1 -0
  345. package/src/core/__tests__/lifecycle.test.ts +14 -0
  346. package/src/core/catalog-revision.ts +15 -2
  347. package/src/core/create-core-server.ts +67 -3
  348. package/src/core/create-core-services.ts +140 -6
  349. package/src/core/lifecycle.ts +28 -2
  350. package/src/core-config.ts +24 -1
  351. package/src/index.ts +2 -2
  352. package/src/modules/access/__tests__/access-control.service.test.ts +19 -0
  353. package/src/modules/access/access-control.interface.ts +13 -0
  354. package/src/modules/access/access-control.service.ts +16 -0
  355. package/src/modules/access/directory-sync-bot.ts +1 -1
  356. package/src/modules/agent-guide/__tests__/agent-guide.test.ts +58 -0
  357. package/src/modules/agent-guide/__tests__/agent-guide.tools.test.ts +19 -1
  358. package/src/modules/agent-guide/agent-guide.ts +9 -0
  359. package/src/modules/agent-guide/index.ts +1 -0
  360. package/src/modules/agent-instructions/__tests__/shared-file-rules.test.ts +14 -0
  361. package/src/modules/agent-instructions/compose.ts +17 -2
  362. package/src/modules/agent-instructions/shared-file-rules.ts +19 -19
  363. package/src/modules/audit/__tests__/agent-audit.service.test.ts +64 -4
  364. package/src/modules/audit/__tests__/agent-connection.routes.test.ts +102 -0
  365. package/src/modules/audit/agent-audit.service.ts +58 -3
  366. package/src/modules/audit/agent-connection.routes.ts +56 -0
  367. package/src/modules/audit/audit.contract.ts +26 -0
  368. package/src/modules/audit/index.ts +1 -0
  369. package/src/modules/auth/__tests__/account.routes.test.ts +5 -3
  370. package/src/modules/auth/account.routes.ts +10 -3
  371. package/src/modules/auth/auth.middleware.ts +1 -1
  372. package/src/modules/code-mode/__tests__/code-mode.tool.test.ts +18 -2
  373. package/src/modules/code-mode/code-mode.tool.ts +13 -2
  374. package/src/modules/database/core-schema.ts +33 -3
  375. package/src/modules/embed/__tests__/account-links.migration.test.ts +98 -0
  376. package/src/modules/embed/__tests__/embed-link.test.ts +122 -0
  377. package/src/modules/embed/__tests__/embed.routes.test.ts +362 -0
  378. package/src/modules/embed/__tests__/embed.service.lock.test.ts +193 -0
  379. package/src/modules/embed/__tests__/embed.service.test.ts +805 -0
  380. package/src/modules/embed/__tests__/embed.tools.test.ts +304 -0
  381. package/src/modules/embed/__tests__/mcp-app.test.ts +207 -0
  382. package/src/modules/embed/account-link.service.ts +76 -0
  383. package/src/modules/embed/embed-link.ts +180 -0
  384. package/src/modules/embed/embed.errors.ts +43 -0
  385. package/src/modules/embed/embed.interface.ts +203 -0
  386. package/src/modules/embed/embed.routes.ts +363 -0
  387. package/src/modules/embed/embed.service.ts +705 -0
  388. package/src/modules/embed/embed.tools.ts +206 -0
  389. package/src/modules/embed/mcp-app.routes.ts +41 -0
  390. package/src/modules/embed/mcp-app.ts +173 -0
  391. package/src/modules/github-app/__tests__/github-app.test.ts +76 -0
  392. package/src/modules/github-app/github-app.connection.ts +38 -4
  393. package/src/modules/kb-fs/__tests__/move-with-edits.test.ts +304 -0
  394. package/src/modules/kb-fs/locking-filesystem.ts +287 -0
  395. package/src/modules/kb-sync/__tests__/kb-sync.service.test.ts +35 -0
  396. package/src/modules/kb-sync/kb-sync.interface.ts +2 -0
  397. package/src/modules/kb-sync/kb-sync.service.ts +6 -0
  398. package/src/modules/mcp/__tests__/bevel-oauth-provider.test.ts +68 -11
  399. package/src/modules/mcp/__tests__/mcp-proxy-helpers.test.ts +37 -2
  400. package/src/modules/mcp/__tests__/mcp.app.e2e.test.ts +200 -0
  401. package/src/modules/mcp/__tests__/mcp.e2e.test.ts +3 -1
  402. package/src/modules/mcp/__tests__/mcp.service.test.ts +23 -13
  403. package/src/modules/mcp/__tests__/tool-call-interface.e2e.test.ts +499 -0
  404. package/src/modules/mcp/mcp-auth.middleware.ts +2 -1
  405. package/src/modules/mcp/mcp.service.ts +81 -12
  406. package/src/modules/mcp/oauth/bevel-oauth-provider.ts +42 -4
  407. package/src/modules/onboarding/__tests__/onboarding.routes.test.ts +163 -0
  408. package/src/modules/onboarding/__tests__/starter-pack.service.test.ts +635 -0
  409. package/src/modules/onboarding/__tests__/starter-packs.test.ts +169 -0
  410. package/src/modules/onboarding/onboarding.contract.ts +85 -0
  411. package/src/modules/onboarding/onboarding.routes.ts +85 -0
  412. package/src/modules/onboarding/starter-pack.service.ts +580 -0
  413. package/src/modules/onboarding/starter-packs.ts +182 -0
  414. package/src/modules/plugins/__tests__/plugins.tools.test.ts +25 -2
  415. package/src/modules/plugins/index.ts +1 -0
  416. package/src/modules/plugins/plugin-provision.service.ts +22 -1
  417. package/src/modules/plugins/plugins.routes.ts +29 -0
  418. package/src/modules/settings/__tests__/deployment-settings.service.test.ts +150 -1
  419. package/src/modules/settings/__tests__/repository-source.test.ts +9 -0
  420. package/src/modules/settings/deployment-settings.service.ts +131 -1
  421. package/src/modules/settings/repository-source.ts +4 -4
  422. package/src/modules/skills/__tests__/skills.tools.description.test.ts +5 -1
  423. package/src/modules/skills/skills.tools.ts +55 -35
  424. package/src/modules/tool-auth/__tests__/connection-key-rejection.test.ts +32 -0
  425. package/src/modules/tool-auth/__tests__/external-api-key.service.test.ts +65 -0
  426. package/src/modules/tool-auth/connection-key-rejection.ts +13 -3
  427. package/src/modules/tool-auth/external-api-key.interface.ts +9 -0
  428. package/src/modules/tool-auth/external-api-key.service.ts +61 -4
  429. package/src/modules/tool-auth/tool-auth.middleware.ts +13 -4
  430. package/src/modules/tool-helpers/__tests__/branch-declaration.test.ts +502 -0
  431. package/src/modules/tool-helpers/__tests__/route-argument-check.test.ts +337 -0
  432. package/src/modules/tool-helpers/index.ts +34 -1
  433. package/src/modules/tool-helpers/route-argument-check.ts +149 -0
  434. package/src/modules/tool-helpers/route-tool-schemas.ts +171 -0
  435. package/src/modules/tool-helpers/tool-def.ts +132 -7
  436. package/src/modules/tool-helpers/tool-handler.ts +196 -8
  437. package/src/modules/tool-helpers/tool.contract.ts +9 -0
  438. package/src/modules/tool-manuals/tool-manuals.tools.ts +15 -5
  439. package/src/modules/tool-registry/__tests__/own-tool-schemas.test.ts +14 -1
  440. package/src/modules/tool-registry/__tests__/tool-description-length.test.ts +23 -10
  441. package/src/modules/tool-registry/description-length.ts +34 -7
  442. package/src/modules/tool-registry/guide-first.ts +8 -1
  443. package/src/modules/workflow/__tests__/file-lock.service.deletion-gate.test.ts +104 -0
  444. package/src/modules/workflow/__tests__/pending-commits.repository-replaced.test.ts +66 -0
  445. package/src/modules/workflow/__tests__/pending-commits.worker.test.ts +31 -2
  446. package/src/modules/workflow/__tests__/preserve-roles-yaml.test.ts +110 -6
  447. package/src/modules/workflow/__tests__/sanitize-error.test.ts +89 -1
  448. package/src/modules/workflow/__tests__/workflow.routes.push-refused.test.ts +202 -0
  449. package/src/modules/workflow/__tests__/workflow.service.branch-in-use.test.ts +6 -6
  450. package/src/modules/workflow/__tests__/workflow.service.delete-branch.test.ts +1026 -0
  451. package/src/modules/workflow/__tests__/workflow.service.facade.test.ts +468 -10
  452. package/src/modules/workflow/__tests__/workflow.service.folder-proposals.test.ts +4 -0
  453. package/src/modules/workflow/__tests__/workflow.service.releaseLock.test.ts +5 -4
  454. package/src/modules/workflow/__tests__/workflow.service.update-from-target.test.ts +54 -2
  455. package/src/modules/workflow/agent-tools/__tests__/change-request-read.tools.test.ts +41 -5
  456. package/src/modules/workflow/agent-tools/__tests__/workflow.tools.test.ts +127 -2
  457. package/src/modules/workflow/agent-tools/change-request-read.tools.ts +157 -122
  458. package/src/modules/workflow/agent-tools/workflow.tools.ts +129 -35
  459. package/src/modules/workflow/file-lock.service.ts +67 -5
  460. package/src/modules/workflow/git/__tests__/git.service.deleteBranch.test.ts +164 -0
  461. package/src/modules/workflow/git/__tests__/git.service.pull.test.ts +48 -1
  462. package/src/modules/workflow/git/__tests__/node-git-runner.test.ts +231 -2
  463. package/src/modules/workflow/git/git.service.ts +176 -38
  464. package/src/modules/workflow/git/node-git-runner.ts +82 -7
  465. package/src/modules/workflow/pending-commits.service.ts +29 -1
  466. package/src/modules/workflow/pending-commits.worker.ts +24 -6
  467. package/src/modules/workflow/recovery-bot.ts +1 -1
  468. package/src/modules/workflow/review-workflow/review-workflow.service.ts +2 -1
  469. package/src/modules/workflow/sanitize-error.ts +50 -0
  470. package/src/modules/workflow/workflow.service.ts +879 -112
  471. package/src/modules/workspace/__tests__/agent-downloads.test.ts +823 -0
  472. package/src/modules/workspace/__tests__/first-run.test.ts +234 -0
  473. package/src/modules/workspace/__tests__/git-internals.security.test.ts +5 -2
  474. package/src/modules/workspace/__tests__/md-links.test.ts +353 -0
  475. package/src/modules/workspace/__tests__/move-links.test.ts +261 -0
  476. package/src/modules/workspace/__tests__/rest-tool-arguments.test.ts +219 -0
  477. package/src/modules/workspace/__tests__/workspace-paths-inside-the-repo.test.ts +3 -0
  478. package/src/modules/workspace/__tests__/workspace.routes.delete.test.ts +102 -0
  479. package/src/modules/workspace/__tests__/workspace.routes.download.test.ts +52 -5
  480. package/src/modules/workspace/__tests__/workspace.routes.folder-zip-access.test.ts +190 -0
  481. package/src/modules/workspace/__tests__/workspace.service.forget-clone-races.test.ts +8 -4
  482. package/src/modules/workspace/__tests__/workspace.service.test.ts +50 -8
  483. package/src/modules/workspace/__tests__/workspace.tools.branch-errors.test.ts +44 -1
  484. package/src/modules/workspace/__tests__/workspace.tools.test.ts +409 -12
  485. package/src/modules/workspace/agent-download.builder.ts +308 -0
  486. package/src/modules/workspace/agent-download.routes.ts +183 -0
  487. package/src/modules/workspace/agent-download.store.ts +533 -0
  488. package/src/modules/workspace/agent-upload.store.ts +23 -7
  489. package/src/modules/workspace/file-readers/raw-mime.ts +40 -0
  490. package/src/modules/workspace/file-readers/text-reader.ts +5 -8
  491. package/src/modules/workspace/first-run.ts +222 -0
  492. package/src/modules/workspace/move-links.ts +237 -0
  493. package/src/modules/workspace/startup/steps/seed-tree.ts +3 -12
  494. package/src/modules/workspace/workspace.routes.ts +64 -22
  495. package/src/modules/workspace/workspace.service.ts +202 -25
  496. package/src/modules/workspace/workspace.tools.ts +492 -63
  497. package/src/modules/write-access/__tests__/write-access.test.ts +13 -0
  498. package/src/modules/write-access/write-access.ts +9 -0
  499. package/src/shared/domain-errors.ts +108 -11
  500. package/src/shared/git.contract.ts +8 -1
  501. package/src/shared/utf8-text.ts +21 -0
  502. package/src/tenancy/__tests__/static-tenant-source.test.ts +1 -0
  503. package/src/tenancy/static-tenant-source.ts +10 -2
  504. package/starter-packs/README.md +103 -0
  505. package/starter-packs/engineering/KnowledgeBase/About us.md +9 -0
  506. package/starter-packs/engineering/KnowledgeBase/Architecture.md +9 -0
  507. package/starter-packs/engineering/KnowledgeBase/Glossary.md +9 -0
  508. package/starter-packs/engineering/KnowledgeBase/How we ship.md +9 -0
  509. package/starter-packs/engineering/Plugins/engineering-starter/LICENSE +21 -0
  510. package/starter-packs/engineering/Plugins/engineering-starter/SOURCE.md +20 -0
  511. package/starter-packs/engineering/Plugins/engineering-starter/access.md +19 -0
  512. package/starter-packs/engineering/Plugins/engineering-starter/plugin.json +10 -0
  513. package/starter-packs/engineering/Plugins/engineering-starter/skills/office-hours/SKILL.md +171 -0
  514. package/starter-packs/engineering/Plugins/engineering-starter/skills/office-hours/sections/phase-2a-startup-diagnostic.md +157 -0
  515. package/starter-packs/engineering/Plugins/engineering-starter/skills/office-hours/sections/phase-2b-builder-brainstorm.md +39 -0
  516. package/starter-packs/engineering/Plugins/engineering-starter/skills/plan-ceo-review/SKILL.md +144 -0
  517. package/starter-packs/engineering/Plugins/engineering-starter/skills/plan-eng-review/SKILL.md +144 -0
  518. package/starter-packs/engineering/Plugins/engineering-starter/skills/review/SKILL.md +80 -0
  519. package/starter-packs/engineering/Plugins/engineering-starter/skills/review/checklist.md +184 -0
  520. package/starter-packs/engineering/pack.yaml +11 -0
  521. package/starter-packs/general/KnowledgeBase/About us.md +9 -0
  522. package/starter-packs/general/KnowledgeBase/Glossary.md +9 -0
  523. package/starter-packs/general/KnowledgeBase/How we work.md +9 -0
  524. package/starter-packs/general/Plugins/general-starter/LICENSE +202 -0
  525. package/starter-packs/general/Plugins/general-starter/SOURCE.md +10 -0
  526. package/starter-packs/general/Plugins/general-starter/access.md +19 -0
  527. package/starter-packs/general/Plugins/general-starter/plugin.json +10 -0
  528. package/starter-packs/general/Plugins/general-starter/skills/memory-management/SKILL.md +332 -0
  529. package/starter-packs/general/Plugins/general-starter/skills/productivity-start/SKILL.md +163 -0
  530. package/starter-packs/general/Plugins/general-starter/skills/productivity-update/SKILL.md +167 -0
  531. package/starter-packs/general/Plugins/general-starter/skills/task-management/SKILL.md +83 -0
  532. package/starter-packs/general/pack.yaml +10 -0
  533. package/starter-packs/operations/KnowledgeBase/Glossary.md +9 -0
  534. package/starter-packs/operations/KnowledgeBase/How we work.md +9 -0
  535. package/starter-packs/operations/KnowledgeBase/Processes.md +9 -0
  536. package/starter-packs/operations/KnowledgeBase/Tools we use.md +9 -0
  537. package/starter-packs/operations/KnowledgeBase/Vendors.md +9 -0
  538. package/starter-packs/operations/Plugins/operations-starter/LICENSE +202 -0
  539. package/starter-packs/operations/Plugins/operations-starter/SOURCE.md +10 -0
  540. package/starter-packs/operations/Plugins/operations-starter/access.md +19 -0
  541. package/starter-packs/operations/Plugins/operations-starter/plugin.json +10 -0
  542. package/starter-packs/operations/Plugins/operations-starter/skills/capacity-plan/SKILL.md +111 -0
  543. package/starter-packs/operations/Plugins/operations-starter/skills/change-request/SKILL.md +119 -0
  544. package/starter-packs/operations/Plugins/operations-starter/skills/compliance-tracking/SKILL.md +44 -0
  545. package/starter-packs/operations/Plugins/operations-starter/skills/process-doc/SKILL.md +80 -0
  546. package/starter-packs/operations/Plugins/operations-starter/skills/process-optimization/SKILL.md +39 -0
  547. package/starter-packs/operations/Plugins/operations-starter/skills/risk-assessment/SKILL.md +40 -0
  548. package/starter-packs/operations/Plugins/operations-starter/skills/runbook/SKILL.md +85 -0
  549. package/starter-packs/operations/Plugins/operations-starter/skills/status-report/SKILL.md +75 -0
  550. package/starter-packs/operations/Plugins/operations-starter/skills/vendor-review/SKILL.md +102 -0
  551. package/starter-packs/operations/pack.yaml +12 -0
  552. package/starter-packs/sales/KnowledgeBase/About us.md +9 -0
  553. package/starter-packs/sales/KnowledgeBase/Customers.md +9 -0
  554. package/starter-packs/sales/KnowledgeBase/Glossary.md +9 -0
  555. package/starter-packs/sales/KnowledgeBase/How we sell.md +9 -0
  556. package/starter-packs/sales/KnowledgeBase/Objections.md +9 -0
  557. package/starter-packs/sales/KnowledgeBase/Pricing.md +9 -0
  558. package/starter-packs/sales/Plugins/sales-starter/LICENSE +202 -0
  559. package/starter-packs/sales/Plugins/sales-starter/SOURCE.md +10 -0
  560. package/starter-packs/sales/Plugins/sales-starter/access.md +19 -0
  561. package/starter-packs/sales/Plugins/sales-starter/plugin.json +10 -0
  562. package/starter-packs/sales/Plugins/sales-starter/skills/account-context/SKILL.md +94 -0
  563. package/starter-packs/sales/Plugins/sales-starter/skills/account-plan/SKILL.md +95 -0
  564. package/starter-packs/sales/Plugins/sales-starter/skills/account-research/SKILL.md +99 -0
  565. package/starter-packs/sales/Plugins/sales-starter/skills/account-tiering/SKILL.md +101 -0
  566. package/starter-packs/sales/Plugins/sales-starter/skills/call-prep/SKILL.md +111 -0
  567. package/starter-packs/sales/Plugins/sales-starter/skills/call-summary/SKILL.md +111 -0
  568. package/starter-packs/sales/Plugins/sales-starter/skills/close-plan/SKILL.md +106 -0
  569. package/starter-packs/sales/Plugins/sales-starter/skills/competitive-intelligence/SKILL.md +102 -0
  570. package/starter-packs/sales/Plugins/sales-starter/skills/create-an-asset/SKILL.md +105 -0
  571. package/starter-packs/sales/Plugins/sales-starter/skills/crm-hygiene-check/SKILL.md +103 -0
  572. package/starter-packs/sales/Plugins/sales-starter/skills/customer-health/SKILL.md +108 -0
  573. package/starter-packs/sales/Plugins/sales-starter/skills/customer-voice/SKILL.md +94 -0
  574. package/starter-packs/sales/Plugins/sales-starter/skills/daily-briefing/SKILL.md +61 -0
  575. package/starter-packs/sales/Plugins/sales-starter/skills/deal-advance-gap/SKILL.md +102 -0
  576. package/starter-packs/sales/Plugins/sales-starter/skills/deal-review/SKILL.md +105 -0
  577. package/starter-packs/sales/Plugins/sales-starter/skills/deal-signals/SKILL.md +96 -0
  578. package/starter-packs/sales/Plugins/sales-starter/skills/deal-slip-scenario/SKILL.md +104 -0
  579. package/starter-packs/sales/Plugins/sales-starter/skills/draft-outreach/SKILL.md +116 -0
  580. package/starter-packs/sales/Plugins/sales-starter/skills/end-of-day/SKILL.md +114 -0
  581. package/starter-packs/sales/Plugins/sales-starter/skills/expansion-whitespace/SKILL.md +97 -0
  582. package/starter-packs/sales/Plugins/sales-starter/skills/forecast/SKILL.md +100 -0
  583. package/starter-packs/sales/Plugins/sales-starter/skills/handle-objection/SKILL.md +93 -0
  584. package/starter-packs/sales/Plugins/sales-starter/skills/inbox-sweep/SKILL.md +114 -0
  585. package/starter-packs/sales/Plugins/sales-starter/skills/lead-triage/SKILL.md +106 -0
  586. package/starter-packs/sales/Plugins/sales-starter/skills/log-activity/SKILL.md +111 -0
  587. package/starter-packs/sales/Plugins/sales-starter/skills/pipeline-review/SKILL.md +97 -0
  588. package/starter-packs/sales/Plugins/sales-starter/skills/renewal-radar/SKILL.md +95 -0
  589. package/starter-packs/sales/Plugins/sales-starter/skills/rep-context/SKILL.md +90 -0
  590. package/starter-packs/sales/Plugins/sales-starter/skills/route-lead/SKILL.md +111 -0
  591. package/starter-packs/sales/Plugins/sales-starter/skills/sales-setup/SKILL.md +200 -0
  592. package/starter-packs/sales/Plugins/sales-starter/skills/schedule-meeting/SKILL.md +99 -0
  593. package/starter-packs/sales/Plugins/sales-starter/skills/stakeholder-map/SKILL.md +107 -0
  594. package/starter-packs/sales/Plugins/sales-starter/skills/team-pipeline/SKILL.md +105 -0
  595. package/starter-packs/sales/Plugins/sales-starter/skills/update-opportunity/SKILL.md +112 -0
  596. package/starter-packs/sales/Plugins/sales-starter/skills/weekly-wrap/SKILL.md +90 -0
  597. package/starter-packs/sales/Plugins/sales-starter/skills/win-loss-review/SKILL.md +88 -0
  598. package/starter-packs/sales/pack.yaml +13 -0
  599. package/starter-packs/support/KnowledgeBase/Escalations.md +9 -0
  600. package/starter-packs/support/KnowledgeBase/Glossary.md +9 -0
  601. package/starter-packs/support/KnowledgeBase/Known issues.md +9 -0
  602. package/starter-packs/support/KnowledgeBase/Products.md +9 -0
  603. package/starter-packs/support/KnowledgeBase/Tone of voice.md +9 -0
  604. package/starter-packs/support/Plugins/support-starter/LICENSE +202 -0
  605. package/starter-packs/support/Plugins/support-starter/SOURCE.md +10 -0
  606. package/starter-packs/support/Plugins/support-starter/access.md +19 -0
  607. package/starter-packs/support/Plugins/support-starter/plugin.json +10 -0
  608. package/starter-packs/support/Plugins/support-starter/skills/customer-escalation/SKILL.md +246 -0
  609. package/starter-packs/support/Plugins/support-starter/skills/customer-research/SKILL.md +250 -0
  610. package/starter-packs/support/Plugins/support-starter/skills/draft-response/SKILL.md +416 -0
  611. package/starter-packs/support/Plugins/support-starter/skills/kb-article/SKILL.md +350 -0
  612. package/starter-packs/support/Plugins/support-starter/skills/ticket-triage/SKILL.md +273 -0
  613. package/starter-packs/support/pack.yaml +12 -0
@@ -1,9 +1,18 @@
1
- import { describe, it, expect, beforeEach, afterEach } from 'vitest';
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
2
  import fs from 'node:fs/promises';
3
+ import http from 'node:http';
3
4
  import os from 'node:os';
4
5
  import path from 'node:path';
6
+ import type { AddressInfo } from 'node:net';
5
7
  import { NodeGitRunner } from '../node-git-runner.js';
6
- import { GitRunError, gitCredentials, isGitTimeout } from '../../../../shared/git.contract.js';
8
+ import {
9
+ GitRunError,
10
+ gitCredentials,
11
+ isGitTimeout,
12
+ type GitCredentials,
13
+ } from '../../../../shared/git.contract.js';
14
+ import { setLogger } from '../../../../shared/logging.js';
15
+ import type { ILogger, LogFields } from '../../../../shared/logger.contract.js';
7
16
 
8
17
  /**
9
18
  * These run REAL git, like the rest of this module's suites: the behaviours
@@ -150,3 +159,223 @@ describe('NodeGitRunner', () => {
150
159
  }
151
160
  });
152
161
  });
162
+
163
+ /**
164
+ * A git host that wants a credential, as a real `ls-remote` over HTTP meets
165
+ * it: a bare repository behind Basic auth, served the dumb way (`info/refs`
166
+ * as a file), which git falls back to when no smart service answers. Every
167
+ * refusal below is git's own — "could not read Username" when it had nothing
168
+ * to offer, "Authentication failed" when what it offered was thrown out.
169
+ */
170
+ describe('NodeGitRunner: a call the host refuses a credential', () => {
171
+ let root: string;
172
+ let clone: string;
173
+ let server: http.Server;
174
+ let remote: string;
175
+ /** The one token the host accepts. */
176
+ let accepted: string;
177
+ /** How many times the host was asked, and refused, in order. */
178
+ let answers: number[];
179
+ /** No helper from the machine this runs on: the clone's config and the call's `-c` are the whole story. */
180
+ let env: NodeJS.ProcessEnv;
181
+
182
+ beforeEach(async () => {
183
+ root = await fs.mkdtemp(path.join(os.tmpdir(), 'git-runner-auth-'));
184
+ const empty = path.join(root, 'empty-gitconfig');
185
+ await fs.writeFile(empty, '');
186
+ env = { GIT_CONFIG_GLOBAL: empty, GIT_CONFIG_NOSYSTEM: '1' };
187
+ const plain = new NodeGitRunner();
188
+ const seed = path.join(root, 'seed');
189
+ await fs.mkdir(seed);
190
+ await plain.run(seed, ['init', '--initial-branch=main'], { env });
191
+ await plain.run(seed, ['-c', 'user.name=t', '-c', 'user.email=t@x', 'commit', '--allow-empty', '-m', 'init'], { env });
192
+ const upstream = path.join(root, 'upstream.git');
193
+ await plain.run(root, ['clone', '--bare', seed, upstream], { env });
194
+ await plain.run(upstream, ['update-server-info'], { env });
195
+ clone = path.join(root, 'clone');
196
+ await fs.mkdir(clone);
197
+ await plain.run(clone, ['init', '--initial-branch=main'], { env });
198
+
199
+ accepted = 'ghp_good';
200
+ answers = [];
201
+ server = http.createServer(async (req, res) => {
202
+ const expected = `Basic ${Buffer.from(`x-access-token:${accepted}`).toString('base64')}`;
203
+ if (req.headers.authorization !== expected) {
204
+ answers.push(401);
205
+ res.writeHead(401, { 'WWW-Authenticate': 'Basic realm="upstream"' });
206
+ res.end();
207
+ return;
208
+ }
209
+ answers.push(200);
210
+ const file = path.join(upstream, new URL(req.url ?? '/', 'http://x').pathname.replace(/^\/upstream\.git\/?/, ''));
211
+ try {
212
+ const bytes = await fs.readFile(file);
213
+ res.writeHead(200, { 'content-type': 'text/plain' });
214
+ res.end(bytes);
215
+ } catch {
216
+ res.writeHead(404);
217
+ res.end();
218
+ }
219
+ });
220
+ await new Promise<void>((r) => server.listen(0, '127.0.0.1', r));
221
+ remote = `http://127.0.0.1:${(server.address() as AddressInfo).port}/upstream.git`;
222
+ await plain.run(clone, ['remote', 'add', 'origin', remote], { env });
223
+ });
224
+
225
+ afterEach(async () => {
226
+ await new Promise<void>((r) => server.close(() => r()));
227
+ await fs.rm(root, { recursive: true, force: true }).catch(() => undefined);
228
+ });
229
+
230
+ /** Credentials whose token a test moves, with a `prepare` that records how it was asked. */
231
+ function moving(initial: string | null, onRefused: (set: (token: string | null) => void) => void = () => undefined) {
232
+ let token = initial;
233
+ const prepare = vi.fn(async (opts?: { refused?: boolean }) => {
234
+ if (opts?.refused) onRefused((next) => { token = next; });
235
+ });
236
+ const credentials: GitCredentials = {
237
+ username: () => 'x-access-token',
238
+ token: () => token,
239
+ prepare,
240
+ };
241
+ return { credentials, prepare };
242
+ }
243
+ const refusals = (prepare: ReturnType<typeof vi.fn>) => prepare.mock.calls.map((c) => (c[0] as { refused?: boolean } | undefined)?.refused ?? false);
244
+
245
+ /** Every line the runner logs during a test. */
246
+ let logged: string[] = [];
247
+ let previousLogger: ILogger | undefined;
248
+ const capturing = (bindings: LogFields = {}): ILogger => {
249
+ const line = (level: string) => (message: string, fields?: LogFields) =>
250
+ void logged.push(`${level} ${message} ${JSON.stringify({ ...bindings, ...fields })}`);
251
+ return { debug: line('debug'), info: line('info'), warn: line('warn'), error: line('error'), child: (m) => capturing({ ...bindings, ...m }) };
252
+ };
253
+ beforeEach(() => {
254
+ logged = [];
255
+ previousLogger = setLogger(capturing());
256
+ });
257
+ afterEach(() => {
258
+ if (previousLogger) setLogger(previousLogger);
259
+ });
260
+
261
+ it('authenticates a clone whose config carries no helper: the helper rides the call', async () => {
262
+ const runner = new NodeGitRunner(undefined, gitCredentials('x-access-token', 'ghp_good'));
263
+ const { stdout } = await runner.run(clone, ['ls-remote', 'origin'], { env });
264
+ expect(stdout).toContain('refs/heads/main');
265
+ // Refused once, with nothing offered; then every request carried the token.
266
+ expect(answers.filter((a) => a === 401)).toHaveLength(1);
267
+ expect(answers).toContain(200);
268
+ });
269
+
270
+ it('with nothing to offer, insists on a token and runs the command once more', async () => {
271
+ const { credentials, prepare } = moving(null, (set) => set('ghp_good'));
272
+ const runner = new NodeGitRunner(undefined, credentials);
273
+ const { stdout } = await runner.run(clone, ['ls-remote', 'origin'], { env });
274
+ expect(stdout).toContain('refs/heads/main');
275
+ // The ordinary renewal before the call, then the one that reports the refusal.
276
+ expect(refusals(prepare)).toEqual([false, true]);
277
+ expect(logged.some((l) => l.includes('the token was renewed and the command runs again'))).toBe(true);
278
+ // The first attempt never got to offer anything (one refusal, and git
279
+ // gave up); the second was refused once more before it offered the token.
280
+ expect(answers.filter((a) => a === 401)).toHaveLength(2);
281
+ expect(answers).toContain(200);
282
+ });
283
+
284
+ it('with a token the host throws out, renews it and runs the command once more', async () => {
285
+ const { credentials, prepare } = moving('ghp_stale', (set) => set('ghp_good'));
286
+ const runner = new NodeGitRunner(undefined, credentials);
287
+ const { stdout } = await runner.run(clone, ['ls-remote', 'origin'], { env });
288
+ expect(stdout).toContain('refs/heads/main');
289
+ expect(refusals(prepare)).toEqual([false, true]);
290
+ });
291
+
292
+ it('runs the command again ONCE: a refusal with a token just issued is the answer', async () => {
293
+ const { credentials, prepare } = moving('ghp_stale', (set) => set('ghp_still_stale'));
294
+ const runner = new NodeGitRunner(undefined, credentials);
295
+ const err = await runner.run(clone, ['ls-remote', 'origin'], { env }).catch((e: unknown) => e);
296
+ expect(err).toBeInstanceOf(GitRunError);
297
+ expect(`${(err as GitRunError).stderr}`).toMatch(/Authentication failed/);
298
+ expect(refusals(prepare).filter(Boolean)).toHaveLength(1);
299
+ // One attempt with a credential offered is two refusals: git asks bare
300
+ // first, then with the token. Two attempts, four — and no third.
301
+ expect(answers).toEqual([401, 401, 401, 401]);
302
+ });
303
+
304
+ it('does not run the command again when no token can be had, and keeps the refusal', async () => {
305
+ const { credentials, prepare } = moving(null, (set) => set(null));
306
+ const runner = new NodeGitRunner(undefined, credentials);
307
+ const err = await runner.run(clone, ['ls-remote', 'origin'], { env }).catch((e: unknown) => e);
308
+ expect(err).toBeInstanceOf(GitRunError);
309
+ expect(`${(err as GitRunError).stderr}`).toMatch(/could not read Username/);
310
+ expect(refusals(prepare).filter(Boolean)).toHaveLength(1);
311
+ // One attempt: with nothing to offer, running it again would only repeat the refusal.
312
+ expect(answers).toEqual([401]);
313
+ expect(logged.some((l) => l.includes('no other token could be had'))).toBe(true);
314
+ });
315
+
316
+ it('does not run the command again with the SAME token the host just refused', async () => {
317
+ // A provider that cannot replace it: the fixed token of a token-mode
318
+ // deployment, whose `prepare` is absent. Running the command again would
319
+ // only be refused again.
320
+ const runner = new NodeGitRunner(undefined, gitCredentials('x-access-token', 'ghp_stale'));
321
+ const err = await runner.run(clone, ['ls-remote', 'origin'], { env }).catch((e: unknown) => e);
322
+ expect(err).toBeInstanceOf(GitRunError);
323
+ expect(`${(err as GitRunError).stderr}`).toMatch(/Authentication failed/);
324
+ // One attempt: the bare ask, then the token, refused. Nothing after.
325
+ expect(answers).toEqual([401, 401]);
326
+ // And one whose renewal hands back what it had.
327
+ answers = [];
328
+ const { credentials, prepare } = moving('ghp_stale', (set) => set('ghp_stale'));
329
+ await expect(new NodeGitRunner(undefined, credentials).run(clone, ['ls-remote', 'origin'], { env })).rejects.toBeInstanceOf(GitRunError);
330
+ expect(refusals(prepare).filter(Boolean)).toHaveLength(1);
331
+ expect(answers).toEqual([401, 401]);
332
+ });
333
+
334
+ it('keeps the refusal, and logs why, when the renewal itself fails', async () => {
335
+ let token: string | null = 'ghp_stale';
336
+ const credentials: GitCredentials = {
337
+ username: () => 'x-access-token',
338
+ token: () => token,
339
+ prepare: async (opts) => {
340
+ if (!opts?.refused) return;
341
+ // What the host refused is dropped, and GitHub will not replace it.
342
+ token = null;
343
+ throw new Error('GitHub answered 401 to the app: bad private key');
344
+ },
345
+ };
346
+ const runner = new NodeGitRunner(undefined, credentials);
347
+ const err = await runner.run(clone, ['ls-remote', 'origin'], { env }).catch((e: unknown) => e);
348
+ expect(err).toBeInstanceOf(GitRunError);
349
+ expect(`${(err as GitRunError).stderr}`).toMatch(/Authentication failed/);
350
+ expect(answers).toEqual([401, 401]);
351
+ const why = logged.find((l) => l.includes('no other token could be had'));
352
+ expect(why).toBeDefined();
353
+ expect(why).toContain('bad private key');
354
+ });
355
+
356
+ it("leaves a credential the caller laid over the call to the caller: the probe's own token is not renewed", async () => {
357
+ const { credentials, prepare } = moving('ghp_good', (set) => set('ghp_good'));
358
+ const runner = new NodeGitRunner(undefined, credentials);
359
+ const err = await runner
360
+ .run(clone, ['ls-remote', 'origin'], { env: { ...env, GITHUB_TOKEN: 'ghp_being_checked' } })
361
+ .catch((e: unknown) => e);
362
+ expect(err).toBeInstanceOf(GitRunError);
363
+ expect(refusals(prepare).filter(Boolean)).toHaveLength(0);
364
+ expect(answers).toEqual([401, 401]);
365
+ });
366
+
367
+ it('names the command the caller asked for in a failure, never its own helper', async () => {
368
+ const runner = new NodeGitRunner(undefined, gitCredentials('x-access-token', 'ghp_stale'));
369
+ const err = await runner.run(clone, ['ls-remote', 'origin'], { env }).catch((e: unknown) => e);
370
+ expect((err as Error).message).toContain('git ls-remote failed');
371
+ expect((err as Error).message).not.toContain('credential.helper');
372
+ expect(`${(err as GitRunError).stderr}`).not.toContain('credential.helper');
373
+ });
374
+
375
+ it('leaves every other failure alone', async () => {
376
+ const { credentials, prepare } = moving('ghp_good');
377
+ const runner = new NodeGitRunner(undefined, credentials);
378
+ await expect(runner.run(clone, ['checkout', 'no-such-branch'], { env })).rejects.toBeInstanceOf(GitRunError);
379
+ expect(refusals(prepare).filter(Boolean)).toHaveLength(0);
380
+ });
381
+ });
@@ -35,6 +35,7 @@ import type { KbContext } from '../../../shared/kb-context.js';
35
35
  import {
36
36
  AppliedChangeMismatchError,
37
37
  BranchAuthorshipError,
38
+ BranchDeleteRefusedError,
38
39
  WorkflowDomainError,
39
40
  WorkflowValidationError,
40
41
  ProtectedBranchError,
@@ -743,12 +744,22 @@ export class GitService implements IGitService {
743
744
  // fresh per-branch workspace gets its backup seeded by the workspace
744
745
  // bootstrap path, not by a branch switch on an existing one.
745
746
 
747
+ /**
748
+ * Delete a branch, here and on origin. Answers the tip it had — origin's
749
+ * when origin had it, else the local head's; null when neither existed —
750
+ * so the caller can say which commit a restore would start from.
751
+ *
752
+ * `expectTip`, when given, is the tip the caller's checks were made
753
+ * against: the remote delete is then leased on it, and a branch that moved
754
+ * since (a colleague pushed) is refused by the host rather than deleted
755
+ * with work nobody looked at.
756
+ */
746
757
  async deleteBranch(
747
758
  workspaceId: string,
748
759
  name: string,
749
760
  user: AuthUser,
750
- opts: { onlyIfNoRemote?: boolean; systemCleanup?: boolean } = {},
751
- ): Promise<void> {
761
+ opts: { onlyIfNoRemote?: boolean; systemCleanup?: boolean; expectTip?: string } = {},
762
+ ): Promise<{ lastCommit: string | null }> {
752
763
  assertValidBranchName(name);
753
764
  if (this.kb.isProtectedBranch(name)) {
754
765
  throw new ProtectedBranchError(name, 'deleting a protected branch');
@@ -777,28 +788,33 @@ export class GitService implements IGitService {
777
788
  `Branch "${name}" still exists on origin — refusing to auto-prune.`,
778
789
  );
779
790
  }
780
- } else {
781
- // Authored-or-admin delete path. The branch's author (per the
782
- // `<email-localpart>/...` naming convention — see `isBranchAuthoredBy`)
783
- // can always delete their own draft. Admins (per the workspace's
784
- // `roles.yaml`) can additionally clean up anyone's branch, including
785
- // unprefixed CLI branches like `fix/...` that have no recognisable
786
- // author. We detect admin via `canWrite(_, _, 'roles.yaml')` because
787
- // that predicate is the existing source of truth for `Admin`-role
788
- // membership inside the access model — no separate `isAdmin()` plumbing
789
- // needed.
790
- // Two authorship conventions: `<localpart>/…` UI drafts, and the
791
- // `suggestions/<who>-<id8>/…` namespace the propose flow files its
792
- // branches under — a user resetting THEIR OWN suggestion bundle is
793
- // deleting their own branch.
794
- const isAuthor =
795
- isBranchAuthoredBy(name, user.email) ||
796
- isOwnSuggestionsBranch(name, { email: user.email, id: user.id });
797
- const isAdmin = this.accessControl
798
- ? await this.accessControl.canWrite(workspaceId, user.email, 'roles.yaml')
799
- : false;
800
- if (!isAuthor && !isAdmin) {
801
- throw new BranchAuthorshipError(name);
791
+ } else if (!(await this.mayDeleteBranch(workspaceId, name, user))) {
792
+ throw new BranchAuthorshipError(name);
793
+ }
794
+
795
+ // Remote delete FIRST. Skip in `onlyIfNoRemote` mode (the contract is
796
+ // local-only cleanup) and skip when origin has no such branch. Origin
797
+ // is asked, not this clone's remote-tracking ref: a branch pushed from
798
+ // another clone since the last fetch has no tracking ref here, and
799
+ // trusting the cache would delete it locally while it lives on.
800
+ // A refusal by the host — or a host that cannot be asked — fails the
801
+ // whole delete: the local branch is untouched, so nothing is
802
+ // half-deleted and the person can try again. Deleting locally first
803
+ // would leave a branch that vanished here but lives on the remote —
804
+ // and, unlike a refused write, a deleted branch has no later push to
805
+ // reconcile it.
806
+ let remoteTip: string | null = null;
807
+ if (!opts.onlyIfNoRemote) {
808
+ try {
809
+ remoteTip = await this.originBranchTip(cwd, name);
810
+ if (remoteTip !== null) {
811
+ const lease = opts.expectTip ? [`--force-with-lease=refs/heads/${name}:${opts.expectTip}`] : [];
812
+ await this.git(cwd, ['push', ...lease, 'origin', '--delete', name]);
813
+ }
814
+ } catch (err) {
815
+ const detail = sanitizeError(err);
816
+ log.warn(`the repository host refused to delete branch "${name}"; leaving it in place:`, { detail });
817
+ throw new BranchDeleteRefusedError(name, detail);
802
818
  }
803
819
  }
804
820
 
@@ -810,19 +826,91 @@ export class GitService implements IGitService {
810
826
  // Skip when no local ref exists — the picker can surface branches that
811
827
  // exist only as `refs/remotes/origin/<name>` (the user hasn't worked on
812
828
  // them locally), and "discard from origin" must still work for those.
829
+ let localTip: string | null = null;
813
830
  if (await this.refExists(cwd, `refs/heads/${name}`)) {
831
+ localTip = await this.revParseOrNull(cwd, `refs/heads/${name}`);
814
832
  await this.git(cwd, ['branch', '-D', name]);
815
833
  }
834
+ return { lastCommit: remoteTip ?? localTip };
835
+ });
836
+ }
816
837
 
817
- // Remote delete. Skip in `onlyIfNoRemote` mode (the contract is
818
- // local-only cleanup) and skip when there's no remote ref to delete.
819
- // A push failure here surfaces to the caller — the local ref is
820
- // already gone, and we don't try to resurrect it because rolling back
821
- // a "merged-into-the-deleted-branch" local commit would be even more
822
- // confusing than the half-finished state.
823
- if (!opts.onlyIfNoRemote
824
- && await this.refExists(cwd, `refs/remotes/origin/${name}`)) {
825
- await this.git(cwd, ['push', 'origin', '--delete', name]);
838
+ /**
839
+ * The author-or-Admin rule for deleting `name`. The branch's author (per
840
+ * the `<email-localpart>/...` naming convention — see `isBranchAuthoredBy`)
841
+ * can always delete their own draft. Admins (per the workspace's
842
+ * `roles.yaml`) can additionally clean up anyone's branch, including
843
+ * unprefixed CLI branches like `fix/...` that have no recognisable author.
844
+ * Admin is detected via `canWrite(_, _, 'roles.yaml')` because that
845
+ * predicate is the existing source of truth for `Admin`-role membership
846
+ * inside the access model — no separate `isAdmin()` plumbing needed.
847
+ *
848
+ * Two authorship conventions: `<localpart>/…` UI drafts, and the
849
+ * `suggestions/<who>-<id8>/…` namespace the propose flow files its branches
850
+ * under — a user resetting THEIR OWN suggestion bundle is deleting their
851
+ * own branch.
852
+ */
853
+ async mayDeleteBranch(workspaceId: string, name: string, user: AuthUser): Promise<boolean> {
854
+ const isAuthor =
855
+ isBranchAuthoredBy(name, user.email) ||
856
+ isOwnSuggestionsBranch(name, { email: user.email, id: user.id });
857
+ if (isAuthor) return true;
858
+ return this.accessControl
859
+ ? await this.accessControl.canWrite(workspaceId, user.email, 'roles.yaml')
860
+ : false;
861
+ }
862
+
863
+ /**
864
+ * Where `name` stands in this clone, read from its refs as the last fetch
865
+ * left them (the caller fetches first): whether it exists on origin or as a
866
+ * local head, its tip (origin's when it has one), and how many commits on
867
+ * it — on either ref — are not on `defaultBranch`.
868
+ *
869
+ * The default branch is read from origin's ref, falling back to the local
870
+ * head on a clone whose default was never pushed. A default that resolves
871
+ * on neither throws: a count against nothing would call every commit
872
+ * unmerged, or none.
873
+ */
874
+ async branchState(
875
+ workspaceId: string,
876
+ name: string,
877
+ defaultBranch: string,
878
+ ): Promise<{ exists: boolean; lastCommit: string | null; unmergedCommits: number }> {
879
+ assertValidBranchName(name);
880
+ assertValidBranchName(defaultBranch);
881
+ return this.mutex.run(workspaceId, async () => {
882
+ const cwd = await this.repoDir(workspaceId);
883
+ const remote = await this.revParseOrNull(cwd, `refs/remotes/origin/${name}`);
884
+ const local = await this.revParseOrNull(cwd, `refs/heads/${name}`);
885
+ if (remote === null && local === null) return { exists: false, lastCommit: null, unmergedCommits: 0 };
886
+ const base =
887
+ (await this.revParseOrNull(cwd, `refs/remotes/origin/${defaultBranch}`)) ??
888
+ (await this.revParseOrNull(cwd, `refs/heads/${defaultBranch}`));
889
+ if (base === null) {
890
+ throw new Error(`the default branch "${defaultBranch}" does not resolve in this clone`);
891
+ }
892
+ const tips = [remote, local].filter((sha): sha is string => sha !== null);
893
+ const { stdout } = await this.git(cwd, ['rev-list', '--count', ...tips, `^${base}`]);
894
+ return { exists: true, lastCommit: remote ?? local, unmergedCommits: Number(stdout.trim()) || 0 };
895
+ });
896
+ }
897
+
898
+ /**
899
+ * Whether commit `ancestor` is contained in commit `descendant`. Exit 1 is
900
+ * git's "no"; anything else — a commit this clone does not have — throws,
901
+ * so a caller deciding to delete on a yes never gets one by accident.
902
+ */
903
+ async isAncestor(workspaceId: string, ancestor: string, descendant: string): Promise<boolean> {
904
+ const sha = /^[0-9a-f]{4,64}$/i;
905
+ if (!sha.test(ancestor) || !sha.test(descendant)) throw new Error('isAncestor takes commit ids');
906
+ return this.mutex.run(workspaceId, async () => {
907
+ const cwd = await this.repoDir(workspaceId);
908
+ try {
909
+ await this.git(cwd, ['merge-base', '--is-ancestor', ancestor, descendant]);
910
+ return true;
911
+ } catch (err) {
912
+ if (err instanceof GitRunError && !err.timedOut && err.exitCode === 1) return false;
913
+ throw err;
826
914
  }
827
915
  });
828
916
  }
@@ -1867,7 +1955,21 @@ export class GitService implements IGitService {
1867
1955
  */
1868
1956
  async remoteBranchExists(workspaceId: string, branch: string): Promise<boolean> {
1869
1957
  assertValidBranchName(branch);
1870
- const cwd = await this.repoDir(workspaceId);
1958
+ return this.originHasBranch(await this.repoDir(workspaceId), branch);
1959
+ }
1960
+
1961
+ /** Origin's tip of `branch`, asked of origin itself; null when it has no such branch. */
1962
+ private async originBranchTip(cwd: string, branch: string): Promise<string | null> {
1963
+ try {
1964
+ const { stdout } = await this.git(cwd, ['ls-remote', '--exit-code', '--heads', 'origin', `refs/heads/${branch}`]);
1965
+ return /^([0-9a-f]+)\s/.exec(stdout)?.[1] ?? null;
1966
+ } catch (err) {
1967
+ if ((err as { exitCode?: number }).exitCode === 2) return null;
1968
+ throw err;
1969
+ }
1970
+ }
1971
+
1972
+ private async originHasBranch(cwd: string, branch: string): Promise<boolean> {
1871
1973
  try {
1872
1974
  await this.git(cwd, ['ls-remote', '--exit-code', '--heads', 'origin', `refs/heads/${branch}`]);
1873
1975
  return true;
@@ -1962,9 +2064,10 @@ export class GitService implements IGitService {
1962
2064
  // commit the clone holds but origin has not seen into cherry-picks of
1963
2065
  // the commits it merged — the merged branch stops being a parent, so
1964
2066
  // "does the published head contain the target's head" answers no
1965
- // however many times the update runs. Only the change-request update
1966
- // asks for it; every other pull replays plain saves, where the two
1967
- // spellings produce the same history.
2067
+ // however many times the update runs. The change-request update and
2068
+ // every push's cooperative recovery ask for it (a merge a refused push
2069
+ // stranded rides along with the branch's next push); with no merge
2070
+ // among the local commits the two spellings produce the same history.
1968
2071
  await this.git(cwd, [
1969
2072
  'rebase',
1970
2073
  '--autostash',
@@ -2081,7 +2184,12 @@ export class GitService implements IGitService {
2081
2184
  }
2082
2185
  throw err;
2083
2186
  }
2084
- await this.rebaseOntoRemote(cwd, branch, remoteRef);
2187
+ // `preserveMerges` here too: this sync runs on a timer, so it is the
2188
+ // pull most likely to reach a branch while a merge a refused open or
2189
+ // update left local is still waiting for its push. A plain rebase would
2190
+ // flatten that merge before the push ever ran, and the branch would be
2191
+ // "behind" its target again however many times it was updated.
2192
+ await this.rebaseOntoRemote(cwd, branch, remoteRef, { preserveMerges: true });
2085
2193
  this.accessControl?.invalidate(workspaceId);
2086
2194
  const after = await this.revParseOrNull(cwd, 'HEAD');
2087
2195
  const treeAfter = (await this.revParseOrNull(cwd, 'HEAD^{tree}')) ?? '';
@@ -2140,6 +2248,36 @@ export class GitService implements IGitService {
2140
2248
  return this.mutex.run(workspaceId, async () => this.hasUnpushedCommitsAt(await this.repoDir(workspaceId)));
2141
2249
  }
2142
2250
 
2251
+ /**
2252
+ * Whether this clone's HEAD already holds everything origin has on its
2253
+ * branch: a push would then fast-forward origin to HEAD, and no cooperative
2254
+ * pull-rebase would replay origin's commits INTO HEAD first. Asked after a
2255
+ * fetch, about the fetched ref.
2256
+ *
2257
+ * The roles.yaml preservation asks this before pushing a restore it finds
2258
+ * already committed. "HEAD's roles.yaml is the base version" is only worth
2259
+ * publishing when origin ends up AT HEAD — a clone behind origin would have
2260
+ * origin's divergent copy rebased into it on the way, and the push would
2261
+ * land exactly what the restore exists to keep off the base.
2262
+ *
2263
+ * Exit 1 is git's "not an ancestor". Every other failure propagates — a
2264
+ * tracking ref that does not exist, a broken repository — so a probe that
2265
+ * cannot answer fails the guard closed, never open.
2266
+ */
2267
+ async headContainsOrigin(workspaceId: string): Promise<boolean> {
2268
+ return this.mutex.run(workspaceId, async () => {
2269
+ const cwd = await this.repoDir(workspaceId);
2270
+ const branch = await this.currentBranch(cwd);
2271
+ try {
2272
+ await this.git(cwd, ['merge-base', '--is-ancestor', `refs/remotes/origin/${branch}`, 'HEAD']);
2273
+ return true;
2274
+ } catch (err) {
2275
+ if (err instanceof GitRunError && !err.timedOut && err.exitCode === 1) return false;
2276
+ throw err;
2277
+ }
2278
+ });
2279
+ }
2280
+
2143
2281
  /**
2144
2282
  * `hasUnpushedCommits` without the workspace reservation — for a caller that
2145
2283
  * already holds one (`mergeChangeRequest`, which has to ask inside the same
@@ -9,6 +9,24 @@ import {
9
9
  type GitRunResult,
10
10
  type IGitRunner,
11
11
  } from '../../../shared/git.contract.js';
12
+ import { logger } from '../../../shared/logging.js';
13
+ import { credentialHelperValue } from '../../kb-fs/clone-config.js';
14
+
15
+ const log = logger('git-runner');
16
+
17
+ /**
18
+ * What git says when it had no credential to offer, or offered one the host
19
+ * threw out. Both happen before a byte of the transfer moves, so a command
20
+ * that failed this way did nothing, and running it again is safe for every
21
+ * subcommand — fetch, push, ls-remote and clone alike.
22
+ */
23
+ const REFUSED_CREDENTIAL_RE = /could not read (?:Username|Password) for|terminal prompts disabled|Authentication failed for/i;
24
+
25
+ /** Whether this failure is git being refused a credential (see {@link REFUSED_CREDENTIAL_RE}). */
26
+ export function isCredentialRefusal(err: unknown): boolean {
27
+ if (!(err instanceof GitRunError) || err.timedOut) return false;
28
+ return REFUSED_CREDENTIAL_RE.test(`${err.stderr ?? ''}\n${err.message}`);
29
+ }
12
30
 
13
31
  /**
14
32
  * The deadline every git call carries unless its caller says otherwise.
@@ -147,7 +165,8 @@ interface CommandFailure extends Error {
147
165
  * missing git — a string `code`, which is how callers tell "git never ran"
148
166
  * from "git said no"), and cuts off a child whose output passes `maxBuffer`.
149
167
  */
150
- function spawnGit(args: string[], options: SpawnOptions & { maxBuffer: number }): {
168
+ /** `shown` is what a failure message names — the caller's arguments, without the runner's own `-c` pairs. */
169
+ function spawnGit(args: string[], shown: string[], options: SpawnOptions & { maxBuffer: number }): {
151
170
  promise: Promise<Captured>;
152
171
  child: ChildProcess;
153
172
  } {
@@ -195,7 +214,7 @@ function spawnGit(args: string[], options: SpawnOptions & { maxBuffer: number })
195
214
  resolve({ stdout, stderr });
196
215
  return;
197
216
  }
198
- const error: CommandFailure = new Error(`Command failed: git ${args.join(' ')}\n${stderr.toString()}`);
217
+ const error: CommandFailure = new Error(`Command failed: git ${shown.join(' ')}\n${stderr.toString()}`);
199
218
  error.code = code ?? undefined;
200
219
  error.signal = signal;
201
220
  error.stdout = stdout;
@@ -243,21 +262,77 @@ export class NodeGitRunner implements IGitRunner {
243
262
  return { ...env, ...overrides, ...FIXED_ENV };
244
263
  }
245
264
 
265
+ /**
266
+ * The credential helper, for THIS invocation, when a token is in hand: the
267
+ * same inline snippet the clones carry in their config, which reads the
268
+ * token from the child's environment. Given to every call so that a clone
269
+ * whose config lost its helper — unstamped while the deployment briefly
270
+ * had no token, created by a build that authenticated only its own clone,
271
+ * or touched by hand — still authenticates: the server's calls never
272
+ * depend on what a `.git/config` on disk happens to say. Nothing for a
273
+ * runner without a token, and nothing when the username cannot be made
274
+ * into a helper (the stamp refuses the same one, and says so).
275
+ */
276
+ private helperArgs(): string[] {
277
+ if (!this.credentials.token()) return [];
278
+ try {
279
+ const helper = credentialHelperValue(this.credentials);
280
+ return helper ? ['-c', `credential.helper=${helper}`] : [];
281
+ } catch {
282
+ return [];
283
+ }
284
+ }
285
+
246
286
  run(cwd: string, args: string[], opts: GitRunOptions & { encoding: 'buffer' }): Promise<GitRunResult<Buffer>>;
247
287
  run(cwd: string, args: string[], opts?: GitRunOptions & { encoding?: 'utf8' }): Promise<GitRunResult>;
248
288
  async run(cwd: string, args: string[], opts: GitRunOptions = {}): Promise<GitRunResult<string | Buffer>> {
249
- const timeoutMs = opts.timeoutMs ?? this.defaultTimeoutMs;
250
- const timers: NodeJS.Timeout[] = [];
251
- let timedOut = false;
252
-
253
289
  // A credential that expires is renewed before it is read. Renewing is
254
290
  // the credential's own business and its failure is not this call's:
255
291
  // plenty of git never leaves the disk, and a call that does is told by
256
292
  // the host what the token in hand is worth.
257
293
  await this.credentials.prepare?.().catch(() => undefined);
294
+ try {
295
+ return await this.attempt(cwd, args, opts);
296
+ } catch (err) {
297
+ if (!isCredentialRefusal(err)) throw err;
298
+ // A credential the CALLER laid over the provider's (the setup probe
299
+ // checking a token it was given) is the caller's to judge: nothing
300
+ // here can replace it, so the refusal is the answer.
301
+ if (opts.env?.[GIT_TOKEN_ENV] !== undefined) throw err;
302
+ // Refused a credential: the token in hand was dead, or there was none
303
+ // to hand over. Tell the provider so — it drops what the host refused
304
+ // and asks for another, past whatever it remembers about a recent
305
+ // failure — and run the command once more ONLY with a credential that
306
+ // is not the one just refused. The same token again would only be
307
+ // refused again, and a renewal that failed leaves none.
308
+ const subcommand = subcommandOf(args);
309
+ const refused = this.credentials.token();
310
+ const renewal = await this.credentials.prepare?.({ refused: true }).then(
311
+ () => null,
312
+ (e: unknown) => (e instanceof Error ? e.message : String(e)),
313
+ );
314
+ const renewed = this.credentials.token();
315
+ if (!renewed || renewed === refused) {
316
+ log.warn(
317
+ `git ${subcommand} was refused a credential, and no other token could be had to run it again` +
318
+ (renewal ? `: ${redactGitToken(renewal, refused)}` : ''),
319
+ );
320
+ throw err;
321
+ }
322
+ log.warn(`git ${subcommand} was refused a credential; the token was renewed and the command runs again`);
323
+ return await this.attempt(cwd, args, opts);
324
+ }
325
+ }
326
+
327
+ private async attempt(cwd: string, args: string[], opts: GitRunOptions): Promise<GitRunResult<string | Buffer>> {
328
+ const timeoutMs = opts.timeoutMs ?? this.defaultTimeoutMs;
329
+ const timers: NodeJS.Timeout[] = [];
330
+ let timedOut = false;
258
331
 
259
332
  try {
260
- const { promise: pending, child } = spawnGit(args, {
333
+ // The helper rides the invocation, not the error text: a failure
334
+ // message names the command the caller asked for.
335
+ const { promise: pending, child } = spawnGit([...this.helperArgs(), ...args], args, {
261
336
  cwd,
262
337
  env: this.childEnv(opts.env),
263
338
  maxBuffer: MAX_OUTPUT_BYTES,