@bevel-software/platform-core-backend 0.25.2 → 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 (733) hide show
  1. package/agent-guide/access-control.md +251 -0
  2. package/agent-guide/conventions.md +35 -0
  3. package/agent-guide/directory-structure.md +145 -0
  4. package/agent-guide/finding-things.md +7 -0
  5. package/agent-guide/html-views.md +83 -0
  6. package/agent-guide/introduction.md +27 -0
  7. package/agent-guide/new-knowledge-base.md +24 -0
  8. package/agent-guide/skills.md +47 -0
  9. package/agent-guide/tool-manuals.md +227 -0
  10. package/agent-guide/where-a-new-file-goes.md +36 -0
  11. package/dist/assets.d.ts +26 -0
  12. package/dist/assets.d.ts.map +1 -1
  13. package/dist/assets.js +35 -2
  14. package/dist/assets.js.map +1 -1
  15. package/dist/core/catalog-revision.d.ts +19 -1
  16. package/dist/core/catalog-revision.d.ts.map +1 -1
  17. package/dist/core/catalog-revision.js +14 -3
  18. package/dist/core/catalog-revision.js.map +1 -1
  19. package/dist/core/core-ports.d.ts +11 -0
  20. package/dist/core/core-ports.d.ts.map +1 -1
  21. package/dist/core/core-ports.js.map +1 -1
  22. package/dist/core/create-core-server.d.ts +5 -0
  23. package/dist/core/create-core-server.d.ts.map +1 -1
  24. package/dist/core/create-core-server.js +65 -3
  25. package/dist/core/create-core-server.js.map +1 -1
  26. package/dist/core/create-core-services.d.ts +35 -0
  27. package/dist/core/create-core-services.d.ts.map +1 -1
  28. package/dist/core/create-core-services.js +114 -12
  29. package/dist/core/create-core-services.js.map +1 -1
  30. package/dist/core/lifecycle.d.ts +16 -1
  31. package/dist/core/lifecycle.d.ts.map +1 -1
  32. package/dist/core/lifecycle.js +18 -1
  33. package/dist/core/lifecycle.js.map +1 -1
  34. package/dist/core-config.d.ts +21 -0
  35. package/dist/core-config.d.ts.map +1 -1
  36. package/dist/core-config.js +22 -1
  37. package/dist/core-config.js.map +1 -1
  38. package/dist/index.d.ts +1 -1
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/index.js +2 -2
  41. package/dist/index.js.map +1 -1
  42. package/dist/modules/access/access-control.interface.d.ts +17 -0
  43. package/dist/modules/access/access-control.interface.d.ts.map +1 -1
  44. package/dist/modules/access/access-control.service.d.ts +2 -0
  45. package/dist/modules/access/access-control.service.d.ts.map +1 -1
  46. package/dist/modules/access/access-control.service.js +27 -0
  47. package/dist/modules/access/access-control.service.js.map +1 -1
  48. package/dist/modules/access/directory-sync-bot.js +1 -1
  49. package/dist/modules/access/directory-sync-bot.js.map +1 -1
  50. package/dist/modules/agent-guide/agent-guide.d.ts +145 -0
  51. package/dist/modules/agent-guide/agent-guide.d.ts.map +1 -0
  52. package/dist/modules/agent-guide/agent-guide.js +199 -0
  53. package/dist/modules/agent-guide/agent-guide.js.map +1 -0
  54. package/dist/modules/agent-guide/agent-guide.tools.d.ts +24 -0
  55. package/dist/modules/agent-guide/agent-guide.tools.d.ts.map +1 -0
  56. package/dist/modules/agent-guide/agent-guide.tools.js +100 -0
  57. package/dist/modules/agent-guide/agent-guide.tools.js.map +1 -0
  58. package/dist/modules/agent-guide/index.d.ts +4 -0
  59. package/dist/modules/agent-guide/index.d.ts.map +1 -0
  60. package/dist/modules/agent-guide/index.js +4 -0
  61. package/dist/modules/agent-guide/index.js.map +1 -0
  62. package/dist/modules/agent-instructions/agent-instructions.routes.d.ts +3 -2
  63. package/dist/modules/agent-instructions/agent-instructions.routes.d.ts.map +1 -1
  64. package/dist/modules/agent-instructions/agent-instructions.routes.js +3 -2
  65. package/dist/modules/agent-instructions/agent-instructions.routes.js.map +1 -1
  66. package/dist/modules/agent-instructions/compose.d.ts +14 -6
  67. package/dist/modules/agent-instructions/compose.d.ts.map +1 -1
  68. package/dist/modules/agent-instructions/compose.js +28 -8
  69. package/dist/modules/agent-instructions/compose.js.map +1 -1
  70. package/dist/modules/agent-instructions/index.d.ts +1 -1
  71. package/dist/modules/agent-instructions/index.d.ts.map +1 -1
  72. package/dist/modules/agent-instructions/index.js +1 -1
  73. package/dist/modules/agent-instructions/index.js.map +1 -1
  74. package/dist/modules/agent-instructions/shared-file-rules.d.ts +10 -50
  75. package/dist/modules/agent-instructions/shared-file-rules.d.ts.map +1 -1
  76. package/dist/modules/agent-instructions/shared-file-rules.js +51 -104
  77. package/dist/modules/agent-instructions/shared-file-rules.js.map +1 -1
  78. package/dist/modules/audit/agent-audit.service.d.ts +21 -2
  79. package/dist/modules/audit/agent-audit.service.d.ts.map +1 -1
  80. package/dist/modules/audit/agent-audit.service.js +43 -2
  81. package/dist/modules/audit/agent-audit.service.js.map +1 -1
  82. package/dist/modules/audit/agent-connection.routes.d.ts +34 -0
  83. package/dist/modules/audit/agent-connection.routes.d.ts.map +1 -0
  84. package/dist/modules/audit/agent-connection.routes.js +38 -0
  85. package/dist/modules/audit/agent-connection.routes.js.map +1 -0
  86. package/dist/modules/audit/audit.contract.d.ts +24 -0
  87. package/dist/modules/audit/audit.contract.d.ts.map +1 -1
  88. package/dist/modules/audit/audit.contract.js.map +1 -1
  89. package/dist/modules/audit/index.d.ts +1 -0
  90. package/dist/modules/audit/index.d.ts.map +1 -1
  91. package/dist/modules/audit/index.js +1 -0
  92. package/dist/modules/audit/index.js.map +1 -1
  93. package/dist/modules/auth/account.routes.d.ts.map +1 -1
  94. package/dist/modules/auth/account.routes.js +10 -3
  95. package/dist/modules/auth/account.routes.js.map +1 -1
  96. package/dist/modules/auth/auth.middleware.d.ts +7 -0
  97. package/dist/modules/auth/auth.middleware.d.ts.map +1 -1
  98. package/dist/modules/auth/auth.middleware.js +1 -1
  99. package/dist/modules/auth/auth.middleware.js.map +1 -1
  100. package/dist/modules/code-mode/code-mode.tool.d.ts.map +1 -1
  101. package/dist/modules/code-mode/code-mode.tool.js +11 -3
  102. package/dist/modules/code-mode/code-mode.tool.js.map +1 -1
  103. package/dist/modules/database/core-schema.d.ts +96 -0
  104. package/dist/modules/database/core-schema.d.ts.map +1 -1
  105. package/dist/modules/database/core-schema.js +32 -3
  106. package/dist/modules/database/core-schema.js.map +1 -1
  107. package/dist/modules/embed/account-link.service.d.ts +42 -0
  108. package/dist/modules/embed/account-link.service.d.ts.map +1 -0
  109. package/dist/modules/embed/account-link.service.js +50 -0
  110. package/dist/modules/embed/account-link.service.js.map +1 -0
  111. package/dist/modules/embed/embed-link.d.ts +58 -0
  112. package/dist/modules/embed/embed-link.d.ts.map +1 -0
  113. package/dist/modules/embed/embed-link.js +160 -0
  114. package/dist/modules/embed/embed-link.js.map +1 -0
  115. package/dist/modules/embed/embed.errors.d.ts +30 -0
  116. package/dist/modules/embed/embed.errors.d.ts.map +1 -0
  117. package/dist/modules/embed/embed.errors.js +42 -0
  118. package/dist/modules/embed/embed.errors.js.map +1 -0
  119. package/dist/modules/embed/embed.interface.d.ts +204 -0
  120. package/dist/modules/embed/embed.interface.d.ts.map +1 -0
  121. package/dist/modules/embed/embed.interface.js +2 -0
  122. package/dist/modules/embed/embed.interface.js.map +1 -0
  123. package/dist/modules/embed/embed.routes.d.ts +59 -0
  124. package/dist/modules/embed/embed.routes.d.ts.map +1 -0
  125. package/dist/modules/embed/embed.routes.js +343 -0
  126. package/dist/modules/embed/embed.routes.js.map +1 -0
  127. package/dist/modules/embed/embed.service.d.ts +169 -0
  128. package/dist/modules/embed/embed.service.d.ts.map +1 -0
  129. package/dist/modules/embed/embed.service.js +627 -0
  130. package/dist/modules/embed/embed.service.js.map +1 -0
  131. package/dist/modules/embed/embed.tools.d.ts +51 -0
  132. package/dist/modules/embed/embed.tools.d.ts.map +1 -0
  133. package/dist/modules/embed/embed.tools.js +170 -0
  134. package/dist/modules/embed/embed.tools.js.map +1 -0
  135. package/dist/modules/embed/mcp-app.d.ts +64 -0
  136. package/dist/modules/embed/mcp-app.d.ts.map +1 -0
  137. package/dist/modules/embed/mcp-app.js +147 -0
  138. package/dist/modules/embed/mcp-app.js.map +1 -0
  139. package/dist/modules/embed/mcp-app.routes.d.ts +21 -0
  140. package/dist/modules/embed/mcp-app.routes.d.ts.map +1 -0
  141. package/dist/modules/embed/mcp-app.routes.js +36 -0
  142. package/dist/modules/embed/mcp-app.routes.js.map +1 -0
  143. package/dist/modules/github-app/github-app.connection.d.ts +15 -0
  144. package/dist/modules/github-app/github-app.connection.d.ts.map +1 -1
  145. package/dist/modules/github-app/github-app.connection.js +34 -3
  146. package/dist/modules/github-app/github-app.connection.js.map +1 -1
  147. package/dist/modules/kb-fs/locking-filesystem.d.ts +65 -0
  148. package/dist/modules/kb-fs/locking-filesystem.d.ts.map +1 -1
  149. package/dist/modules/kb-fs/locking-filesystem.js +300 -0
  150. package/dist/modules/kb-fs/locking-filesystem.js.map +1 -1
  151. package/dist/modules/kb-sync/kb-sync.interface.d.ts +5 -0
  152. package/dist/modules/kb-sync/kb-sync.interface.d.ts.map +1 -1
  153. package/dist/modules/kb-sync/kb-sync.service.d.ts.map +1 -1
  154. package/dist/modules/kb-sync/kb-sync.service.js +6 -0
  155. package/dist/modules/kb-sync/kb-sync.service.js.map +1 -1
  156. package/dist/modules/mcp/mcp-auth.middleware.d.ts.map +1 -1
  157. package/dist/modules/mcp/mcp-auth.middleware.js +2 -1
  158. package/dist/modules/mcp/mcp-auth.middleware.js.map +1 -1
  159. package/dist/modules/mcp/mcp.service.d.ts +57 -3
  160. package/dist/modules/mcp/mcp.service.d.ts.map +1 -1
  161. package/dist/modules/mcp/mcp.service.js +177 -24
  162. package/dist/modules/mcp/mcp.service.js.map +1 -1
  163. package/dist/modules/mcp/oauth/bevel-oauth-provider.d.ts +24 -0
  164. package/dist/modules/mcp/oauth/bevel-oauth-provider.d.ts.map +1 -1
  165. package/dist/modules/mcp/oauth/bevel-oauth-provider.js +32 -4
  166. package/dist/modules/mcp/oauth/bevel-oauth-provider.js.map +1 -1
  167. package/dist/modules/mcp/tool-schema-guard.d.ts +105 -0
  168. package/dist/modules/mcp/tool-schema-guard.d.ts.map +1 -0
  169. package/dist/modules/mcp/tool-schema-guard.js +171 -0
  170. package/dist/modules/mcp/tool-schema-guard.js.map +1 -0
  171. package/dist/modules/onboarding/onboarding.contract.d.ts +80 -0
  172. package/dist/modules/onboarding/onboarding.contract.d.ts.map +1 -0
  173. package/dist/modules/onboarding/onboarding.contract.js +7 -0
  174. package/dist/modules/onboarding/onboarding.contract.js.map +1 -0
  175. package/dist/modules/onboarding/onboarding.routes.d.ts +18 -0
  176. package/dist/modules/onboarding/onboarding.routes.d.ts.map +1 -0
  177. package/dist/modules/onboarding/onboarding.routes.js +81 -0
  178. package/dist/modules/onboarding/onboarding.routes.js.map +1 -0
  179. package/dist/modules/onboarding/starter-pack.service.d.ts +213 -0
  180. package/dist/modules/onboarding/starter-pack.service.d.ts.map +1 -0
  181. package/dist/modules/onboarding/starter-pack.service.js +492 -0
  182. package/dist/modules/onboarding/starter-pack.service.js.map +1 -0
  183. package/dist/modules/onboarding/starter-packs.d.ts +78 -0
  184. package/dist/modules/onboarding/starter-packs.d.ts.map +1 -0
  185. package/dist/modules/onboarding/starter-packs.js +156 -0
  186. package/dist/modules/onboarding/starter-packs.js.map +1 -0
  187. package/dist/modules/plugins/index.d.ts +1 -1
  188. package/dist/modules/plugins/index.d.ts.map +1 -1
  189. package/dist/modules/plugins/index.js +1 -1
  190. package/dist/modules/plugins/index.js.map +1 -1
  191. package/dist/modules/plugins/plugin-provision.service.d.ts +19 -0
  192. package/dist/modules/plugins/plugin-provision.service.d.ts.map +1 -1
  193. package/dist/modules/plugins/plugin-provision.service.js +18 -1
  194. package/dist/modules/plugins/plugin-provision.service.js.map +1 -1
  195. package/dist/modules/plugins/plugins.routes.d.ts +0 -36
  196. package/dist/modules/plugins/plugins.routes.d.ts.map +1 -1
  197. package/dist/modules/plugins/plugins.routes.js +31 -0
  198. package/dist/modules/plugins/plugins.routes.js.map +1 -1
  199. package/dist/modules/plugins/plugins.tools.d.ts +36 -2
  200. package/dist/modules/plugins/plugins.tools.d.ts.map +1 -1
  201. package/dist/modules/plugins/plugins.tools.js +71 -14
  202. package/dist/modules/plugins/plugins.tools.js.map +1 -1
  203. package/dist/modules/settings/deployment-settings.service.d.ts +34 -7
  204. package/dist/modules/settings/deployment-settings.service.d.ts.map +1 -1
  205. package/dist/modules/settings/deployment-settings.service.js +148 -54
  206. package/dist/modules/settings/deployment-settings.service.js.map +1 -1
  207. package/dist/modules/settings/repository-source.d.ts +1 -0
  208. package/dist/modules/settings/repository-source.d.ts.map +1 -1
  209. package/dist/modules/settings/repository-source.js +3 -3
  210. package/dist/modules/settings/repository-source.js.map +1 -1
  211. package/dist/modules/settings/setup.routes.d.ts.map +1 -1
  212. package/dist/modules/settings/setup.routes.js +3 -6
  213. package/dist/modules/settings/setup.routes.js.map +1 -1
  214. package/dist/modules/skills/skills.tools.d.ts.map +1 -1
  215. package/dist/modules/skills/skills.tools.js +103 -43
  216. package/dist/modules/skills/skills.tools.js.map +1 -1
  217. package/dist/modules/tool-auth/connection-key-rejection.d.ts +10 -1
  218. package/dist/modules/tool-auth/connection-key-rejection.d.ts.map +1 -1
  219. package/dist/modules/tool-auth/connection-key-rejection.js +11 -3
  220. package/dist/modules/tool-auth/connection-key-rejection.js.map +1 -1
  221. package/dist/modules/tool-auth/external-api-key.interface.d.ts +8 -0
  222. package/dist/modules/tool-auth/external-api-key.interface.d.ts.map +1 -1
  223. package/dist/modules/tool-auth/external-api-key.service.d.ts +24 -2
  224. package/dist/modules/tool-auth/external-api-key.service.d.ts.map +1 -1
  225. package/dist/modules/tool-auth/external-api-key.service.js +59 -2
  226. package/dist/modules/tool-auth/external-api-key.service.js.map +1 -1
  227. package/dist/modules/tool-auth/tool-auth.middleware.d.ts.map +1 -1
  228. package/dist/modules/tool-auth/tool-auth.middleware.js +9 -4
  229. package/dist/modules/tool-auth/tool-auth.middleware.js.map +1 -1
  230. package/dist/modules/tool-helpers/index.d.ts +28 -1
  231. package/dist/modules/tool-helpers/index.d.ts.map +1 -1
  232. package/dist/modules/tool-helpers/index.js +28 -1
  233. package/dist/modules/tool-helpers/index.js.map +1 -1
  234. package/dist/modules/tool-helpers/route-argument-check.d.ts +10 -0
  235. package/dist/modules/tool-helpers/route-argument-check.d.ts.map +1 -0
  236. package/dist/modules/tool-helpers/route-argument-check.js +132 -0
  237. package/dist/modules/tool-helpers/route-argument-check.js.map +1 -0
  238. package/dist/modules/tool-helpers/route-tool-schemas.d.ts +89 -0
  239. package/dist/modules/tool-helpers/route-tool-schemas.d.ts.map +1 -0
  240. package/dist/modules/tool-helpers/route-tool-schemas.js +113 -0
  241. package/dist/modules/tool-helpers/route-tool-schemas.js.map +1 -0
  242. package/dist/modules/tool-helpers/tool-def.d.ts +71 -0
  243. package/dist/modules/tool-helpers/tool-def.d.ts.map +1 -1
  244. package/dist/modules/tool-helpers/tool-def.js +70 -6
  245. package/dist/modules/tool-helpers/tool-def.js.map +1 -1
  246. package/dist/modules/tool-helpers/tool-handler.d.ts +46 -6
  247. package/dist/modules/tool-helpers/tool-handler.d.ts.map +1 -1
  248. package/dist/modules/tool-helpers/tool-handler.js +157 -8
  249. package/dist/modules/tool-helpers/tool-handler.js.map +1 -1
  250. package/dist/modules/tool-helpers/tool.contract.d.ts +9 -0
  251. package/dist/modules/tool-helpers/tool.contract.d.ts.map +1 -1
  252. package/dist/modules/tool-helpers/tool.contract.js.map +1 -1
  253. package/dist/modules/tool-manuals/tool-manuals.contract.d.ts +23 -4
  254. package/dist/modules/tool-manuals/tool-manuals.contract.d.ts.map +1 -1
  255. package/dist/modules/tool-manuals/tool-manuals.contract.js.map +1 -1
  256. package/dist/modules/tool-manuals/tool-manuals.service.d.ts +4 -0
  257. package/dist/modules/tool-manuals/tool-manuals.service.d.ts.map +1 -1
  258. package/dist/modules/tool-manuals/tool-manuals.service.js +14 -0
  259. package/dist/modules/tool-manuals/tool-manuals.service.js.map +1 -1
  260. package/dist/modules/tool-manuals/tool-manuals.tools.d.ts +7 -0
  261. package/dist/modules/tool-manuals/tool-manuals.tools.d.ts.map +1 -1
  262. package/dist/modules/tool-manuals/tool-manuals.tools.js +77 -38
  263. package/dist/modules/tool-manuals/tool-manuals.tools.js.map +1 -1
  264. package/dist/modules/tool-registry/description-length.d.ts +20 -16
  265. package/dist/modules/tool-registry/description-length.d.ts.map +1 -1
  266. package/dist/modules/tool-registry/description-length.js +50 -30
  267. package/dist/modules/tool-registry/description-length.js.map +1 -1
  268. package/dist/modules/tool-registry/guide-first.d.ts +27 -0
  269. package/dist/modules/tool-registry/guide-first.d.ts.map +1 -0
  270. package/dist/modules/tool-registry/guide-first.js +40 -0
  271. package/dist/modules/tool-registry/guide-first.js.map +1 -0
  272. package/dist/modules/tool-registry/tool-registry.d.ts +6 -0
  273. package/dist/modules/tool-registry/tool-registry.d.ts.map +1 -1
  274. package/dist/modules/tool-registry/tool-registry.js +9 -2
  275. package/dist/modules/tool-registry/tool-registry.js.map +1 -1
  276. package/dist/modules/workflow/agent-tools/change-request-read-shape.d.ts +449 -0
  277. package/dist/modules/workflow/agent-tools/change-request-read-shape.d.ts.map +1 -0
  278. package/dist/modules/workflow/agent-tools/change-request-read-shape.js +481 -0
  279. package/dist/modules/workflow/agent-tools/change-request-read-shape.js.map +1 -0
  280. package/dist/modules/workflow/agent-tools/change-request-read.tools.d.ts +126 -0
  281. package/dist/modules/workflow/agent-tools/change-request-read.tools.d.ts.map +1 -0
  282. package/dist/modules/workflow/agent-tools/change-request-read.tools.js +611 -0
  283. package/dist/modules/workflow/agent-tools/change-request-read.tools.js.map +1 -0
  284. package/dist/modules/workflow/agent-tools/change-request-summary.d.ts +12 -1
  285. package/dist/modules/workflow/agent-tools/change-request-summary.d.ts.map +1 -1
  286. package/dist/modules/workflow/agent-tools/change-request-summary.js +5 -1
  287. package/dist/modules/workflow/agent-tools/change-request-summary.js.map +1 -1
  288. package/dist/modules/workflow/agent-tools/workflow.tools.d.ts +8 -1
  289. package/dist/modules/workflow/agent-tools/workflow.tools.d.ts.map +1 -1
  290. package/dist/modules/workflow/agent-tools/workflow.tools.js +133 -37
  291. package/dist/modules/workflow/agent-tools/workflow.tools.js.map +1 -1
  292. package/dist/modules/workflow/file-lock.service.d.ts +29 -5
  293. package/dist/modules/workflow/file-lock.service.d.ts.map +1 -1
  294. package/dist/modules/workflow/file-lock.service.js +62 -5
  295. package/dist/modules/workflow/file-lock.service.js.map +1 -1
  296. package/dist/modules/workflow/git/git.service.d.ts +282 -14
  297. package/dist/modules/workflow/git/git.service.d.ts.map +1 -1
  298. package/dist/modules/workflow/git/git.service.js +627 -126
  299. package/dist/modules/workflow/git/git.service.js.map +1 -1
  300. package/dist/modules/workflow/git/merge-commit.d.ts +73 -0
  301. package/dist/modules/workflow/git/merge-commit.d.ts.map +1 -0
  302. package/dist/modules/workflow/git/merge-commit.js +89 -0
  303. package/dist/modules/workflow/git/merge-commit.js.map +1 -0
  304. package/dist/modules/workflow/git/node-git-runner.d.ts +15 -0
  305. package/dist/modules/workflow/git/node-git-runner.d.ts.map +1 -1
  306. package/dist/modules/workflow/git/node-git-runner.js +78 -6
  307. package/dist/modules/workflow/git/node-git-runner.js.map +1 -1
  308. package/dist/modules/workflow/git/pull-request.service.d.ts +94 -1
  309. package/dist/modules/workflow/git/pull-request.service.d.ts.map +1 -1
  310. package/dist/modules/workflow/git/pull-request.service.js +332 -37
  311. package/dist/modules/workflow/git/pull-request.service.js.map +1 -1
  312. package/dist/modules/workflow/pending-commits.service.d.ts +15 -0
  313. package/dist/modules/workflow/pending-commits.service.d.ts.map +1 -1
  314. package/dist/modules/workflow/pending-commits.service.js +25 -1
  315. package/dist/modules/workflow/pending-commits.service.js.map +1 -1
  316. package/dist/modules/workflow/pending-commits.worker.d.ts +6 -1
  317. package/dist/modules/workflow/pending-commits.worker.d.ts.map +1 -1
  318. package/dist/modules/workflow/pending-commits.worker.js +23 -6
  319. package/dist/modules/workflow/pending-commits.worker.js.map +1 -1
  320. package/dist/modules/workflow/recovery-bot.js +1 -1
  321. package/dist/modules/workflow/recovery-bot.js.map +1 -1
  322. package/dist/modules/workflow/review-workflow/review-workflow.service.d.ts +35 -0
  323. package/dist/modules/workflow/review-workflow/review-workflow.service.d.ts.map +1 -1
  324. package/dist/modules/workflow/review-workflow/review-workflow.service.js +180 -13
  325. package/dist/modules/workflow/review-workflow/review-workflow.service.js.map +1 -1
  326. package/dist/modules/workflow/sanitize-error.d.ts +16 -0
  327. package/dist/modules/workflow/sanitize-error.d.ts.map +1 -1
  328. package/dist/modules/workflow/sanitize-error.js +45 -0
  329. package/dist/modules/workflow/sanitize-error.js.map +1 -1
  330. package/dist/modules/workflow/workflow.routes.d.ts +6 -2
  331. package/dist/modules/workflow/workflow.routes.d.ts.map +1 -1
  332. package/dist/modules/workflow/workflow.routes.js +7 -2
  333. package/dist/modules/workflow/workflow.routes.js.map +1 -1
  334. package/dist/modules/workflow/workflow.service.d.ts +187 -15
  335. package/dist/modules/workflow/workflow.service.d.ts.map +1 -1
  336. package/dist/modules/workflow/workflow.service.js +808 -106
  337. package/dist/modules/workflow/workflow.service.js.map +1 -1
  338. package/dist/modules/workspace/agent-download.builder.d.ts +105 -0
  339. package/dist/modules/workspace/agent-download.builder.d.ts.map +1 -0
  340. package/dist/modules/workspace/agent-download.builder.js +238 -0
  341. package/dist/modules/workspace/agent-download.builder.js.map +1 -0
  342. package/dist/modules/workspace/agent-download.routes.d.ts +77 -0
  343. package/dist/modules/workspace/agent-download.routes.d.ts.map +1 -0
  344. package/dist/modules/workspace/agent-download.routes.js +156 -0
  345. package/dist/modules/workspace/agent-download.routes.js.map +1 -0
  346. package/dist/modules/workspace/agent-download.store.d.ts +225 -0
  347. package/dist/modules/workspace/agent-download.store.d.ts.map +1 -0
  348. package/dist/modules/workspace/agent-download.store.js +423 -0
  349. package/dist/modules/workspace/agent-download.store.js.map +1 -0
  350. package/dist/modules/workspace/agent-upload.store.d.ts +10 -1
  351. package/dist/modules/workspace/agent-upload.store.d.ts.map +1 -1
  352. package/dist/modules/workspace/agent-upload.store.js +16 -6
  353. package/dist/modules/workspace/agent-upload.store.js.map +1 -1
  354. package/dist/modules/workspace/file-readers/raw-mime.d.ts +6 -0
  355. package/dist/modules/workspace/file-readers/raw-mime.d.ts.map +1 -0
  356. package/dist/modules/workspace/file-readers/raw-mime.js +38 -0
  357. package/dist/modules/workspace/file-readers/raw-mime.js.map +1 -0
  358. package/dist/modules/workspace/file-readers/text-reader.d.ts +5 -5
  359. package/dist/modules/workspace/file-readers/text-reader.d.ts.map +1 -1
  360. package/dist/modules/workspace/file-readers/text-reader.js +5 -8
  361. package/dist/modules/workspace/file-readers/text-reader.js.map +1 -1
  362. package/dist/modules/workspace/first-run.d.ts +111 -0
  363. package/dist/modules/workspace/first-run.d.ts.map +1 -0
  364. package/dist/modules/workspace/first-run.js +181 -0
  365. package/dist/modules/workspace/first-run.js.map +1 -0
  366. package/dist/modules/workspace/move-links.d.ts +92 -0
  367. package/dist/modules/workspace/move-links.d.ts.map +1 -0
  368. package/dist/modules/workspace/move-links.js +173 -0
  369. package/dist/modules/workspace/move-links.js.map +1 -0
  370. package/dist/modules/workspace/startup/steps/seed-tree.d.ts.map +1 -1
  371. package/dist/modules/workspace/startup/steps/seed-tree.js +24 -39
  372. package/dist/modules/workspace/startup/steps/seed-tree.js.map +1 -1
  373. package/dist/modules/workspace/startup/steps/template-files.step.d.ts +58 -52
  374. package/dist/modules/workspace/startup/steps/template-files.step.d.ts.map +1 -1
  375. package/dist/modules/workspace/startup/steps/template-files.step.js +209 -223
  376. package/dist/modules/workspace/startup/steps/template-files.step.js.map +1 -1
  377. package/dist/modules/workspace/startup/steps/template-source.d.ts +5 -3
  378. package/dist/modules/workspace/startup/steps/template-source.d.ts.map +1 -1
  379. package/dist/modules/workspace/startup/steps/template-source.js +5 -3
  380. package/dist/modules/workspace/startup/steps/template-source.js.map +1 -1
  381. package/dist/modules/workspace/workspace.routes.d.ts.map +1 -1
  382. package/dist/modules/workspace/workspace.routes.js +63 -22
  383. package/dist/modules/workspace/workspace.routes.js.map +1 -1
  384. package/dist/modules/workspace/workspace.service.d.ts +75 -2
  385. package/dist/modules/workspace/workspace.service.d.ts.map +1 -1
  386. package/dist/modules/workspace/workspace.service.js +197 -24
  387. package/dist/modules/workspace/workspace.service.js.map +1 -1
  388. package/dist/modules/workspace/workspace.tools.d.ts +68 -1
  389. package/dist/modules/workspace/workspace.tools.d.ts.map +1 -1
  390. package/dist/modules/workspace/workspace.tools.js +642 -71
  391. package/dist/modules/workspace/workspace.tools.js.map +1 -1
  392. package/dist/modules/write-access/write-access.d.ts.map +1 -1
  393. package/dist/modules/write-access/write-access.js +9 -0
  394. package/dist/modules/write-access/write-access.js.map +1 -1
  395. package/dist/shared/domain-errors.d.ts +84 -1
  396. package/dist/shared/domain-errors.d.ts.map +1 -1
  397. package/dist/shared/domain-errors.js +100 -12
  398. package/dist/shared/domain-errors.js.map +1 -1
  399. package/dist/shared/git.contract.d.ts +10 -1
  400. package/dist/shared/git.contract.d.ts.map +1 -1
  401. package/dist/shared/git.contract.js.map +1 -1
  402. package/dist/shared/hidden-tools.d.ts +44 -0
  403. package/dist/shared/hidden-tools.d.ts.map +1 -0
  404. package/dist/shared/hidden-tools.js +13 -0
  405. package/dist/shared/hidden-tools.js.map +1 -0
  406. package/dist/shared/utf8-text.d.ts +15 -0
  407. package/dist/shared/utf8-text.d.ts.map +1 -0
  408. package/dist/shared/utf8-text.js +20 -0
  409. package/dist/shared/utf8-text.js.map +1 -0
  410. package/dist/tenancy/static-tenant-source.d.ts +2 -1
  411. package/dist/tenancy/static-tenant-source.d.ts.map +1 -1
  412. package/dist/tenancy/static-tenant-source.js +9 -2
  413. package/dist/tenancy/static-tenant-source.js.map +1 -1
  414. package/kb-template/.bevelignore +0 -5
  415. package/mcp-app/page.html +349 -0
  416. package/migrations/0017_atlassian_account_links.sql +37 -0
  417. package/migrations/meta/0017_snapshot.json +2396 -0
  418. package/migrations/meta/_journal.json +8 -1
  419. package/package.json +6 -3
  420. package/src/__tests__/kb-layout-config.test.ts +10 -100
  421. package/src/__tests__/packaged-assets-ship.test.ts +66 -0
  422. package/src/assets.ts +38 -2
  423. package/src/core/__tests__/catalog-revision.test.ts +40 -0
  424. package/src/core/__tests__/gated-boot.test.ts +1 -0
  425. package/src/core/__tests__/lifecycle.test.ts +14 -0
  426. package/src/core/catalog-revision.ts +15 -2
  427. package/src/core/core-ports.ts +11 -0
  428. package/src/core/create-core-server.ts +78 -3
  429. package/src/core/create-core-services.ts +168 -10
  430. package/src/core/lifecycle.ts +28 -2
  431. package/src/core-config.ts +24 -1
  432. package/src/index.ts +2 -2
  433. package/src/modules/access/__tests__/access-control.atref-batch.test.ts +58 -0
  434. package/src/modules/access/__tests__/access-control.platform-restore.test.ts +8 -7
  435. package/src/modules/access/__tests__/access-control.service.test.ts +19 -0
  436. package/src/modules/access/__tests__/access-personal-plugin.test.ts +1 -18
  437. package/src/modules/access/access-control.interface.ts +28 -0
  438. package/src/modules/access/access-control.service.ts +37 -0
  439. package/src/modules/access/directory-sync-bot.ts +1 -1
  440. package/src/modules/agent-guide/__tests__/agent-guide.test.ts +386 -0
  441. package/src/modules/agent-guide/__tests__/agent-guide.tools.test.ts +207 -0
  442. package/src/modules/agent-guide/agent-guide.tools.ts +122 -0
  443. package/src/modules/agent-guide/agent-guide.ts +300 -0
  444. package/src/modules/agent-guide/index.ts +22 -0
  445. package/src/modules/agent-instructions/__tests__/shared-file-rules.test.ts +39 -118
  446. package/src/modules/agent-instructions/agent-instructions.routes.ts +3 -2
  447. package/src/modules/agent-instructions/compose.ts +26 -8
  448. package/src/modules/agent-instructions/index.ts +0 -3
  449. package/src/modules/agent-instructions/shared-file-rules.ts +50 -112
  450. package/src/modules/audit/__tests__/agent-audit.service.test.ts +64 -4
  451. package/src/modules/audit/__tests__/agent-connection.routes.test.ts +102 -0
  452. package/src/modules/audit/agent-audit.service.ts +58 -3
  453. package/src/modules/audit/agent-connection.routes.ts +56 -0
  454. package/src/modules/audit/audit.contract.ts +26 -0
  455. package/src/modules/audit/index.ts +1 -0
  456. package/src/modules/auth/__tests__/account.routes.test.ts +5 -3
  457. package/src/modules/auth/account.routes.ts +10 -3
  458. package/src/modules/auth/auth.middleware.ts +1 -1
  459. package/src/modules/code-mode/__tests__/code-mode.tool.test.ts +18 -2
  460. package/src/modules/code-mode/code-mode.tool.ts +13 -2
  461. package/src/modules/database/core-schema.ts +33 -3
  462. package/src/modules/embed/__tests__/account-links.migration.test.ts +98 -0
  463. package/src/modules/embed/__tests__/embed-link.test.ts +122 -0
  464. package/src/modules/embed/__tests__/embed.routes.test.ts +362 -0
  465. package/src/modules/embed/__tests__/embed.service.lock.test.ts +193 -0
  466. package/src/modules/embed/__tests__/embed.service.test.ts +805 -0
  467. package/src/modules/embed/__tests__/embed.tools.test.ts +304 -0
  468. package/src/modules/embed/__tests__/mcp-app.test.ts +207 -0
  469. package/src/modules/embed/account-link.service.ts +76 -0
  470. package/src/modules/embed/embed-link.ts +180 -0
  471. package/src/modules/embed/embed.errors.ts +43 -0
  472. package/src/modules/embed/embed.interface.ts +203 -0
  473. package/src/modules/embed/embed.routes.ts +363 -0
  474. package/src/modules/embed/embed.service.ts +705 -0
  475. package/src/modules/embed/embed.tools.ts +206 -0
  476. package/src/modules/embed/mcp-app.routes.ts +41 -0
  477. package/src/modules/embed/mcp-app.ts +173 -0
  478. package/src/modules/github-app/__tests__/github-app.test.ts +76 -0
  479. package/src/modules/github-app/github-app.connection.ts +38 -4
  480. package/src/modules/kb-fs/__tests__/move-with-edits.test.ts +304 -0
  481. package/src/modules/kb-fs/locking-filesystem.ts +287 -0
  482. package/src/modules/kb-sync/__tests__/kb-sync.service.test.ts +35 -0
  483. package/src/modules/kb-sync/kb-sync.interface.ts +2 -0
  484. package/src/modules/kb-sync/kb-sync.service.ts +6 -0
  485. package/src/modules/mcp/__tests__/bevel-oauth-provider.test.ts +68 -11
  486. package/src/modules/mcp/__tests__/fake-downstream-mcp-server.ts +14 -3
  487. package/src/modules/mcp/__tests__/mcp-proxy-helpers.test.ts +37 -2
  488. package/src/modules/mcp/__tests__/mcp.app.e2e.test.ts +200 -0
  489. package/src/modules/mcp/__tests__/mcp.e2e.test.ts +252 -0
  490. package/src/modules/mcp/__tests__/mcp.service.test.ts +42 -24
  491. package/src/modules/mcp/__tests__/tool-call-interface.e2e.test.ts +499 -0
  492. package/src/modules/mcp/__tests__/tool-schema-guard.test.ts +266 -0
  493. package/src/modules/mcp/mcp-auth.middleware.ts +2 -1
  494. package/src/modules/mcp/mcp.service.ts +218 -31
  495. package/src/modules/mcp/oauth/bevel-oauth-provider.ts +42 -4
  496. package/src/modules/mcp/tool-schema-guard.ts +196 -0
  497. package/src/modules/onboarding/__tests__/onboarding.routes.test.ts +163 -0
  498. package/src/modules/onboarding/__tests__/starter-pack.service.test.ts +635 -0
  499. package/src/modules/onboarding/__tests__/starter-packs.test.ts +169 -0
  500. package/src/modules/onboarding/onboarding.contract.ts +85 -0
  501. package/src/modules/onboarding/onboarding.routes.ts +85 -0
  502. package/src/modules/onboarding/starter-pack.service.ts +580 -0
  503. package/src/modules/onboarding/starter-packs.ts +182 -0
  504. package/src/modules/plugins/__tests__/plugins.tools.test.ts +177 -4
  505. package/src/modules/plugins/index.ts +1 -0
  506. package/src/modules/plugins/plugin-provision.service.ts +22 -1
  507. package/src/modules/plugins/plugins.routes.ts +29 -0
  508. package/src/modules/plugins/plugins.tools.ts +75 -15
  509. package/src/modules/settings/__tests__/deployment-settings.service.test.ts +176 -56
  510. package/src/modules/settings/__tests__/repository-source.test.ts +9 -0
  511. package/src/modules/settings/deployment-settings.service.ts +144 -55
  512. package/src/modules/settings/repository-source.ts +4 -4
  513. package/src/modules/settings/setup.routes.ts +3 -6
  514. package/src/modules/skills/__tests__/skills.tools.description.test.ts +95 -0
  515. package/src/modules/skills/skills.tools.ts +112 -46
  516. package/src/modules/tool-auth/__tests__/connection-key-rejection.test.ts +32 -0
  517. package/src/modules/tool-auth/__tests__/external-api-key.service.test.ts +65 -0
  518. package/src/modules/tool-auth/connection-key-rejection.ts +13 -3
  519. package/src/modules/tool-auth/external-api-key.interface.ts +9 -0
  520. package/src/modules/tool-auth/external-api-key.service.ts +61 -4
  521. package/src/modules/tool-auth/tool-auth.middleware.ts +13 -4
  522. package/src/modules/tool-helpers/__tests__/branch-declaration.test.ts +502 -0
  523. package/src/modules/tool-helpers/__tests__/route-argument-check.test.ts +337 -0
  524. package/src/modules/tool-helpers/index.ts +34 -1
  525. package/src/modules/tool-helpers/route-argument-check.ts +149 -0
  526. package/src/modules/tool-helpers/route-tool-schemas.ts +171 -0
  527. package/src/modules/tool-helpers/tool-def.ts +132 -7
  528. package/src/modules/tool-helpers/tool-handler.ts +196 -8
  529. package/src/modules/tool-helpers/tool.contract.ts +9 -0
  530. package/src/modules/tool-manuals/__tests__/tool-manuals.detail.route.test.ts +57 -0
  531. package/src/modules/tool-manuals/__tests__/tool-manuals.tools.test.ts +73 -4
  532. package/src/modules/tool-manuals/tool-manuals.contract.ts +24 -4
  533. package/src/modules/tool-manuals/tool-manuals.service.ts +17 -0
  534. package/src/modules/tool-manuals/tool-manuals.tools.ts +86 -38
  535. package/src/modules/tool-registry/__tests__/own-tool-schemas.test.ts +173 -0
  536. package/src/modules/tool-registry/__tests__/tool-description-length.test.ts +81 -66
  537. package/src/modules/tool-registry/description-length.ts +57 -32
  538. package/src/modules/tool-registry/guide-first.ts +41 -0
  539. package/src/modules/tool-registry/tool-registry.ts +9 -2
  540. package/src/modules/workflow/__tests__/apply-failure.test.ts +6 -1
  541. package/src/modules/workflow/__tests__/file-lock.service.deletion-gate.test.ts +104 -0
  542. package/src/modules/workflow/__tests__/pending-commits.repository-replaced.test.ts +66 -0
  543. package/src/modules/workflow/__tests__/pending-commits.worker.test.ts +31 -2
  544. package/src/modules/workflow/__tests__/preserve-roles-yaml.test.ts +110 -6
  545. package/src/modules/workflow/__tests__/sanitize-error.test.ts +89 -1
  546. package/src/modules/workflow/__tests__/workflow.routes.push-refused.test.ts +202 -0
  547. package/src/modules/workflow/__tests__/workflow.service.branch-in-use.test.ts +6 -6
  548. package/src/modules/workflow/__tests__/workflow.service.delete-branch.test.ts +1026 -0
  549. package/src/modules/workflow/__tests__/workflow.service.facade.test.ts +468 -10
  550. package/src/modules/workflow/__tests__/workflow.service.folder-proposals.test.ts +4 -0
  551. package/src/modules/workflow/__tests__/workflow.service.releaseLock.test.ts +5 -4
  552. package/src/modules/workflow/__tests__/workflow.service.update-from-target.test.ts +54 -2
  553. package/src/modules/workflow/agent-tools/__tests__/change-request-read-shape.test.ts +705 -0
  554. package/src/modules/workflow/agent-tools/__tests__/change-request-read.tools.test.ts +1554 -0
  555. package/src/modules/workflow/agent-tools/__tests__/workflow.tools.test.ts +150 -4
  556. package/src/modules/workflow/agent-tools/change-request-read-shape.ts +712 -0
  557. package/src/modules/workflow/agent-tools/change-request-read.tools.ts +759 -0
  558. package/src/modules/workflow/agent-tools/change-request-summary.ts +5 -1
  559. package/src/modules/workflow/agent-tools/workflow.tools.ts +137 -35
  560. package/src/modules/workflow/file-lock.service.ts +67 -5
  561. package/src/modules/workflow/git/__tests__/git.service.appliedChange.test.ts +285 -0
  562. package/src/modules/workflow/git/__tests__/git.service.changedFilesForPr.test.ts +124 -0
  563. package/src/modules/workflow/git/__tests__/git.service.deleteBranch.test.ts +164 -0
  564. package/src/modules/workflow/git/__tests__/git.service.mergeChangeRequest.test.ts +334 -0
  565. package/src/modules/workflow/git/__tests__/git.service.pull.test.ts +48 -1
  566. package/src/modules/workflow/git/__tests__/node-git-runner.test.ts +231 -2
  567. package/src/modules/workflow/git/__tests__/pull-request.service.list-fetch.test.ts +72 -2
  568. package/src/modules/workflow/git/__tests__/pull-request.service.placeholder.test.ts +24 -2
  569. package/src/modules/workflow/git/__tests__/pull-request.service.test.ts +620 -1
  570. package/src/modules/workflow/git/git.service.ts +714 -133
  571. package/src/modules/workflow/git/merge-commit.ts +88 -0
  572. package/src/modules/workflow/git/node-git-runner.ts +82 -7
  573. package/src/modules/workflow/git/pull-request.service.ts +380 -54
  574. package/src/modules/workflow/pending-commits.service.ts +29 -1
  575. package/src/modules/workflow/pending-commits.worker.ts +24 -6
  576. package/src/modules/workflow/recovery-bot.ts +1 -1
  577. package/src/modules/workflow/review-workflow/__tests__/approval-states.test.ts +7 -1
  578. package/src/modules/workflow/review-workflow/__tests__/merge-records-own-commit.test.ts +407 -0
  579. package/src/modules/workflow/review-workflow/review-workflow.service.ts +191 -12
  580. package/src/modules/workflow/sanitize-error.ts +50 -0
  581. package/src/modules/workflow/workflow.routes.ts +7 -2
  582. package/src/modules/workflow/workflow.service.ts +886 -112
  583. package/src/modules/workspace/__tests__/agent-downloads.test.ts +823 -0
  584. package/src/modules/workspace/__tests__/escape-sequences.routes.test.ts +4 -3
  585. package/src/modules/workspace/__tests__/first-run.test.ts +234 -0
  586. package/src/modules/workspace/__tests__/git-internals.security.test.ts +5 -2
  587. package/src/modules/workspace/__tests__/md-links.test.ts +353 -0
  588. package/src/modules/workspace/__tests__/move-links.test.ts +261 -0
  589. package/src/modules/workspace/__tests__/rest-tool-arguments.test.ts +219 -0
  590. package/src/modules/workspace/__tests__/workspace-paths-inside-the-repo.test.ts +3 -0
  591. package/src/modules/workspace/__tests__/workspace.routes.delete.test.ts +102 -0
  592. package/src/modules/workspace/__tests__/workspace.routes.download.test.ts +52 -5
  593. package/src/modules/workspace/__tests__/workspace.routes.folder-zip-access.test.ts +190 -0
  594. package/src/modules/workspace/__tests__/workspace.routes.move-platform-files.test.ts +21 -10
  595. package/src/modules/workspace/__tests__/workspace.service.forget-clone-races.test.ts +8 -4
  596. package/src/modules/workspace/__tests__/workspace.service.test.ts +50 -8
  597. package/src/modules/workspace/__tests__/workspace.tools.agents-file.test.ts +33 -55
  598. package/src/modules/workspace/__tests__/workspace.tools.branch-errors.test.ts +44 -1
  599. package/src/modules/workspace/__tests__/workspace.tools.test.ts +664 -34
  600. package/src/modules/workspace/agent-download.builder.ts +308 -0
  601. package/src/modules/workspace/agent-download.routes.ts +183 -0
  602. package/src/modules/workspace/agent-download.store.ts +533 -0
  603. package/src/modules/workspace/agent-upload.store.ts +23 -7
  604. package/src/modules/workspace/file-readers/raw-mime.ts +40 -0
  605. package/src/modules/workspace/file-readers/text-reader.ts +5 -8
  606. package/src/modules/workspace/first-run.ts +222 -0
  607. package/src/modules/workspace/move-links.ts +237 -0
  608. package/src/modules/workspace/startup/steps/__tests__/steps.test.ts +191 -489
  609. package/src/modules/workspace/startup/steps/seed-tree.ts +23 -38
  610. package/src/modules/workspace/startup/steps/template-files.step.ts +217 -249
  611. package/src/modules/workspace/startup/steps/template-source.ts +5 -3
  612. package/src/modules/workspace/workspace.routes.ts +64 -22
  613. package/src/modules/workspace/workspace.service.ts +202 -25
  614. package/src/modules/workspace/workspace.tools.ts +707 -68
  615. package/src/modules/write-access/__tests__/write-access.test.ts +13 -0
  616. package/src/modules/write-access/write-access.ts +9 -0
  617. package/src/shared/domain-errors.ts +123 -11
  618. package/src/shared/git.contract.ts +8 -1
  619. package/src/shared/hidden-tools.ts +45 -0
  620. package/src/shared/utf8-text.ts +21 -0
  621. package/src/tenancy/__tests__/static-tenant-source.test.ts +1 -0
  622. package/src/tenancy/static-tenant-source.ts +10 -2
  623. package/starter-packs/README.md +103 -0
  624. package/starter-packs/engineering/KnowledgeBase/About us.md +9 -0
  625. package/starter-packs/engineering/KnowledgeBase/Architecture.md +9 -0
  626. package/starter-packs/engineering/KnowledgeBase/Glossary.md +9 -0
  627. package/starter-packs/engineering/KnowledgeBase/How we ship.md +9 -0
  628. package/starter-packs/engineering/Plugins/engineering-starter/LICENSE +21 -0
  629. package/starter-packs/engineering/Plugins/engineering-starter/SOURCE.md +20 -0
  630. package/starter-packs/engineering/Plugins/engineering-starter/access.md +19 -0
  631. package/starter-packs/engineering/Plugins/engineering-starter/plugin.json +10 -0
  632. package/starter-packs/engineering/Plugins/engineering-starter/skills/office-hours/SKILL.md +171 -0
  633. package/starter-packs/engineering/Plugins/engineering-starter/skills/office-hours/sections/phase-2a-startup-diagnostic.md +157 -0
  634. package/starter-packs/engineering/Plugins/engineering-starter/skills/office-hours/sections/phase-2b-builder-brainstorm.md +39 -0
  635. package/starter-packs/engineering/Plugins/engineering-starter/skills/plan-ceo-review/SKILL.md +144 -0
  636. package/starter-packs/engineering/Plugins/engineering-starter/skills/plan-eng-review/SKILL.md +144 -0
  637. package/starter-packs/engineering/Plugins/engineering-starter/skills/review/SKILL.md +80 -0
  638. package/starter-packs/engineering/Plugins/engineering-starter/skills/review/checklist.md +184 -0
  639. package/starter-packs/engineering/pack.yaml +11 -0
  640. package/starter-packs/general/KnowledgeBase/About us.md +9 -0
  641. package/starter-packs/general/KnowledgeBase/Glossary.md +9 -0
  642. package/starter-packs/general/KnowledgeBase/How we work.md +9 -0
  643. package/starter-packs/general/Plugins/general-starter/LICENSE +202 -0
  644. package/starter-packs/general/Plugins/general-starter/SOURCE.md +10 -0
  645. package/starter-packs/general/Plugins/general-starter/access.md +19 -0
  646. package/starter-packs/general/Plugins/general-starter/plugin.json +10 -0
  647. package/starter-packs/general/Plugins/general-starter/skills/memory-management/SKILL.md +332 -0
  648. package/starter-packs/general/Plugins/general-starter/skills/productivity-start/SKILL.md +163 -0
  649. package/starter-packs/general/Plugins/general-starter/skills/productivity-update/SKILL.md +167 -0
  650. package/starter-packs/general/Plugins/general-starter/skills/task-management/SKILL.md +83 -0
  651. package/starter-packs/general/pack.yaml +10 -0
  652. package/starter-packs/operations/KnowledgeBase/Glossary.md +9 -0
  653. package/starter-packs/operations/KnowledgeBase/How we work.md +9 -0
  654. package/starter-packs/operations/KnowledgeBase/Processes.md +9 -0
  655. package/starter-packs/operations/KnowledgeBase/Tools we use.md +9 -0
  656. package/starter-packs/operations/KnowledgeBase/Vendors.md +9 -0
  657. package/starter-packs/operations/Plugins/operations-starter/LICENSE +202 -0
  658. package/starter-packs/operations/Plugins/operations-starter/SOURCE.md +10 -0
  659. package/starter-packs/operations/Plugins/operations-starter/access.md +19 -0
  660. package/starter-packs/operations/Plugins/operations-starter/plugin.json +10 -0
  661. package/starter-packs/operations/Plugins/operations-starter/skills/capacity-plan/SKILL.md +111 -0
  662. package/starter-packs/operations/Plugins/operations-starter/skills/change-request/SKILL.md +119 -0
  663. package/starter-packs/operations/Plugins/operations-starter/skills/compliance-tracking/SKILL.md +44 -0
  664. package/starter-packs/operations/Plugins/operations-starter/skills/process-doc/SKILL.md +80 -0
  665. package/starter-packs/operations/Plugins/operations-starter/skills/process-optimization/SKILL.md +39 -0
  666. package/starter-packs/operations/Plugins/operations-starter/skills/risk-assessment/SKILL.md +40 -0
  667. package/starter-packs/operations/Plugins/operations-starter/skills/runbook/SKILL.md +85 -0
  668. package/starter-packs/operations/Plugins/operations-starter/skills/status-report/SKILL.md +75 -0
  669. package/starter-packs/operations/Plugins/operations-starter/skills/vendor-review/SKILL.md +102 -0
  670. package/starter-packs/operations/pack.yaml +12 -0
  671. package/starter-packs/sales/KnowledgeBase/About us.md +9 -0
  672. package/starter-packs/sales/KnowledgeBase/Customers.md +9 -0
  673. package/starter-packs/sales/KnowledgeBase/Glossary.md +9 -0
  674. package/starter-packs/sales/KnowledgeBase/How we sell.md +9 -0
  675. package/starter-packs/sales/KnowledgeBase/Objections.md +9 -0
  676. package/starter-packs/sales/KnowledgeBase/Pricing.md +9 -0
  677. package/starter-packs/sales/Plugins/sales-starter/LICENSE +202 -0
  678. package/starter-packs/sales/Plugins/sales-starter/SOURCE.md +10 -0
  679. package/starter-packs/sales/Plugins/sales-starter/access.md +19 -0
  680. package/starter-packs/sales/Plugins/sales-starter/plugin.json +10 -0
  681. package/starter-packs/sales/Plugins/sales-starter/skills/account-context/SKILL.md +94 -0
  682. package/starter-packs/sales/Plugins/sales-starter/skills/account-plan/SKILL.md +95 -0
  683. package/starter-packs/sales/Plugins/sales-starter/skills/account-research/SKILL.md +99 -0
  684. package/starter-packs/sales/Plugins/sales-starter/skills/account-tiering/SKILL.md +101 -0
  685. package/starter-packs/sales/Plugins/sales-starter/skills/call-prep/SKILL.md +111 -0
  686. package/starter-packs/sales/Plugins/sales-starter/skills/call-summary/SKILL.md +111 -0
  687. package/starter-packs/sales/Plugins/sales-starter/skills/close-plan/SKILL.md +106 -0
  688. package/starter-packs/sales/Plugins/sales-starter/skills/competitive-intelligence/SKILL.md +102 -0
  689. package/starter-packs/sales/Plugins/sales-starter/skills/create-an-asset/SKILL.md +105 -0
  690. package/starter-packs/sales/Plugins/sales-starter/skills/crm-hygiene-check/SKILL.md +103 -0
  691. package/starter-packs/sales/Plugins/sales-starter/skills/customer-health/SKILL.md +108 -0
  692. package/starter-packs/sales/Plugins/sales-starter/skills/customer-voice/SKILL.md +94 -0
  693. package/starter-packs/sales/Plugins/sales-starter/skills/daily-briefing/SKILL.md +61 -0
  694. package/starter-packs/sales/Plugins/sales-starter/skills/deal-advance-gap/SKILL.md +102 -0
  695. package/starter-packs/sales/Plugins/sales-starter/skills/deal-review/SKILL.md +105 -0
  696. package/starter-packs/sales/Plugins/sales-starter/skills/deal-signals/SKILL.md +96 -0
  697. package/starter-packs/sales/Plugins/sales-starter/skills/deal-slip-scenario/SKILL.md +104 -0
  698. package/starter-packs/sales/Plugins/sales-starter/skills/draft-outreach/SKILL.md +116 -0
  699. package/starter-packs/sales/Plugins/sales-starter/skills/end-of-day/SKILL.md +114 -0
  700. package/starter-packs/sales/Plugins/sales-starter/skills/expansion-whitespace/SKILL.md +97 -0
  701. package/starter-packs/sales/Plugins/sales-starter/skills/forecast/SKILL.md +100 -0
  702. package/starter-packs/sales/Plugins/sales-starter/skills/handle-objection/SKILL.md +93 -0
  703. package/starter-packs/sales/Plugins/sales-starter/skills/inbox-sweep/SKILL.md +114 -0
  704. package/starter-packs/sales/Plugins/sales-starter/skills/lead-triage/SKILL.md +106 -0
  705. package/starter-packs/sales/Plugins/sales-starter/skills/log-activity/SKILL.md +111 -0
  706. package/starter-packs/sales/Plugins/sales-starter/skills/pipeline-review/SKILL.md +97 -0
  707. package/starter-packs/sales/Plugins/sales-starter/skills/renewal-radar/SKILL.md +95 -0
  708. package/starter-packs/sales/Plugins/sales-starter/skills/rep-context/SKILL.md +90 -0
  709. package/starter-packs/sales/Plugins/sales-starter/skills/route-lead/SKILL.md +111 -0
  710. package/starter-packs/sales/Plugins/sales-starter/skills/sales-setup/SKILL.md +200 -0
  711. package/starter-packs/sales/Plugins/sales-starter/skills/schedule-meeting/SKILL.md +99 -0
  712. package/starter-packs/sales/Plugins/sales-starter/skills/stakeholder-map/SKILL.md +107 -0
  713. package/starter-packs/sales/Plugins/sales-starter/skills/team-pipeline/SKILL.md +105 -0
  714. package/starter-packs/sales/Plugins/sales-starter/skills/update-opportunity/SKILL.md +112 -0
  715. package/starter-packs/sales/Plugins/sales-starter/skills/weekly-wrap/SKILL.md +90 -0
  716. package/starter-packs/sales/Plugins/sales-starter/skills/win-loss-review/SKILL.md +88 -0
  717. package/starter-packs/sales/pack.yaml +13 -0
  718. package/starter-packs/support/KnowledgeBase/Escalations.md +9 -0
  719. package/starter-packs/support/KnowledgeBase/Glossary.md +9 -0
  720. package/starter-packs/support/KnowledgeBase/Known issues.md +9 -0
  721. package/starter-packs/support/KnowledgeBase/Products.md +9 -0
  722. package/starter-packs/support/KnowledgeBase/Tone of voice.md +9 -0
  723. package/starter-packs/support/Plugins/support-starter/LICENSE +202 -0
  724. package/starter-packs/support/Plugins/support-starter/SOURCE.md +10 -0
  725. package/starter-packs/support/Plugins/support-starter/access.md +19 -0
  726. package/starter-packs/support/Plugins/support-starter/plugin.json +10 -0
  727. package/starter-packs/support/Plugins/support-starter/skills/customer-escalation/SKILL.md +246 -0
  728. package/starter-packs/support/Plugins/support-starter/skills/customer-research/SKILL.md +250 -0
  729. package/starter-packs/support/Plugins/support-starter/skills/draft-response/SKILL.md +416 -0
  730. package/starter-packs/support/Plugins/support-starter/skills/kb-article/SKILL.md +350 -0
  731. package/starter-packs/support/Plugins/support-starter/skills/ticket-triage/SKILL.md +273 -0
  732. package/starter-packs/support/pack.yaml +12 -0
  733. package/kb-template/AGENTS.md +0 -730
@@ -2,8 +2,11 @@ import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import { logger } from '../../../shared/logging.js';
4
4
  import type {
5
+ AppliedChangeRef,
6
+ AppliedMergeResult,
5
7
  AuthUser,
6
8
  BranchInfo,
9
+ ChangedPathPair,
7
10
  CommitAttribution,
8
11
  IGitService,
9
12
  RemoteSyncPullResult,
@@ -13,6 +16,7 @@ import type {
13
16
  WorkingTreeStatus,
14
17
  } from '@bevel-software/platform-shared';
15
18
  import { isFolderPlaceholder } from '@bevel-software/platform-shared';
19
+ import { mergeCommitMessageNames } from './merge-commit.js';
16
20
  import type { WorkspaceService } from '../../workspace/workspace.service.js';
17
21
  import type { WorkflowHooks, CommitValidationContext } from '../workflow-hooks.js';
18
22
  import type { IAccessControl } from '../../access/access-control.interface.js';
@@ -29,7 +33,9 @@ import {
29
33
  } from '../../kb-fs/branch-name.js';
30
34
  import type { KbContext } from '../../../shared/kb-context.js';
31
35
  import {
36
+ AppliedChangeMismatchError,
32
37
  BranchAuthorshipError,
38
+ BranchDeleteRefusedError,
33
39
  WorkflowDomainError,
34
40
  WorkflowValidationError,
35
41
  ProtectedBranchError,
@@ -738,12 +744,22 @@ export class GitService implements IGitService {
738
744
  // fresh per-branch workspace gets its backup seeded by the workspace
739
745
  // bootstrap path, not by a branch switch on an existing one.
740
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
+ */
741
757
  async deleteBranch(
742
758
  workspaceId: string,
743
759
  name: string,
744
760
  user: AuthUser,
745
- opts: { onlyIfNoRemote?: boolean; systemCleanup?: boolean } = {},
746
- ): Promise<void> {
761
+ opts: { onlyIfNoRemote?: boolean; systemCleanup?: boolean; expectTip?: string } = {},
762
+ ): Promise<{ lastCommit: string | null }> {
747
763
  assertValidBranchName(name);
748
764
  if (this.kb.isProtectedBranch(name)) {
749
765
  throw new ProtectedBranchError(name, 'deleting a protected branch');
@@ -772,28 +788,33 @@ export class GitService implements IGitService {
772
788
  `Branch "${name}" still exists on origin — refusing to auto-prune.`,
773
789
  );
774
790
  }
775
- } else {
776
- // Authored-or-admin delete path. The branch's author (per the
777
- // `<email-localpart>/...` naming convention — see `isBranchAuthoredBy`)
778
- // can always delete their own draft. Admins (per the workspace's
779
- // `roles.yaml`) can additionally clean up anyone's branch, including
780
- // unprefixed CLI branches like `fix/...` that have no recognisable
781
- // author. We detect admin via `canWrite(_, _, 'roles.yaml')` because
782
- // that predicate is the existing source of truth for `Admin`-role
783
- // membership inside the access model — no separate `isAdmin()` plumbing
784
- // needed.
785
- // Two authorship conventions: `<localpart>/…` UI drafts, and the
786
- // `suggestions/<who>-<id8>/…` namespace the propose flow files its
787
- // branches under — a user resetting THEIR OWN suggestion bundle is
788
- // deleting their own branch.
789
- const isAuthor =
790
- isBranchAuthoredBy(name, user.email) ||
791
- isOwnSuggestionsBranch(name, { email: user.email, id: user.id });
792
- const isAdmin = this.accessControl
793
- ? await this.accessControl.canWrite(workspaceId, user.email, 'roles.yaml')
794
- : false;
795
- if (!isAuthor && !isAdmin) {
796
- 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);
797
818
  }
798
819
  }
799
820
 
@@ -805,19 +826,91 @@ export class GitService implements IGitService {
805
826
  // Skip when no local ref exists — the picker can surface branches that
806
827
  // exist only as `refs/remotes/origin/<name>` (the user hasn't worked on
807
828
  // them locally), and "discard from origin" must still work for those.
829
+ let localTip: string | null = null;
808
830
  if (await this.refExists(cwd, `refs/heads/${name}`)) {
831
+ localTip = await this.revParseOrNull(cwd, `refs/heads/${name}`);
809
832
  await this.git(cwd, ['branch', '-D', name]);
810
833
  }
834
+ return { lastCommit: remoteTip ?? localTip };
835
+ });
836
+ }
837
+
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
+ }
811
862
 
812
- // Remote delete. Skip in `onlyIfNoRemote` mode (the contract is
813
- // local-only cleanup) and skip when there's no remote ref to delete.
814
- // A push failure here surfaces to the caller — the local ref is
815
- // already gone, and we don't try to resurrect it because rolling back
816
- // a "merged-into-the-deleted-branch" local commit would be even more
817
- // confusing than the half-finished state.
818
- if (!opts.onlyIfNoRemote
819
- && await this.refExists(cwd, `refs/remotes/origin/${name}`)) {
820
- await this.git(cwd, ['push', 'origin', '--delete', name]);
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;
821
914
  }
822
915
  });
823
916
  }
@@ -1442,7 +1535,9 @@ export class GitService implements IGitService {
1442
1535
  * the new base and re-merge on top of it. Conflicts are deterministic — they
1443
1536
  * return immediately (no retry) so the caller can route into resolution.
1444
1537
  *
1445
- * Returns the merge commit SHA, or the conflicting paths.
1538
+ * Returns the state the target is left at (`sha`) together with the merge
1539
+ * commit this change request owns (`mergeCommit`, null when there is none), or
1540
+ * the conflicting paths.
1446
1541
  */
1447
1542
  async mergeChangeRequest(
1448
1543
  baseWorkspaceId: string,
@@ -1477,11 +1572,33 @@ export class GitService implements IGitService {
1477
1572
  * instead of falling back to the stale one.
1478
1573
  */
1479
1574
  authorize?: (target: { sha: string; changedPaths: string[] }) => Promise<void>;
1575
+ /**
1576
+ * The change request this merge applies, when it is one (the agent
1577
+ * `merge_branch` path merges no request and leaves it unset).
1578
+ *
1579
+ * It is what lets the "nothing to merge" case tell apart a request that
1580
+ * never had anything to land from one whose merge commit an EARLIER
1581
+ * attempt already pushed — see {@link ownMergeCommitOn}. Without it the
1582
+ * second is indistinguishable from the first, and finalizing it would
1583
+ * forget the only commit its files can be read from.
1584
+ */
1585
+ appliedChangeNumber?: number;
1586
+ /** The request's stored title, so a merge commit in the old message format is recognised too. */
1587
+ appliedChangeTitle?: string;
1480
1588
  } = {},
1481
- ): Promise<{ kind: 'merged'; sha: string } | { kind: 'conflicts'; paths: string[] }> {
1589
+ ): Promise<AppliedMergeResult> {
1482
1590
  assertValidBranchName(sourceBranch);
1483
1591
  assertValidBranchName(targetBranch);
1484
1592
  assertValidAuthor(user);
1593
+ // The same single-line rule `commit` applies, and for a sharper reason here:
1594
+ // `git commit -m` takes the first PARAGRAPH of this string as the subject, so
1595
+ // a blank line inside it would silently push everything after the break into
1596
+ // the body — including the `(#N)` an applied request is read back by.
1597
+ // `mergeCommitSubject` already flattens the title it builds from; this is the
1598
+ // guard that keeps any other caller from reintroducing the break.
1599
+ if (/\n\s*\n/.test(commit.subject)) {
1600
+ throw new WorkflowValidationError('merge commit subject must be a single paragraph');
1601
+ }
1485
1602
  const MAX_ATTEMPTS = 3;
1486
1603
  return this.mutex.run(baseWorkspaceId, async () => {
1487
1604
  const cwd = await this.repoDir(baseWorkspaceId);
@@ -1561,13 +1678,29 @@ export class GitService implements IGitService {
1561
1678
  );
1562
1679
  }
1563
1680
 
1564
- // Nothing staged ⇒ base already contains source (empty CR). The base tip
1565
- // is the "merged" state; report it without an empty commit.
1681
+ // Nothing staged ⇒ the target already contains the source. The target tip
1682
+ // is the "merged" state; report it without an empty commit — but NOT as
1683
+ // this request's own merge commit. It is whatever landed on the target
1684
+ // last, usually another request's merge commit, and a reader that took it
1685
+ // for this request's would answer with that other request's files under
1686
+ // this number (cubic P1 on #347).
1687
+ //
1688
+ // There are two ways to arrive here, and they differ in exactly one
1689
+ // thing: whether this request has a merge commit on the target already.
1690
+ // An EMPTY request never had anything to land and has none. A request
1691
+ // whose previous attempt pushed its merge commit and then failed to
1692
+ // finalize the row (a transient database fault between the push and the
1693
+ // update) has one, and it is the only place its file list survives — so
1694
+ // it is looked for rather than assumed absent (cubic P2 on #347).
1566
1695
  const { stdout: staged } = await this.git(cwd, ['diff', '--cached', '--name-only']);
1567
1696
  if (staged.trim() === '') {
1568
1697
  const { stdout: sha } = await this.git(cwd, ['rev-parse', 'HEAD']);
1569
1698
  await this.git(cwd, ['merge', '--abort']).catch(() => undefined);
1570
- return { kind: 'merged' as const, sha: sha.trim() };
1699
+ const own =
1700
+ opts.appliedChangeNumber === undefined
1701
+ ? null
1702
+ : await this.ownMergeCommitOn(cwd, `origin/${targetBranch}`, opts.appliedChangeNumber, opts.appliedChangeTitle);
1703
+ return { kind: 'merged' as const, sha: sha.trim(), mergeCommit: own };
1571
1704
  }
1572
1705
 
1573
1706
  await this.git(cwd, [
@@ -1581,7 +1714,8 @@ export class GitService implements IGitService {
1581
1714
  try {
1582
1715
  await this.git(cwd, ['push', 'origin', `HEAD:refs/heads/${targetBranch}`]);
1583
1716
  this.accessControl?.invalidate(baseWorkspaceId);
1584
- return { kind: 'merged' as const, sha: sha.trim() };
1717
+ // A commit of this request's own, with the subject the reader verifies.
1718
+ return { kind: 'merged' as const, sha: sha.trim(), mergeCommit: sha.trim() };
1585
1719
  } catch (err) {
1586
1720
  const msg = err instanceof Error ? err.message : String(err);
1587
1721
  // Base moved under us — reset to the new tip and re-merge next loop.
@@ -1597,6 +1731,112 @@ export class GitService implements IGitService {
1597
1731
  });
1598
1732
  }
1599
1733
 
1734
+ /**
1735
+ * The merge commit change request `number` OWNS on `targetBranch`, or null when
1736
+ * the target carries none. Asks the published target only — no source branch,
1737
+ * no working tree, nothing merged, nothing written.
1738
+ *
1739
+ * This is the same question {@link mergeChangeRequest} answers for itself when
1740
+ * it finds nothing to merge, asked WITHOUT attempting a merge at all. The
1741
+ * merge path cannot be the only way to ask it: a request whose merge commit
1742
+ * was pushed by an attempt that then failed to record the row has an empty
1743
+ * diff against the target, so the approval gate refuses the retry ("no file
1744
+ * changes to approve") BEFORE any merge runs, and the recovery inside the
1745
+ * merge never gets its turn (cubic P2 on #347). The gate asks here instead.
1746
+ *
1747
+ * The target ref is refreshed first, because the attempt that pushed the
1748
+ * commit may have run in another process or another clone — and a fetch that
1749
+ * FAILS answers null without looking, which is the only safe answer: what the
1750
+ * caller does with a commit is record it as this request's published state, so
1751
+ * a decision taken on a stale tracking ref could finalize a request with a
1752
+ * commit the published branch no longer carries, and a clone that has no
1753
+ * tracking ref at all would turn the gate's clean refusal into a git error
1754
+ * (cubic P1 on #347). No authority, no answer; the caller then refuses exactly
1755
+ * as it did before this method existed.
1756
+ */
1757
+ async appliedMergeCommitOnTarget(
1758
+ baseWorkspaceId: string,
1759
+ targetBranch: string,
1760
+ number: number,
1761
+ title?: string,
1762
+ ): Promise<string | null> {
1763
+ assertValidBranchName(targetBranch);
1764
+ const cwd = await this.repoDir(baseWorkspaceId);
1765
+ // The fetch runs outside the workspace mutex, the way the implicit and
1766
+ // per-request fetches do (see `fetchLocks`, `fetchPrRefs`): it writes only
1767
+ // `refs/remotes/origin/*` and new objects, never HEAD, the index or the
1768
+ // working tree, so nothing it does needs serializing against a checkout or
1769
+ // a commit, and holding the mutex through a network round trip would park
1770
+ // every other git op behind the origin. (`mergeChangeRequest` fetches under
1771
+ // the mutex for a reason this method lacks: its fetch is one step of a
1772
+ // merge whose working tree must not move between the fetch and the push.)
1773
+ // The read that follows is a local one, and takes the mutex like
1774
+ // `appliedChangeShas` does.
1775
+ const refreshed = await this.git(cwd, [
1776
+ 'fetch', '--no-write-fetch-head', 'origin',
1777
+ `+refs/heads/${targetBranch}:refs/remotes/origin/${targetBranch}`,
1778
+ ]).then(() => true, () => false);
1779
+ if (!refreshed) {
1780
+ log.warn(
1781
+ `could not refresh "${targetBranch}" to look for change request #${number}'s merge commit, ` +
1782
+ 'so it is reported as owning none.',
1783
+ );
1784
+ return null;
1785
+ }
1786
+ return this.mutex.run(baseWorkspaceId, () => this.ownMergeCommitOn(cwd, `origin/${targetBranch}`, number, title));
1787
+ }
1788
+
1789
+ /**
1790
+ * The newest merge commit reachable from `targetRef` that is change request
1791
+ * `number`'s OWN, or null if the target carries none.
1792
+ *
1793
+ * Asked when a merge finds nothing to merge, to tell "this request never had
1794
+ * anything to land" apart from "a previous attempt already landed it". The
1795
+ * second happens when the push succeeded and the row update did not — the
1796
+ * merge commit is on the target, and if this attempt then records no sha the
1797
+ * request becomes permanently fileless, readable by its author alone, with the
1798
+ * commit that holds its files still sitting there unreferenced (cubic P2 on
1799
+ * #347).
1800
+ *
1801
+ * The two conditions are the reader's own (`appliedChangeShas`): a merge
1802
+ * commit, whose subject names this number. Deciding it here with the same
1803
+ * predicate means the writer records only what the reader will accept. The
1804
+ * walk is bounded by `--grep` (the number, as a fixed string), which is the
1805
+ * whole of the target's history for commits mentioning the number — and not
1806
+ * by a count: a fixed count of newer merges once hid a stuck request's commit
1807
+ * from the probe on a busy target, and a request that could no longer be
1808
+ * finalized is worse than a log walk. The message check then confirms a
1809
+ * body-only mention is not the request's own. Never on an ordinary merge:
1810
+ * the two callers are the empty-merge case here and
1811
+ * {@link appliedMergeCommitOnTarget}, which the approval gate asks before it
1812
+ * refuses a retry that has nothing left to merge.
1813
+ *
1814
+ * `title` lets the old message format be recognised — see
1815
+ * {@link mergeCommitMessageNames}.
1816
+ */
1817
+ private async ownMergeCommitOn(
1818
+ cwd: string,
1819
+ targetRef: string,
1820
+ number: number,
1821
+ title?: string,
1822
+ ): Promise<string | null> {
1823
+ // The whole message per commit. Records are NUL-separated (`-z`), the one
1824
+ // byte a commit message cannot hold, and the sha is the record's first
1825
+ // line: nothing a title may contain can split a record or a line early.
1826
+ const { stdout } = await this.git(cwd, [
1827
+ 'log', '-z', '--merges', '--fixed-strings', `--grep=(#${number})`,
1828
+ '--format=%H%n%B', targetRef,
1829
+ ]);
1830
+ for (const record of stdout.split('\0')) {
1831
+ const newline = record.indexOf('\n');
1832
+ const sha = (newline === -1 ? record : record.slice(0, newline)).trim();
1833
+ if (!/^[0-9a-f]{40,64}$/.test(sha)) continue;
1834
+ const message = newline === -1 ? '' : record.slice(newline + 1);
1835
+ if (mergeCommitMessageNames(message, number, title)) return sha;
1836
+ }
1837
+ return null;
1838
+ }
1839
+
1600
1840
  /** Conflicted paths from a half-done merge, parsed from porcelain status. */
1601
1841
  private async conflictedPaths(cwd: string): Promise<string[]> {
1602
1842
  const { stdout } = await this.git(cwd, ['status', '--porcelain=v1']);
@@ -1715,7 +1955,21 @@ export class GitService implements IGitService {
1715
1955
  */
1716
1956
  async remoteBranchExists(workspaceId: string, branch: string): Promise<boolean> {
1717
1957
  assertValidBranchName(branch);
1718
- 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> {
1719
1973
  try {
1720
1974
  await this.git(cwd, ['ls-remote', '--exit-code', '--heads', 'origin', `refs/heads/${branch}`]);
1721
1975
  return true;
@@ -1810,9 +2064,10 @@ export class GitService implements IGitService {
1810
2064
  // commit the clone holds but origin has not seen into cherry-picks of
1811
2065
  // the commits it merged — the merged branch stops being a parent, so
1812
2066
  // "does the published head contain the target's head" answers no
1813
- // however many times the update runs. Only the change-request update
1814
- // asks for it; every other pull replays plain saves, where the two
1815
- // 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.
1816
2071
  await this.git(cwd, [
1817
2072
  'rebase',
1818
2073
  '--autostash',
@@ -1929,7 +2184,12 @@ export class GitService implements IGitService {
1929
2184
  }
1930
2185
  throw err;
1931
2186
  }
1932
- 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 });
1933
2193
  this.accessControl?.invalidate(workspaceId);
1934
2194
  const after = await this.revParseOrNull(cwd, 'HEAD');
1935
2195
  const treeAfter = (await this.revParseOrNull(cwd, 'HEAD^{tree}')) ?? '';
@@ -1988,6 +2248,36 @@ export class GitService implements IGitService {
1988
2248
  return this.mutex.run(workspaceId, async () => this.hasUnpushedCommitsAt(await this.repoDir(workspaceId)));
1989
2249
  }
1990
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
+
1991
2281
  /**
1992
2282
  * `hasUnpushedCommits` without the workspace reservation — for a caller that
1993
2283
  * already holds one (`mergeChangeRequest`, which has to ask inside the same
@@ -2557,73 +2847,265 @@ export class GitService implements IGitService {
2557
2847
  return this.mutex.run(workspaceId, async () => {
2558
2848
  const baseRef = opts.at ? opts.at.baseSha : await this.resolvePublishedBranchRef(cwd, baseBranch);
2559
2849
  const headRef = opts.at ? opts.at.headSha : await this.resolvePublishedBranchRef(cwd, headBranch);
2560
- const range = `${baseRef}...${headRef}`; // three-dot = changes on head since merge-base
2850
+ // three-dot = changes on head since merge-base
2851
+ return this.prFilesForRange(cwd, `${baseRef}...${headRef}`, patchCap);
2852
+ });
2853
+ }
2561
2854
 
2562
- const [{ stdout: nameStatusOut }, { stdout: numstatOut }] = await Promise.all([
2563
- this.git(cwd, ['diff', '-M', '-z', '--name-status', range]),
2564
- this.git(cwd, ['diff', '-M', '-z', '--numstat', range]),
2565
- ]);
2566
- let statuses = parseNameStatusZ(nameStatusOut);
2567
- let counts = parseNumstatZ(numstatOut);
2568
- // roles.yaml can NEVER change through a merge — `preserveBaseRolesYaml`
2569
- // restores the base copy onto the source branch before every merge — so
2570
- // listing it as "changed" claims something the merge will not do, and
2571
- // (worse) makes its approval a requirement for a change that cannot
2572
- // land. Filter it from the review surface entirely; the neutralisation
2573
- // reads the raw refs itself and is unaffected. Both lists are filtered
2574
- // IN STEP so the index-zip below stays aligned.
2575
- // The empty-folder placeholder is filtered the same way: it is never
2576
- // content, so it is not a file to review or approve. It still merges
2577
- // with the rest, and `changedPathsForPr` keeps it, so a request that
2578
- // only creates a folder is not mistaken for an empty one and closed.
2579
- // An empty file deleted as its folder gets the (equally empty)
2580
- // placeholder reads to `-M` as a RENAME onto it; the real side of such
2581
- // a pair is first made the plain removal (or addition) it is, so
2582
- // dropping the placeholder never drops the file with it.
2583
- statuses = statuses.map(withoutPlaceholderRename);
2584
- if (statuses.some((s) => s.path === 'roles.yaml' || isFolderPlaceholder(s.path))) {
2585
- const keep = statuses.map((s) => s.path !== 'roles.yaml' && !isFolderPlaceholder(s.path));
2586
- statuses = statuses.filter((_, i) => keep[i]);
2587
- if (counts.length === keep.length) counts = counts.filter((_, i) => keep[i]);
2588
- }
2589
-
2590
- // `--name-status` and `--numstat` enumerate the same files in the same
2591
- // order (same `-M` over the same range), so we zip by index. If the two
2592
- // ever disagree in length, the index alignment is unsafe — fall back to
2593
- // zeroed counts (statuses/paths stay correct) rather than pin the wrong
2594
- // +/- to a file.
2595
- const aligned = counts.length === statuses.length;
2596
- if (!aligned) {
2597
- crLog.warn(
2598
- `diff name-status/numstat length mismatch (${statuses.length} vs ${counts.length}) ` +
2599
- `for ${range} — reporting file list without +/- counts`,
2600
- );
2601
- }
2855
+ /**
2856
+ * The changed-file list over one already-decided diff `range`, with the
2857
+ * statuses, the +/- counts and up to `patchCap` patches — everything
2858
+ * `changedFilesForPr` does once it knows which two commits it is comparing.
2859
+ *
2860
+ * Separated from the ref resolution because a change request has a second
2861
+ * diff that means exactly the same thing and resolves nothing: the one a
2862
+ * MERGE COMMIT holds, read against its first parent. Both must filter
2863
+ * `roles.yaml` and the folder placeholder the same way and zip the two
2864
+ * `--name-status` / `--numstat` lists by the same index, or a merged request
2865
+ * would answer a different file list from the one it answered while open.
2866
+ *
2867
+ * The caller holds the workspace reservation; this takes none.
2868
+ */
2869
+ private async prFilesForRange(
2870
+ cwd: string,
2871
+ range: string,
2872
+ patchCap: number,
2873
+ ): Promise<PullRequestFile[]> {
2874
+ const [{ stdout: nameStatusOut }, { stdout: numstatOut }] = await Promise.all([
2875
+ this.git(cwd, ['diff', '-M', '-z', '--name-status', range]),
2876
+ this.git(cwd, ['diff', '-M', '-z', '--numstat', range]),
2877
+ ]);
2878
+ let statuses = parseNameStatusZ(nameStatusOut);
2879
+ let counts = parseNumstatZ(numstatOut);
2880
+ // roles.yaml can NEVER change through a merge — `preserveBaseRolesYaml`
2881
+ // restores the base copy onto the source branch before every merge — so
2882
+ // listing it as "changed" claims something the merge will not do, and
2883
+ // (worse) makes its approval a requirement for a change that cannot
2884
+ // land. Filter it from the review surface entirely; the neutralisation
2885
+ // reads the raw refs itself and is unaffected. Both lists are filtered
2886
+ // IN STEP so the index-zip below stays aligned.
2887
+ // The empty-folder placeholder is filtered the same way: it is never
2888
+ // content, so it is not a file to review or approve. It still merges
2889
+ // with the rest, and `changedPathsForPr` keeps it, so a request that
2890
+ // only creates a folder is not mistaken for an empty one and closed.
2891
+ // An empty file deleted as its folder gets the (equally empty)
2892
+ // placeholder reads to `-M` as a RENAME onto it; the real side of such
2893
+ // a pair is first made the plain removal (or addition) it is, so
2894
+ // dropping the placeholder never drops the file with it.
2895
+ statuses = statuses.map(withoutPlaceholderRename);
2896
+ if (statuses.some((s) => s.path === 'roles.yaml' || isFolderPlaceholder(s.path))) {
2897
+ const keep = statuses.map((s) => s.path !== 'roles.yaml' && !isFolderPlaceholder(s.path));
2898
+ statuses = statuses.filter((_, i) => keep[i]);
2899
+ if (counts.length === keep.length) counts = counts.filter((_, i) => keep[i]);
2900
+ }
2602
2901
 
2603
- const files: PullRequestFile[] = statuses.map((s, i) => {
2604
- const c = (aligned ? counts[i] : undefined) ?? { additions: 0, deletions: 0, isBinary: false };
2605
- return {
2606
- path: s.path,
2607
- previousPath: s.previousPath,
2608
- status: s.status,
2609
- additions: c.additions,
2610
- deletions: c.deletions,
2611
- patch: undefined,
2612
- isBinary: c.isBinary,
2613
- sha: '',
2614
- rawUrl: '',
2615
- };
2616
- });
2902
+ // `--name-status` and `--numstat` enumerate the same files in the same
2903
+ // order (same `-M` over the same range), so we zip by index. If the two
2904
+ // ever disagree in length, the index alignment is unsafe — fall back to
2905
+ // zeroed counts (statuses/paths stay correct) rather than pin the wrong
2906
+ // +/- to a file.
2907
+ const aligned = counts.length === statuses.length;
2908
+ if (!aligned) {
2909
+ crLog.warn(
2910
+ `diff name-status/numstat length mismatch (${statuses.length} vs ${counts.length}) ` +
2911
+ `for ${range} — reporting file list without +/- counts`,
2912
+ );
2913
+ }
2617
2914
 
2618
- // Generate per-file patches for the first `patchCap` non-binary files.
2619
- let generated = 0;
2620
- for (const f of files) {
2621
- if (generated >= patchCap) break;
2622
- if (f.isBinary) continue;
2623
- f.patch = await this.filePatchForPr(cwd, range, f);
2624
- generated += 1;
2915
+ const files: PullRequestFile[] = statuses.map((s, i) => {
2916
+ const c = (aligned ? counts[i] : undefined) ?? { additions: 0, deletions: 0, isBinary: false };
2917
+ return {
2918
+ path: s.path,
2919
+ previousPath: s.previousPath,
2920
+ status: s.status,
2921
+ additions: c.additions,
2922
+ deletions: c.deletions,
2923
+ patch: undefined,
2924
+ isBinary: c.isBinary,
2925
+ sha: '',
2926
+ rawUrl: '',
2927
+ };
2928
+ });
2929
+
2930
+ // Generate per-file patches for the first `patchCap` non-binary files.
2931
+ let generated = 0;
2932
+ for (const f of files) {
2933
+ if (generated >= patchCap) break;
2934
+ if (f.isBinary) continue;
2935
+ f.patch = await this.filePatchForPr(cwd, range, f);
2936
+ generated += 1;
2937
+ }
2938
+ return files;
2939
+ }
2940
+
2941
+ /**
2942
+ * The FIRST PARENT of a commit, resolved — the other end of the diff one
2943
+ * commit introduced, and the check that this clone holds the commit at all.
2944
+ *
2945
+ * This is how an applied change request is read back. Its source branch is
2946
+ * retired, so there are no two branch refs left to diff; what is left is the
2947
+ * merge commit the row records, whose first parent is the target as it stood
2948
+ * before the merge and whose tree is the target with the change in it. The
2949
+ * diff between the two is precisely what the request applied — and it is
2950
+ * immutable, which the branch pair never was.
2951
+ *
2952
+ * First-parent and TWO dots, both deliberately. A three-dot diff would be
2953
+ * read from the merge base of the two parents, i.e. it would re-report the
2954
+ * source branch's whole history rather than what landed. `^1` is the target
2955
+ * side for a merge commit, and for a squashed or fast-forwarded commit it is
2956
+ * simply the commit before — the same answer either way.
2957
+ *
2958
+ * Throws `WorkflowValidationError` whenever the recorded commit cannot be
2959
+ * PROVEN to be this request's own merge commit — see
2960
+ * {@link appliedChangeShas}. Every caller reads that as "the file set could
2961
+ * not be resolved" and falls back to its own fail-closed answer, which is what
2962
+ * a clone that has not fetched the merge yet must get: no files, never
2963
+ * somebody else's.
2964
+ */
2965
+ private async appliedEnds(
2966
+ cwd: string,
2967
+ applied: AppliedChangeRef,
2968
+ ): Promise<{ baseSha: string; headSha: string }> {
2969
+ const { mergeSha, number } = applied;
2970
+ if (!/^[0-9a-f]{40,64}$/.test(mergeSha)) {
2971
+ throw new WorkflowValidationError(`invalid commit sha: ${mergeSha}`);
2972
+ }
2973
+ const revParse = async (rev: string): Promise<string | null> => {
2974
+ try {
2975
+ // One question, both answers: `--verify --quiet` prints what it resolved
2976
+ // and exits 1 if there is nothing to resolve.
2977
+ const { stdout } = await this.git(cwd, ['rev-parse', '--verify', '--quiet', rev]);
2978
+ return stdout.trim();
2979
+ } catch (err) {
2980
+ // Exit 1 under --quiet is git's own "no such object": this clone does
2981
+ // not have the commit, or the commit has no such parent. Anything else
2982
+ // (a deadline, a broken repository) is the caller's to see.
2983
+ if (err instanceof GitRunError && !err.timedOut && err.exitCode === 1) return null;
2984
+ throw err;
2625
2985
  }
2626
- return files;
2986
+ };
2987
+ // Two refusals, told apart for the caller's sake. A commit this clone does
2988
+ // NOT HOLD is a passing state — the next fetch may bring it — and is
2989
+ // reported as plain validation failure, to be asked again. A commit the
2990
+ // clone holds that is NOT THIS REQUEST'S merge commit (no second parent,
2991
+ // or a message that does not name the request) never becomes it, and is
2992
+ // reported as a mismatch the caller may remember.
2993
+ if (!(await revParse(`${mergeSha}^{commit}`))) {
2994
+ throw new WorkflowValidationError(
2995
+ `commit ${mergeSha} is not in this clone, so change request #${number} cannot be read from it yet`,
2996
+ );
2997
+ }
2998
+ // The SECOND parent first, because its absence is the whole P1: a merge
2999
+ // `--no-ff` always has one, and a commit that does not is not a merge this
3000
+ // request made.
3001
+ const headSha = await revParse(`${mergeSha}^2^{commit}`);
3002
+ if (!headSha) {
3003
+ throw new AppliedChangeMismatchError(
3004
+ `commit ${mergeSha} is not a merge commit, so it cannot be change request #${number}'s`,
3005
+ );
3006
+ }
3007
+ const baseSha = await revParse(`${mergeSha}^1^{commit}`);
3008
+ if (!baseSha) {
3009
+ throw new AppliedChangeMismatchError(`no first parent for commit ${mergeSha}`);
3010
+ }
3011
+ const { stdout: message } = await this.git(cwd, ['log', '-1', '--format=%B', mergeSha]);
3012
+ if (!mergeCommitMessageNames(message, number, applied.title)) {
3013
+ throw new AppliedChangeMismatchError(
3014
+ `commit ${mergeSha} is not the merge commit of change request #${number}`,
3015
+ );
3016
+ }
3017
+ return { baseSha, headSha };
3018
+ }
3019
+
3020
+ /**
3021
+ * The two commits an APPLIED change request spanned, recovered from the merge
3022
+ * commit its row records: the target as it stood before the merge (`^1`) and
3023
+ * the source tip that was merged (`^2`).
3024
+ *
3025
+ * These are what the detail of a merged request reports as its `baseSha` and
3026
+ * `headSha`, and the `headSha` matters beyond being informative: an approval
3027
+ * row stores the source head it was given against, and the detail calls an
3028
+ * approval stale when that sha is not the detail's `headSha`. Answering the
3029
+ * merge commit itself there would report every approval a merged request ever
3030
+ * collected as stale, and every file of it as unapproved — the opposite of
3031
+ * what happened.
3032
+ *
3033
+ * ## Why the commit is VERIFIED rather than taken
3034
+ *
3035
+ * `merged_sha` is not reliably a commit this request created. When the target
3036
+ * already contains the source there is nothing to merge, so
3037
+ * `mergeChangeRequest` writes no commit and reports the TARGET TIP as the
3038
+ * merged state — and in a deployment that lands everything through change
3039
+ * requests, that tip is usually ANOTHER request's merge commit. Reading "the
3040
+ * recorded commit's own change" then answers with somebody else's files under
3041
+ * this request's number: the request looks like it changed files it never
3042
+ * touched, and becomes visible to whoever may read THOSE (cubic P1 on #347).
3043
+ *
3044
+ * So three things must hold, and all three are cheap:
3045
+ *
3046
+ * 1. the commit is in this clone,
3047
+ * 2. it has a SECOND parent — `mergeChangeRequest` merges `--no-ff`, so
3048
+ * every merge commit it writes has one, and a commit that has none was
3049
+ * not written by a merge,
3050
+ * 3. its subject names THIS request — see `merge-commit.ts`.
3051
+ *
3052
+ * Anything else fails closed: no files, so author-only, rather than a file
3053
+ * list that belongs to another change. The write side no longer records a
3054
+ * no-op merge's sha at all, so for rows written from now on all three hold by
3055
+ * construction; the verification is what protects the rows written before it.
3056
+ */
3057
+ async appliedChangeShas(
3058
+ workspaceId: string,
3059
+ applied: AppliedChangeRef,
3060
+ ): Promise<{ baseSha: string; headSha: string }> {
3061
+ const cwd = await this.repoDir(workspaceId);
3062
+ return this.mutex.run(workspaceId, async () => this.appliedEnds(cwd, applied));
3063
+ }
3064
+
3065
+ /**
3066
+ * The changed-file list an APPLIED change request landed, read from its merge
3067
+ * commit against that commit's first parent, and shaped the way
3068
+ * `changedFilesForPr` shapes a branch pair — how an applied request is read
3069
+ * back, since its source branch is retired and the merge commit is what is
3070
+ * left of it.
3071
+ *
3072
+ * TWO dots, from the first parent. A three-dot diff would be read from the
3073
+ * merge base of the merge commit's two parents, i.e. it would re-report the
3074
+ * source branch's whole history instead of what landed on the target.
3075
+ *
3076
+ * Verified, not assumed — see {@link appliedChangeShas}. No fetch: the commit
3077
+ * is in this clone or this rejects.
3078
+ */
3079
+ async changedFilesOfAppliedChange(
3080
+ workspaceId: string,
3081
+ applied: AppliedChangeRef,
3082
+ opts: { patchCap?: number } = {},
3083
+ ): Promise<PullRequestFile[]> {
3084
+ const cwd = await this.repoDir(workspaceId);
3085
+ return this.mutex.run(workspaceId, async () => {
3086
+ const { baseSha } = await this.appliedEnds(cwd, applied);
3087
+ return this.prFilesForRange(cwd, `${baseSha}..${applied.mergeSha}`, opts.patchCap ?? 400);
3088
+ });
3089
+ }
3090
+
3091
+ /**
3092
+ * The same applied change as the two path views a change-request SUMMARY needs
3093
+ * — the flat touched-path list and the rename-aware pairs — out of one
3094
+ * `git diff`, exactly as {@link changedPathsAndPairsForPr} does for a branch
3095
+ * pair. Same verification and same no-fetch contract.
3096
+ */
3097
+ async changedPathsAndPairsOfAppliedChange(
3098
+ workspaceId: string,
3099
+ applied: AppliedChangeRef,
3100
+ ): Promise<{ paths: string[]; pairs: ChangedPathPair[] }> {
3101
+ const cwd = await this.repoDir(workspaceId);
3102
+ return this.mutex.run(workspaceId, async () => {
3103
+ const { baseSha } = await this.appliedEnds(cwd, applied);
3104
+ const entries = await this.prChangedEntriesForRange(
3105
+ cwd,
3106
+ `${baseSha}..${applied.mergeSha}`,
3107
+ );
3108
+ return { paths: flattenChangedPaths(entries, {}), pairs: changedPathPairs(entries) };
2627
3109
  });
2628
3110
  }
2629
3111
 
@@ -2702,32 +3184,70 @@ export class GitService implements IGitService {
2702
3184
  headRef: string,
2703
3185
  opts: { forAccessCheck?: boolean } = {},
2704
3186
  ): Promise<string[]> {
2705
- const { stdout } = await this.git(cwd, [
2706
- 'diff', '-M', '-z', '--name-status', `${baseRef}...${headRef}`,
2707
- ]);
2708
- // Deduped: both sides of a rename can collide with another entry's path.
2709
- return [
2710
- ...new Set(
2711
- parseNameStatusZ(stdout)
2712
- // A rename onto or off the placeholder is a real file removed or
2713
- // added (see `withoutPlaceholderRename`): both of its paths are
2714
- // touched, or filtering the placeholder would lose the file.
2715
- //
2716
- // Authorizing the change needs both sides of EVERY rename, not just
2717
- // the placeholder's: git reports a rename under its new name alone,
2718
- // and the old name is a path the change deletes.
2719
- .flatMap((s) =>
2720
- s.previousPath &&
2721
- (opts.forAccessCheck || isFolderPlaceholder(s.path) || isFolderPlaceholder(s.previousPath))
2722
- ? [s.previousPath, s.path]
2723
- : [s.path],
2724
- )
2725
- // Same rule as `changedFilesForPr`: a roles.yaml change never
2726
- // survives a merge, so it is not a touched path for routing or
2727
- // summaries either.
2728
- .filter((p) => opts.forAccessCheck || p !== 'roles.yaml'),
2729
- ),
2730
- ];
3187
+ return flattenChangedPaths(await this.prChangedEntriesAt(cwd, baseRef, headRef), opts);
3188
+ }
3189
+
3190
+ /**
3191
+ * The same diff, left as parsed ENTRIES: each changed file with the path it
3192
+ * came from when git detected a rename.
3193
+ *
3194
+ * The flat path list cannot express that pairing, and a caller deciding what
3195
+ * someone may READ needs it. A file renamed out of a folder the caller cannot
3196
+ * open is readable under neither of its names — the diff of a rename shows
3197
+ * the old side's content — and a flat list that offers both paths unlabelled
3198
+ * cannot say which new file the closed old path belongs to. `forAccessCheck`
3199
+ * solves the write side of this by taking the union; a read gate needs the
3200
+ * pairing itself.
3201
+ */
3202
+ private async prChangedEntriesAt(
3203
+ cwd: string,
3204
+ baseRef: string,
3205
+ headRef: string,
3206
+ ): Promise<NameStatusEntry[]> {
3207
+ return this.prChangedEntriesForRange(cwd, `${baseRef}...${headRef}`);
3208
+ }
3209
+
3210
+ /**
3211
+ * The same parsed entries over an already-decided `range`, so a merge
3212
+ * commit's own diff ({@link firstParentRange}) is read by the same parser as a
3213
+ * change request's branch pair.
3214
+ */
3215
+ private async prChangedEntriesForRange(
3216
+ cwd: string,
3217
+ range: string,
3218
+ ): Promise<NameStatusEntry[]> {
3219
+ const { stdout } = await this.git(cwd, ['diff', '-M', '-z', '--name-status', range]);
3220
+ return parseNameStatusZ(stdout);
3221
+ }
3222
+
3223
+ /**
3224
+ * One diff, both views of it: the flat touched-path list every existing
3225
+ * caller reads, and the rename-aware pairs a read gate needs. The change-
3226
+ * request summary builder wants both, and must not pay for two `git diff`s
3227
+ * to get them — it is on the list path that is polled every 60s.
3228
+ */
3229
+ async changedPathsAndPairsForPr(
3230
+ workspaceId: string,
3231
+ baseBranch: string,
3232
+ headBranch: string,
3233
+ opts: { fetch?: boolean } = {},
3234
+ ): Promise<{ paths: string[]; pairs: ChangedPathPair[] }> {
3235
+ assertValidBranchName(baseBranch);
3236
+ assertValidBranchName(headBranch);
3237
+ const cwd = await this.repoDir(workspaceId);
3238
+ // Same ref pinning and the same fetch skip as `changedPathsForPr`, for the
3239
+ // same reasons spelled out there.
3240
+ const pinned =
3241
+ opts.fetch === false ? await this.publishedPrCommits(cwd, baseBranch, headBranch) : null;
3242
+ if (!pinned) {
3243
+ await this.fetchPrRefs(cwd, baseBranch, headBranch);
3244
+ }
3245
+ return this.mutex.run(workspaceId, async () => {
3246
+ const baseRef = pinned ? pinned.base : await this.resolvePublishedBranchRef(cwd, baseBranch);
3247
+ const headRef = pinned ? pinned.head : await this.resolvePublishedBranchRef(cwd, headBranch);
3248
+ const entries = await this.prChangedEntriesAt(cwd, baseRef, headRef);
3249
+ return { paths: flattenChangedPaths(entries, {}), pairs: changedPathPairs(entries) };
3250
+ });
2731
3251
  }
2732
3252
 
2733
3253
  /**
@@ -3616,6 +4136,67 @@ interface NameStatusEntry {
3616
4136
  * Parse `git diff -M -z --name-status` output. NUL-separated tokens: each record
3617
4137
  * is `<statusLetter>\0<path>`, or for a rename/copy `<Rxxx|Cxxx>\0<old>\0<new>`.
3618
4138
  */
4139
+ /**
4140
+ * The flat touched-path list of a parsed `--name-status` diff. Lifted out of
4141
+ * `prChangedPathsAt` unchanged, so the pair-returning reader can share one
4142
+ * diff with it rather than running a second.
4143
+ */
4144
+ function flattenChangedPaths(
4145
+ entries: NameStatusEntry[],
4146
+ opts: { forAccessCheck?: boolean } = {},
4147
+ ): string[] {
4148
+ // Deduped: both sides of a rename can collide with another entry's path.
4149
+ return [
4150
+ ...new Set(
4151
+ entries
4152
+ // A rename onto or off the placeholder is a real file removed or
4153
+ // added (see `withoutPlaceholderRename`): both of its paths are
4154
+ // touched, or filtering the placeholder would lose the file.
4155
+ //
4156
+ // Authorizing the change needs both sides of EVERY rename, not just
4157
+ // the placeholder's: git reports a rename under its new name alone,
4158
+ // and the old name is a path the change deletes.
4159
+ .flatMap((s) =>
4160
+ s.previousPath &&
4161
+ (opts.forAccessCheck || isFolderPlaceholder(s.path) || isFolderPlaceholder(s.previousPath))
4162
+ ? [s.previousPath, s.path]
4163
+ : [s.path],
4164
+ )
4165
+ // Same rule as `changedFilesForPr`: a roles.yaml change never
4166
+ // survives a merge, so it is not a touched path for routing or
4167
+ // summaries either.
4168
+ .filter((p) => opts.forAccessCheck || p !== 'roles.yaml'),
4169
+ ),
4170
+ ];
4171
+ }
4172
+
4173
+ /**
4174
+ * The changed files of a diff as read-gate pairs: every file the request
4175
+ * proposes, carrying the path it was renamed from when it has one.
4176
+ *
4177
+ * Normalised and filtered EXACTLY as `changedFilesForPr` does it, through the
4178
+ * same `withoutPlaceholderRename` and the same two drops — because this is the
4179
+ * file set a change-request LIST decides visibility over and that one is the
4180
+ * file set its DETAIL decides over, and the two answering differently is how a
4181
+ * list comes to advertise a request its own detail refuses (or to hide one the
4182
+ * detail serves). Any future change to one filter belongs in both.
4183
+ */
4184
+ function changedPathPairs(entries: NameStatusEntry[]): ChangedPathPair[] {
4185
+ const byPath = new Map<string, ChangedPathPair>();
4186
+ for (const raw of entries) {
4187
+ // A rename between a real file and the placeholder is a removal or an
4188
+ // addition of the real side; after this, the placeholder filter below
4189
+ // cannot take the file with it.
4190
+ const entry = withoutPlaceholderRename(raw);
4191
+ if (isFolderPlaceholder(entry.path) || entry.path === 'roles.yaml') continue;
4192
+ byPath.set(entry.path, {
4193
+ path: entry.path,
4194
+ ...(entry.previousPath ? { previousPath: entry.previousPath } : {}),
4195
+ });
4196
+ }
4197
+ return [...byPath.values()];
4198
+ }
4199
+
3619
4200
  export function parseNameStatusZ(out: string): NameStatusEntry[] {
3620
4201
  const tokens = out.split('\0');
3621
4202
  const entries: NameStatusEntry[] = [];