agent-tower 0.5.3 → 0.5.4-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (312) hide show
  1. package/README.md +6 -1
  2. package/dist/app.d.ts.map +1 -1
  3. package/dist/app.js +15 -1
  4. package/dist/app.js.map +1 -1
  5. package/dist/app.test.js +12 -0
  6. package/dist/app.test.js.map +1 -1
  7. package/dist/cli.js +18 -1
  8. package/dist/cli.js.map +1 -1
  9. package/dist/core/event-bus.d.ts +3 -0
  10. package/dist/core/event-bus.d.ts.map +1 -1
  11. package/dist/core/event-bus.js.map +1 -1
  12. package/dist/executors/__tests__/base.executor.test.js +29 -1
  13. package/dist/executors/__tests__/base.executor.test.js.map +1 -1
  14. package/dist/executors/__tests__/codex.executor.test.js +13 -0
  15. package/dist/executors/__tests__/codex.executor.test.js.map +1 -1
  16. package/dist/executors/base.executor.d.ts.map +1 -1
  17. package/dist/executors/base.executor.js +4 -0
  18. package/dist/executors/base.executor.js.map +1 -1
  19. package/dist/executors/execution-env.d.ts +1 -1
  20. package/dist/executors/execution-env.d.ts.map +1 -1
  21. package/dist/executors/execution-env.js +7 -0
  22. package/dist/executors/execution-env.js.map +1 -1
  23. package/dist/index.js +2 -0
  24. package/dist/index.js.map +1 -1
  25. package/dist/mcp/http-client.d.ts +33 -1
  26. package/dist/mcp/http-client.d.ts.map +1 -1
  27. package/dist/mcp/http-client.js +21 -2
  28. package/dist/mcp/http-client.js.map +1 -1
  29. package/dist/mcp/index.js +4 -1
  30. package/dist/mcp/index.js.map +1 -1
  31. package/dist/mcp/server.d.ts +3 -1
  32. package/dist/mcp/server.d.ts.map +1 -1
  33. package/dist/mcp/server.js +64 -9
  34. package/dist/mcp/server.js.map +1 -1
  35. package/dist/mcp/tools/__tests__/workspaces.test.d.ts +2 -0
  36. package/dist/mcp/tools/__tests__/workspaces.test.d.ts.map +1 -0
  37. package/dist/mcp/tools/__tests__/workspaces.test.js +72 -0
  38. package/dist/mcp/tools/__tests__/workspaces.test.js.map +1 -0
  39. package/dist/mcp/tools/workspaces.d.ts +9 -1
  40. package/dist/mcp/tools/workspaces.d.ts.map +1 -1
  41. package/dist/mcp/tools/workspaces.js +96 -2
  42. package/dist/mcp/tools/workspaces.js.map +1 -1
  43. package/dist/mcp/types.d.ts +46 -0
  44. package/dist/mcp/types.d.ts.map +1 -1
  45. package/dist/mcp/types.js +23 -0
  46. package/dist/mcp/types.js.map +1 -1
  47. package/dist/middleware/__tests__/access-auth.test.d.ts +2 -0
  48. package/dist/middleware/__tests__/access-auth.test.d.ts.map +1 -0
  49. package/dist/middleware/__tests__/access-auth.test.js +249 -0
  50. package/dist/middleware/__tests__/access-auth.test.js.map +1 -0
  51. package/dist/middleware/access-auth.d.ts +3 -0
  52. package/dist/middleware/access-auth.d.ts.map +1 -0
  53. package/dist/middleware/access-auth.js +110 -0
  54. package/dist/middleware/access-auth.js.map +1 -0
  55. package/dist/routes/__tests__/files.test.d.ts +2 -0
  56. package/dist/routes/__tests__/files.test.d.ts.map +1 -0
  57. package/dist/routes/__tests__/files.test.js +101 -0
  58. package/dist/routes/__tests__/files.test.js.map +1 -0
  59. package/dist/routes/__tests__/previews.integration.test.d.ts +2 -0
  60. package/dist/routes/__tests__/previews.integration.test.d.ts.map +1 -0
  61. package/dist/routes/__tests__/previews.integration.test.js +269 -0
  62. package/dist/routes/__tests__/previews.integration.test.js.map +1 -0
  63. package/dist/routes/__tests__/previews.test.js +103 -1
  64. package/dist/routes/__tests__/previews.test.js.map +1 -1
  65. package/dist/routes/__tests__/team-runs-merge.test.d.ts +2 -0
  66. package/dist/routes/__tests__/team-runs-merge.test.d.ts.map +1 -0
  67. package/dist/routes/__tests__/team-runs-merge.test.js +156 -0
  68. package/dist/routes/__tests__/team-runs-merge.test.js.map +1 -0
  69. package/dist/routes/__tests__/team-runs-private-messages.test.d.ts +2 -0
  70. package/dist/routes/__tests__/team-runs-private-messages.test.d.ts.map +1 -0
  71. package/dist/routes/__tests__/team-runs-private-messages.test.js +82 -0
  72. package/dist/routes/__tests__/team-runs-private-messages.test.js.map +1 -0
  73. package/dist/routes/access-auth.d.ts +3 -0
  74. package/dist/routes/access-auth.d.ts.map +1 -0
  75. package/dist/routes/access-auth.js +89 -0
  76. package/dist/routes/access-auth.js.map +1 -0
  77. package/dist/routes/files.d.ts.map +1 -1
  78. package/dist/routes/files.js +46 -0
  79. package/dist/routes/files.js.map +1 -1
  80. package/dist/routes/index.d.ts.map +1 -1
  81. package/dist/routes/index.js +3 -0
  82. package/dist/routes/index.js.map +1 -1
  83. package/dist/routes/previews.d.ts +7 -2
  84. package/dist/routes/previews.d.ts.map +1 -1
  85. package/dist/routes/previews.js +106 -32
  86. package/dist/routes/previews.js.map +1 -1
  87. package/dist/routes/projects.d.ts.map +1 -1
  88. package/dist/routes/projects.js +9 -0
  89. package/dist/routes/projects.js.map +1 -1
  90. package/dist/routes/team-runs.d.ts +2 -0
  91. package/dist/routes/team-runs.d.ts.map +1 -1
  92. package/dist/routes/team-runs.js +48 -1
  93. package/dist/routes/team-runs.js.map +1 -1
  94. package/dist/routes/workspaces.d.ts.map +1 -1
  95. package/dist/routes/workspaces.js +43 -4
  96. package/dist/routes/workspaces.js.map +1 -1
  97. package/dist/services/__tests__/access-auth.service.test.d.ts +2 -0
  98. package/dist/services/__tests__/access-auth.service.test.d.ts.map +1 -0
  99. package/dist/services/__tests__/access-auth.service.test.js +316 -0
  100. package/dist/services/__tests__/access-auth.service.test.js.map +1 -0
  101. package/dist/services/__tests__/member-heartbeat.test.d.ts +2 -0
  102. package/dist/services/__tests__/member-heartbeat.test.d.ts.map +1 -0
  103. package/dist/services/__tests__/member-heartbeat.test.js +344 -0
  104. package/dist/services/__tests__/member-heartbeat.test.js.map +1 -0
  105. package/dist/services/__tests__/project.service.test.js +29 -0
  106. package/dist/services/__tests__/project.service.test.js.map +1 -1
  107. package/dist/services/__tests__/session-manager.team-run.test.js +144 -0
  108. package/dist/services/__tests__/session-manager.team-run.test.js.map +1 -1
  109. package/dist/services/__tests__/task.service.test.js +34 -0
  110. package/dist/services/__tests__/task.service.test.js.map +1 -1
  111. package/dist/services/__tests__/team-reconciler.service.test.js +307 -11
  112. package/dist/services/__tests__/team-reconciler.service.test.js.map +1 -1
  113. package/dist/services/__tests__/team-run.service.test.js +368 -2
  114. package/dist/services/__tests__/team-run.service.test.js.map +1 -1
  115. package/dist/services/__tests__/team-scheduler.service.test.js +409 -5
  116. package/dist/services/__tests__/team-scheduler.service.test.js.map +1 -1
  117. package/dist/services/__tests__/workspace.service.test.js +991 -3
  118. package/dist/services/__tests__/workspace.service.test.js.map +1 -1
  119. package/dist/services/access-auth.service.d.ts +82 -0
  120. package/dist/services/access-auth.service.d.ts.map +1 -0
  121. package/dist/services/access-auth.service.js +385 -0
  122. package/dist/services/access-auth.service.js.map +1 -0
  123. package/dist/services/mcp-config.service.d.ts.map +1 -1
  124. package/dist/services/mcp-config.service.js +8 -2
  125. package/dist/services/mcp-config.service.js.map +1 -1
  126. package/dist/services/mcp-config.service.test.js +18 -3
  127. package/dist/services/mcp-config.service.test.js.map +1 -1
  128. package/dist/services/member-heartbeat-scheduler.d.ts +33 -0
  129. package/dist/services/member-heartbeat-scheduler.d.ts.map +1 -0
  130. package/dist/services/member-heartbeat-scheduler.js +83 -0
  131. package/dist/services/member-heartbeat-scheduler.js.map +1 -0
  132. package/dist/services/project-guards.d.ts +5 -0
  133. package/dist/services/project-guards.d.ts.map +1 -1
  134. package/dist/services/project-guards.js +47 -0
  135. package/dist/services/project-guards.js.map +1 -1
  136. package/dist/services/project.service.d.ts +6 -4
  137. package/dist/services/project.service.d.ts.map +1 -1
  138. package/dist/services/project.service.js +16 -7
  139. package/dist/services/project.service.js.map +1 -1
  140. package/dist/services/session-manager.d.ts +11 -0
  141. package/dist/services/session-manager.d.ts.map +1 -1
  142. package/dist/services/session-manager.js +76 -0
  143. package/dist/services/session-manager.js.map +1 -1
  144. package/dist/services/task.service.d.ts +11 -9
  145. package/dist/services/task.service.d.ts.map +1 -1
  146. package/dist/services/task.service.js +17 -12
  147. package/dist/services/task.service.js.map +1 -1
  148. package/dist/services/team-reconciler.service.d.ts +39 -0
  149. package/dist/services/team-reconciler.service.d.ts.map +1 -1
  150. package/dist/services/team-reconciler.service.js +234 -3
  151. package/dist/services/team-reconciler.service.js.map +1 -1
  152. package/dist/services/team-run.service.d.ts +7 -1
  153. package/dist/services/team-run.service.d.ts.map +1 -1
  154. package/dist/services/team-run.service.js +98 -9
  155. package/dist/services/team-run.service.js.map +1 -1
  156. package/dist/services/team-scheduler.service.d.ts +21 -1
  157. package/dist/services/team-scheduler.service.d.ts.map +1 -1
  158. package/dist/services/team-scheduler.service.js +252 -15
  159. package/dist/services/team-scheduler.service.js.map +1 -1
  160. package/dist/services/terminal-manager.d.ts.map +1 -1
  161. package/dist/services/terminal-manager.js +1 -0
  162. package/dist/services/terminal-manager.js.map +1 -1
  163. package/dist/services/workspace.service.d.ts +56 -1
  164. package/dist/services/workspace.service.d.ts.map +1 -1
  165. package/dist/services/workspace.service.js +841 -8
  166. package/dist/services/workspace.service.js.map +1 -1
  167. package/dist/socket/__tests__/socket-gateway.test.d.ts +2 -0
  168. package/dist/socket/__tests__/socket-gateway.test.d.ts.map +1 -0
  169. package/dist/socket/__tests__/socket-gateway.test.js +119 -0
  170. package/dist/socket/__tests__/socket-gateway.test.js.map +1 -0
  171. package/dist/socket/middleware/__tests__/auth.test.js +102 -2
  172. package/dist/socket/middleware/__tests__/auth.test.js.map +1 -1
  173. package/dist/socket/middleware/auth.d.ts +1 -0
  174. package/dist/socket/middleware/auth.d.ts.map +1 -1
  175. package/dist/socket/middleware/auth.js +22 -2
  176. package/dist/socket/middleware/auth.js.map +1 -1
  177. package/dist/socket/socket-gateway.d.ts +6 -2
  178. package/dist/socket/socket-gateway.d.ts.map +1 -1
  179. package/dist/socket/socket-gateway.js +78 -15
  180. package/dist/socket/socket-gateway.js.map +1 -1
  181. package/dist/utils/internal-api-token.d.ts +9 -0
  182. package/dist/utils/internal-api-token.d.ts.map +1 -0
  183. package/dist/utils/internal-api-token.js +74 -0
  184. package/dist/utils/internal-api-token.js.map +1 -0
  185. package/dist/utils/preview-path.d.ts +11 -0
  186. package/dist/utils/preview-path.d.ts.map +1 -0
  187. package/dist/utils/preview-path.js +46 -0
  188. package/dist/utils/preview-path.js.map +1 -0
  189. package/dist/utils/process-launch.test.js +14 -0
  190. package/dist/utils/process-launch.test.js.map +1 -1
  191. package/dist/web/assets/{AgentDemoPage-DMLw4xpg.js → AgentDemoPage-UmBBXiZq.js} +1 -1
  192. package/dist/web/assets/{AgentEnvironmentSettingsPage-NgRdouLu.js → AgentEnvironmentSettingsPage-DeMVC_O2.js} +1 -1
  193. package/dist/web/assets/{AgentLogo-ThhvSi7G.js → AgentLogo-sxp-yGLB.js} +1 -1
  194. package/dist/web/assets/{ConversationPage-B1ZcyF_h.js → ConversationPage-ykUuKcMG.js} +2 -2
  195. package/dist/web/assets/CreateTaskInput-WmNhRnVp.js +1 -0
  196. package/dist/web/assets/{DemoPage-CN2OBKzu.js → DemoPage-Cx-F46PV.js} +3 -3
  197. package/dist/web/assets/GeneralSettingsPage-DM4iy6dp.js +1 -0
  198. package/dist/web/assets/{Icons-DIZBOtuC.js → Icons-CO02_BMI.js} +1 -1
  199. package/dist/web/assets/{LoadingPreviewPage-CRBcypeY.js → LoadingPreviewPage-C58uR8H7.js} +1 -1
  200. package/dist/web/assets/McpSettingsPage-CTe-SNfY.js +1 -0
  201. package/dist/web/assets/{MemberAvatar-Y_pJlw-N.js → MemberAvatar-CZegYpYl.js} +1 -1
  202. package/dist/web/assets/NotificationSettingsPage-D17SAmMK.js +1 -0
  203. package/dist/web/assets/ProfileSettingsPage-B2rHV2eU.js +3 -0
  204. package/dist/web/assets/{ProjectKanbanPage-CBfocvKp.js → ProjectKanbanPage-SpCqpcZ0.js} +31 -32
  205. package/dist/web/assets/ProjectSettingsPage-2dD_ahm-.js +2 -0
  206. package/dist/web/assets/{ProviderSettingsPage-D-1Fyh5B.js → ProviderSettingsPage-D9vu3rO0.js} +6 -6
  207. package/dist/web/assets/{SettingsMasterDetail-CZfVaFo2.js → SettingsMasterDetail-CwRB1WBt.js} +1 -1
  208. package/dist/web/assets/{TeamSettingsPage-DysTmOGE.js → TeamSettingsPage-QXwc1AJE.js} +1 -1
  209. package/dist/web/assets/{arc-CgLBAHTH.js → arc-BRCC5BeU.js} +1 -1
  210. package/dist/web/assets/{architectureDiagram-3BPJPVTR-CP9eQfdk.js → architectureDiagram-3BPJPVTR-7lTGsSu5.js} +1 -1
  211. package/dist/web/assets/{arrow-left-y1LwO7sB.js → arrow-left-CNgpy5r6.js} +1 -1
  212. package/dist/web/assets/{blockDiagram-GPEHLZMM-BURZnUwn.js → blockDiagram-GPEHLZMM-CnscyQkX.js} +1 -1
  213. package/dist/web/assets/{c4Diagram-AAUBKEIU-Dzgp3z3S.js → c4Diagram-AAUBKEIU-DDhuPTLZ.js} +1 -1
  214. package/dist/web/assets/channel-S-3oxkZZ.js +1 -0
  215. package/dist/web/assets/check-DRdZLPss.js +1 -0
  216. package/dist/web/assets/{chevron-down--B7iZqBL.js → chevron-down-CqZBsPeR.js} +1 -1
  217. package/dist/web/assets/{chevron-right-7PShDIwg.js → chevron-right-YWciNkox.js} +1 -1
  218. package/dist/web/assets/{chevron-up-3n_vHdrD.js → chevron-up-BJ4i5iAn.js} +1 -1
  219. package/dist/web/assets/{chunk-2J33WTMH-CIpasa01.js → chunk-2J33WTMH-B96XJhZG.js} +1 -1
  220. package/dist/web/assets/{chunk-4BX2VUAB-DeSe2m1y.js → chunk-4BX2VUAB-IUZUHtbw.js} +1 -1
  221. package/dist/web/assets/{chunk-55IACEB6-BkJ6ogdn.js → chunk-55IACEB6-DcO4xSOO.js} +1 -1
  222. package/dist/web/assets/{chunk-727SXJPM-Bkbv-knP.js → chunk-727SXJPM-DuQwKkk-.js} +1 -1
  223. package/dist/web/assets/{chunk-AQP2D5EJ-BG9wGFIo.js → chunk-AQP2D5EJ-QotCqjAo.js} +1 -1
  224. package/dist/web/assets/{chunk-FMBD7UC4-Cvl_2za9.js → chunk-FMBD7UC4-C7hUbfZh.js} +1 -1
  225. package/dist/web/assets/{chunk-ND2GUHAM-B_zdzkWX.js → chunk-ND2GUHAM-B_C5eChQ.js} +1 -1
  226. package/dist/web/assets/{chunk-QZHKN3VN-Ch6dvKNw.js → chunk-QZHKN3VN-DRgZY4LJ.js} +1 -1
  227. package/dist/web/assets/{circle-alert-W13JkYj3.js → circle-alert-Ch4pQ3zT.js} +1 -1
  228. package/dist/web/assets/classDiagram-4FO5ZUOK-C00aPgpD.js +1 -0
  229. package/dist/web/assets/classDiagram-v2-Q7XG4LA2-C00aPgpD.js +1 -0
  230. package/dist/web/assets/{code-block-OCS4YCEC-CYhJ9OPu.js → code-block-OCS4YCEC-h5zD4EvJ.js} +1 -1
  231. package/dist/web/assets/confirm-dialog-DG0L1v-9.js +1 -0
  232. package/dist/web/assets/{copy-bJ2sut_L.js → copy-JHS-5tjr.js} +1 -1
  233. package/dist/web/assets/{cose-bilkent-S5V4N54A-DWlnxyfH.js → cose-bilkent-S5V4N54A-6Gv8U8Cj.js} +1 -1
  234. package/dist/web/assets/{dagre-BM42HDAG-Bt3QI02a.js → dagre-BM42HDAG-Cf4rsmyS.js} +1 -1
  235. package/dist/web/assets/{diagram-2AECGRRQ-EaJBYSnS.js → diagram-2AECGRRQ-DW3DQ8NA.js} +1 -1
  236. package/dist/web/assets/{diagram-5GNKFQAL-BGyRb-_h.js → diagram-5GNKFQAL-C5cQ_MTX.js} +1 -1
  237. package/dist/web/assets/{diagram-KO2AKTUF-CZhDTMfm.js → diagram-KO2AKTUF-Dcs4NTfn.js} +1 -1
  238. package/dist/web/assets/{diagram-LMA3HP47-Be8BAxmL.js → diagram-LMA3HP47-lLI-OolH.js} +1 -1
  239. package/dist/web/assets/{diagram-OG6HWLK6-DEdQ7Cml.js → diagram-OG6HWLK6-BTdMG1u6.js} +1 -1
  240. package/dist/web/assets/{erDiagram-TEJ5UH35-CG9SNlbz.js → erDiagram-TEJ5UH35-BrKfSMWb.js} +1 -1
  241. package/dist/web/assets/{flowDiagram-I6XJVG4X-BUpx5PVX.js → flowDiagram-I6XJVG4X-BFT9EHqo.js} +1 -1
  242. package/dist/web/assets/folder-picker--V79noua.js +1 -0
  243. package/dist/web/assets/{ganttDiagram-6RSMTGT7-D7vRxMpE.js → ganttDiagram-6RSMTGT7-D_44EJ_y.js} +1 -1
  244. package/dist/web/assets/{gitGraphDiagram-PVQCEYII-3lhH-RcD.js → gitGraphDiagram-PVQCEYII-COrCr27e.js} +1 -1
  245. package/dist/web/assets/index-BC5stA77.js +122 -0
  246. package/dist/web/assets/{index-CGMjRoF7.js → index-Cjaho2ST.js} +4 -4
  247. package/dist/web/assets/index-DRJLGAPR.css +1 -0
  248. package/dist/web/assets/{infoDiagram-5YYISTIA-C5SNbJpO.js → infoDiagram-5YYISTIA-Derwhgw-.js} +1 -1
  249. package/dist/web/assets/{ishikawaDiagram-YF4QCWOH-Ca7tX9_0.js → ishikawaDiagram-YF4QCWOH-Dv_SKfKy.js} +1 -1
  250. package/dist/web/assets/{journeyDiagram-JHISSGLW-B2oo6nxY.js → journeyDiagram-JHISSGLW-BvL0Myto.js} +1 -1
  251. package/dist/web/assets/{kanban-definition-UN3LZRKU-D1CzIzX9.js → kanban-definition-UN3LZRKU-D9iCCF7g.js} +1 -1
  252. package/dist/web/assets/{layers-BbLzQqyy.js → layers-D-OLv40U.js} +1 -1
  253. package/dist/web/assets/{linear-BWowtcGw.js → linear-DLebIyPZ.js} +1 -1
  254. package/dist/web/assets/{mermaid-NOHMQCX5-B42ziicV.js → mermaid-NOHMQCX5-CyA4Kmm7.js} +40 -40
  255. package/dist/web/assets/{message-square-Bd_m34OX.js → message-square-ByVmnBN0.js} +1 -1
  256. package/dist/web/assets/{mindmap-definition-RKZ34NQL-CYbO6QCk.js → mindmap-definition-RKZ34NQL-CJXkujXD.js} +1 -1
  257. package/dist/web/assets/{modal-CxOKWOjZ.js → modal-KKPLseLk.js} +1 -1
  258. package/dist/web/assets/{pencil-h5kxwbcu.js → pencil-B7ylaB7k.js} +1 -1
  259. package/dist/web/assets/{pieDiagram-4H26LBE5-DXQghKRy.js → pieDiagram-4H26LBE5-C8DfXjYu.js} +1 -1
  260. package/dist/web/assets/{quadrantDiagram-W4KKPZXB-C6lAMMQB.js → quadrantDiagram-W4KKPZXB-BxaUhA-J.js} +1 -1
  261. package/dist/web/assets/{requirementDiagram-4Y6WPE33-Di9Le7Wy.js → requirementDiagram-4Y6WPE33-CkP381y3.js} +1 -1
  262. package/dist/web/assets/{sankeyDiagram-5OEKKPKP-BGpnptvR.js → sankeyDiagram-5OEKKPKP-Ciot-1f1.js} +1 -1
  263. package/dist/web/assets/{select-DFuN9DX8.js → select-CLvHC1t4.js} +1 -1
  264. package/dist/web/assets/{sequenceDiagram-3UESZ5HK-DjfkfaQL.js → sequenceDiagram-3UESZ5HK-ZrO6aulq.js} +1 -1
  265. package/dist/web/assets/{stateDiagram-AJRCARHV-DsELotm9.js → stateDiagram-AJRCARHV-BDC3J_xT.js} +1 -1
  266. package/dist/web/assets/stateDiagram-v2-BHNVJYJU-C_4zupNH.js +1 -0
  267. package/dist/web/assets/{switch-gL7lBFj6.js → switch-DqXAu8pW.js} +1 -1
  268. package/dist/web/assets/{textarea-Lr4GZaYW.js → textarea-ByV2n54w.js} +1 -1
  269. package/dist/web/assets/{timeline-definition-PNZ67QCA-Cq1BWC0T.js → timeline-definition-PNZ67QCA-CG898aCW.js} +1 -1
  270. package/dist/web/assets/{trash-2-B7GFt5Wl.js → trash-2-D-MBxpK4.js} +1 -1
  271. package/dist/web/assets/{upload-L8hpDFoO.js → upload-CkkorsYk.js} +1 -1
  272. package/dist/web/assets/{use-profiles-zIsoigD8.js → use-profiles-B62R9Kbn.js} +1 -1
  273. package/dist/web/assets/use-projects-JMhtZFd6.js +1 -0
  274. package/dist/web/assets/{use-providers-erJ2iHLz.js → use-providers-Cg1RDnUJ.js} +1 -1
  275. package/dist/web/assets/{useNormalizedLogs-CXAn7xj_.js → useNormalizedLogs-BMa5-cCl.js} +1 -1
  276. package/dist/web/assets/{vennDiagram-CIIHVFJN-DdOfNtRo.js → vennDiagram-CIIHVFJN-CNRNbGoo.js} +1 -1
  277. package/dist/web/assets/{wardley-L42UT6IY-BeKcEIgK.js → wardley-L42UT6IY-Fm0b_w6O.js} +1 -1
  278. package/dist/web/assets/{wardleyDiagram-YWT4CUSO-DPKOxABd.js → wardleyDiagram-YWT4CUSO-C_xf1EWg.js} +1 -1
  279. package/dist/web/assets/{xychartDiagram-2RQKCTM6-olIy_aaY.js → xychartDiagram-2RQKCTM6-D9yke6dF.js} +1 -1
  280. package/dist/web/index.html +2 -2
  281. package/node_modules/@agent-tower/shared/dist/types.d.ts +198 -1
  282. package/node_modules/@agent-tower/shared/dist/types.d.ts.map +1 -1
  283. package/node_modules/@agent-tower/shared/dist/types.js.map +1 -1
  284. package/node_modules/@prisma/client/.prisma/client/edge.js +52 -5
  285. package/node_modules/@prisma/client/.prisma/client/index-browser.js +47 -0
  286. package/node_modules/@prisma/client/.prisma/client/index.d.ts +6144 -2196
  287. package/node_modules/@prisma/client/.prisma/client/index.js +52 -5
  288. package/node_modules/@prisma/client/.prisma/client/package.json +1 -1
  289. package/node_modules/@prisma/client/.prisma/client/schema.prisma +94 -30
  290. package/node_modules/@prisma/client/.prisma/client/wasm.js +47 -0
  291. package/package.json +1 -1
  292. package/prisma/migrations/20260618010000_add_workspace_verdicts/migration.sql +27 -0
  293. package/prisma/migrations/20260701000000_add_access_auth_settings/migration.sql +9 -0
  294. package/prisma/migrations/20260702000000_add_teamrun_target_commit_fields/migration.sql +38 -0
  295. package/prisma/schema.prisma +64 -0
  296. package/dist/web/assets/CreateTaskInput-Cb3aRHr3.js +0 -1
  297. package/dist/web/assets/GeneralSettingsPage-CE7P3t_v.js +0 -1
  298. package/dist/web/assets/McpSettingsPage-gp1XDMUd.js +0 -1
  299. package/dist/web/assets/NotificationSettingsPage-BhZeHEef.js +0 -1
  300. package/dist/web/assets/ProfileSettingsPage-D_y3ePFs.js +0 -3
  301. package/dist/web/assets/ProjectSettingsPage-V38j8T61.js +0 -2
  302. package/dist/web/assets/channel-CWEYGfCJ.js +0 -1
  303. package/dist/web/assets/check-D2WWIhwV.js +0 -1
  304. package/dist/web/assets/classDiagram-4FO5ZUOK-CcU8zfK8.js +0 -1
  305. package/dist/web/assets/classDiagram-v2-Q7XG4LA2-CcU8zfK8.js +0 -1
  306. package/dist/web/assets/confirm-dialog-BAqfLDT8.js +0 -1
  307. package/dist/web/assets/folder-picker-DFB9_pv4.js +0 -1
  308. package/dist/web/assets/folder-u7fKAtz_.js +0 -1
  309. package/dist/web/assets/index-7nw4fV42.js +0 -122
  310. package/dist/web/assets/index-YCjjSSLC.css +0 -1
  311. package/dist/web/assets/input-CKwRYuUJ.js +0 -1
  312. package/dist/web/assets/stateDiagram-v2-BHNVJYJU-CudtEyHd.js +0 -1
@@ -9,13 +9,14 @@ import { WorkspaceKind } from '../../types/index.js';
9
9
  const testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-tower-workspace-service-'));
10
10
  const dbPath = path.join(testDir, 'test.db');
11
11
  process.env.AGENT_TOWER_DATABASE_URL = `file:${dbPath}`;
12
- const { createWorktreeMock, ensureWorktreeExistsMock, removeWorktreeMock, deleteBranchIfSafeMock, mergeWorktreeMock, mergeIntoWorktreeMock, execGitMock, refreshWorkspaceGitWatcherMock, unwatchWorkspaceGitWatcherMock, } = vi.hoisted(() => ({
12
+ const { createWorktreeMock, ensureWorktreeExistsMock, removeWorktreeMock, deleteBranchIfSafeMock, mergeWorktreeMock, mergeIntoWorktreeMock, getGitOperationStatusMock, execGitMock, refreshWorkspaceGitWatcherMock, unwatchWorkspaceGitWatcherMock, } = vi.hoisted(() => ({
13
13
  createWorktreeMock: vi.fn(),
14
14
  ensureWorktreeExistsMock: vi.fn(),
15
15
  removeWorktreeMock: vi.fn(),
16
16
  deleteBranchIfSafeMock: vi.fn(),
17
17
  mergeWorktreeMock: vi.fn(),
18
18
  mergeIntoWorktreeMock: vi.fn(),
19
+ getGitOperationStatusMock: vi.fn(),
19
20
  execGitMock: vi.fn(),
20
21
  refreshWorkspaceGitWatcherMock: vi.fn(async () => undefined),
21
22
  unwatchWorkspaceGitWatcherMock: vi.fn(),
@@ -29,7 +30,7 @@ vi.mock('../../git/worktree.manager.js', () => ({
29
30
  deleteBranchIfSafe: deleteBranchIfSafeMock,
30
31
  getDiff: vi.fn(),
31
32
  rebase: vi.fn(),
32
- getGitOperationStatus: vi.fn(),
33
+ getGitOperationStatus: getGitOperationStatusMock,
33
34
  abortOperation: vi.fn(),
34
35
  merge: mergeWorktreeMock,
35
36
  mergeIntoWorktree: mergeIntoWorktreeMock,
@@ -46,6 +47,9 @@ execGitMock.mockImplementation(async (_repoPath, args) => {
46
47
  if (args[0] === 'rev-parse' && args.includes('--abbrev-ref')) {
47
48
  return 'main\n';
48
49
  }
50
+ if (args[0] === 'rev-parse' && args[1] === 'HEAD') {
51
+ return 'child-head-sha\n';
52
+ }
49
53
  if (args[0] === 'status') {
50
54
  return '';
51
55
  }
@@ -118,6 +122,133 @@ async function createTeamRunWithMember(options = {}) {
118
122
  });
119
123
  return { project, task, teamRun, member };
120
124
  }
125
+ async function addTeamMember(teamRunId, overrides = {}) {
126
+ const suffix = Math.random().toString(36).slice(2, 8);
127
+ return prisma.teamMember.create({
128
+ data: {
129
+ teamRunId,
130
+ presetId: null,
131
+ name: overrides.name ?? `Member ${suffix}`,
132
+ aliases: JSON.stringify([`member-${suffix}`]),
133
+ providerId: `provider-${suffix}`,
134
+ rolePrompt: `Role ${suffix}`,
135
+ capabilities: JSON.stringify(overrides.capabilities ?? {}),
136
+ workspacePolicy: 'dedicated',
137
+ triggerPolicy: 'MENTION_ONLY',
138
+ sessionPolicy: 'new_per_request',
139
+ avatar: null,
140
+ },
141
+ });
142
+ }
143
+ async function createTeamRunChildMergeFixture(options = {}) {
144
+ const { task, teamRun, member: owner } = await createTeamRunWithMember();
145
+ await prisma.task.update({
146
+ where: { id: task.id },
147
+ data: { status: 'IN_REVIEW' },
148
+ });
149
+ const reviewer = options.reviewerIsOwner
150
+ ? owner
151
+ : await addTeamMember(teamRun.id, { name: 'Reviewer', capabilities: { readDiff: true } });
152
+ const merger = await addTeamMember(teamRun.id, {
153
+ name: 'Merger',
154
+ capabilities: options.mergerCapabilities ?? { mergeWorkspace: true },
155
+ });
156
+ const mainWorkspace = await prisma.workspace.create({
157
+ data: {
158
+ taskId: task.id,
159
+ branchName: 'team-main',
160
+ baseBranch: 'main',
161
+ worktreePath: path.join(testDir, 'team-main'),
162
+ status: 'ACTIVE',
163
+ },
164
+ });
165
+ await prisma.teamRun.update({
166
+ where: { id: teamRun.id },
167
+ data: { mainWorkspaceId: mainWorkspace.id },
168
+ });
169
+ const childWorkspace = await prisma.workspace.create({
170
+ data: {
171
+ taskId: task.id,
172
+ parentWorkspaceId: mainWorkspace.id,
173
+ ownerMemberId: owner.id,
174
+ branchName: 'dedicated-child',
175
+ baseBranch: mainWorkspace.branchName,
176
+ worktreePath: path.join(testDir, 'dedicated-child'),
177
+ status: options.childStatus ?? 'ACTIVE',
178
+ },
179
+ });
180
+ const request = await prisma.workRequest.create({
181
+ data: {
182
+ teamRunId: teamRun.id,
183
+ requesterMemberId: null,
184
+ requesterType: 'user',
185
+ targetMemberId: merger.id,
186
+ triggerMessageId: await createRoomMessageId(teamRun.id),
187
+ instruction: 'merge workspace',
188
+ status: 'STARTED',
189
+ },
190
+ });
191
+ const invocation = await prisma.agentInvocation.create({
192
+ data: {
193
+ teamRunId: teamRun.id,
194
+ workRequestId: request.id,
195
+ memberId: merger.id,
196
+ workspaceId: mainWorkspace.id,
197
+ status: 'RUNNING',
198
+ },
199
+ });
200
+ if (options.reviewVerdict) {
201
+ await prisma.workspaceVerdict.create({
202
+ data: {
203
+ workspaceId: childWorkspace.id,
204
+ teamRunId: teamRun.id,
205
+ kind: 'REVIEW',
206
+ verdict: options.reviewVerdict,
207
+ reviewedSha: options.reviewSha ?? 'child-head-sha',
208
+ reviewerMemberId: reviewer.id,
209
+ reason: 'reviewed',
210
+ sequence: 1,
211
+ },
212
+ });
213
+ }
214
+ return { task, teamRun, owner, reviewer, merger, mainWorkspace, childWorkspace, invocation };
215
+ }
216
+ async function createMergeInvocation(teamRunId, memberId, workspaceId) {
217
+ const request = await prisma.workRequest.create({
218
+ data: {
219
+ teamRunId,
220
+ requesterMemberId: null,
221
+ requesterType: 'user',
222
+ targetMemberId: memberId,
223
+ triggerMessageId: await createRoomMessageId(teamRunId),
224
+ instruction: 'merge workspace',
225
+ status: 'STARTED',
226
+ },
227
+ });
228
+ return prisma.agentInvocation.create({
229
+ data: {
230
+ teamRunId,
231
+ workRequestId: request.id,
232
+ memberId,
233
+ workspaceId,
234
+ status: 'RUNNING',
235
+ },
236
+ });
237
+ }
238
+ async function createRoomMessageId(teamRunId) {
239
+ const message = await prisma.roomMessage.create({
240
+ data: {
241
+ teamRunId,
242
+ senderType: 'user',
243
+ senderId: null,
244
+ senderInvocationId: null,
245
+ kind: 'chat',
246
+ content: 'trigger',
247
+ mentions: '[]',
248
+ },
249
+ });
250
+ return message.id;
251
+ }
121
252
  function mockCreatedWorktreePath(branchName) {
122
253
  const worktreePath = path.join(testDir, 'created-worktrees', branchName);
123
254
  fs.mkdirSync(path.join(worktreePath, '.git'), { recursive: true });
@@ -147,6 +278,9 @@ describe('WorkspaceService TeamRun workspace lifecycle', () => {
147
278
  if (args[0] === 'rev-parse' && args.includes('--abbrev-ref')) {
148
279
  return 'main\n';
149
280
  }
281
+ if (args[0] === 'rev-parse' && args[1] === 'HEAD') {
282
+ return 'child-head-sha\n';
283
+ }
150
284
  if (args[0] === 'status') {
151
285
  return '';
152
286
  }
@@ -169,6 +303,16 @@ describe('WorkspaceService TeamRun workspace lifecycle', () => {
169
303
  sourceBranch: 'dedicated-child',
170
304
  targetBranch: 'team-main',
171
305
  });
306
+ getGitOperationStatusMock.mockResolvedValue({
307
+ operation: 'idle',
308
+ conflictedFiles: [],
309
+ conflictOp: null,
310
+ ahead: 1,
311
+ behind: 0,
312
+ hasUncommittedChanges: false,
313
+ uncommittedCount: 0,
314
+ untrackedCount: 0,
315
+ });
172
316
  service = new WorkspaceService();
173
317
  await prisma.agentInvocation.deleteMany();
174
318
  await prisma.workRequest.deleteMany();
@@ -272,6 +416,38 @@ describe('WorkspaceService TeamRun workspace lifecycle', () => {
272
416
  });
273
417
  expect(createWorktreeMock).not.toHaveBeenCalled();
274
418
  });
419
+ it('rejects worktree workspace creation for Git repositories without commits', async () => {
420
+ const repoPath = fs.mkdtempSync(path.join(testDir, 'empty-git-project-'));
421
+ fs.mkdirSync(path.join(repoPath, '.git'), { recursive: true });
422
+ execGitMock.mockImplementation(async (_repoPath, args) => {
423
+ if (args[0] === 'rev-parse' && args[1] === '--verify' && args[2] === 'HEAD') {
424
+ throw new Error('HEAD does not exist');
425
+ }
426
+ if (args[0] === 'rev-parse' && args[1] === '--is-inside-work-tree') {
427
+ return 'true\n';
428
+ }
429
+ return '';
430
+ });
431
+ const project = await prisma.project.create({
432
+ data: {
433
+ name: 'Empty Git project',
434
+ repoPath,
435
+ },
436
+ });
437
+ const task = await prisma.task.create({
438
+ data: {
439
+ title: 'Empty Git task',
440
+ projectId: project.id,
441
+ },
442
+ });
443
+ await expect(service.create(task.id, {
444
+ workspaceKind: WorkspaceKind.WORKTREE,
445
+ })).rejects.toMatchObject({
446
+ code: 'VALIDATION_ERROR',
447
+ message: expect.stringContaining('has no commits'),
448
+ });
449
+ expect(createWorktreeMock).not.toHaveBeenCalled();
450
+ });
275
451
  it('rejects git-status for non-git main-directory workspaces', async () => {
276
452
  const repoPath = fs.mkdtempSync(path.join(testDir, 'local-git-status-project-'));
277
453
  const project = await prisma.project.create({
@@ -581,7 +757,25 @@ describe('WorkspaceService TeamRun workspace lifecycle', () => {
581
757
  status: 'ACTIVE',
582
758
  },
583
759
  });
584
- const sha = await service.merge(childWorkspace.id, 'merge child');
760
+ const reviewer = await addTeamMember(teamRun.id, { name: 'Reviewer', capabilities: { readDiff: true } });
761
+ const merger = await addTeamMember(teamRun.id, { name: 'Merger', capabilities: { mergeWorkspace: true } });
762
+ const invocation = await createMergeInvocation(teamRun.id, merger.id, mainWorkspace.id);
763
+ await prisma.workspaceVerdict.create({
764
+ data: {
765
+ workspaceId: childWorkspace.id,
766
+ teamRunId: teamRun.id,
767
+ kind: 'REVIEW',
768
+ verdict: 'APPROVED',
769
+ reviewedSha: 'child-head-sha',
770
+ reviewerMemberId: reviewer.id,
771
+ reason: 'approved',
772
+ },
773
+ });
774
+ const sha = await service.merge(childWorkspace.id, {
775
+ commitMessage: 'merge child',
776
+ lockOwnerId: invocation.id,
777
+ invocationId: invocation.id,
778
+ });
585
779
  expect(sha).toBe('child-merge-sha');
586
780
  expect(mergeIntoWorktreeMock).toHaveBeenCalledWith(childWorkspace.worktreePath, mainWorkspace.worktreePath, { commitMessage: 'merge child' });
587
781
  expect(mergeWorktreeMock).not.toHaveBeenCalled();
@@ -616,6 +810,20 @@ describe('WorkspaceService TeamRun workspace lifecycle', () => {
616
810
  status: 'ACTIVE',
617
811
  },
618
812
  });
813
+ const reviewer = await addTeamMember(teamRun.id, { name: 'Reviewer', capabilities: { readDiff: true } });
814
+ const merger = await addTeamMember(teamRun.id, { name: 'Merger', capabilities: { mergeWorkspace: true } });
815
+ const invocation = await createMergeInvocation(teamRun.id, merger.id, mainWorkspace.id);
816
+ await prisma.workspaceVerdict.create({
817
+ data: {
818
+ workspaceId: childWorkspace.id,
819
+ teamRunId: teamRun.id,
820
+ kind: 'REVIEW',
821
+ verdict: 'APPROVED',
822
+ reviewedSha: 'child-head-sha',
823
+ reviewerMemberId: reviewer.id,
824
+ reason: 'approved',
825
+ },
826
+ });
619
827
  await prisma.session.create({
620
828
  data: {
621
829
  workspaceId: mainWorkspace.id,
@@ -624,11 +832,791 @@ describe('WorkspaceService TeamRun workspace lifecycle', () => {
624
832
  status: 'RUNNING',
625
833
  },
626
834
  });
835
+ await expect(service.merge(childWorkspace.id, {
836
+ lockOwnerId: invocation.id,
837
+ invocationId: invocation.id,
838
+ })).rejects.toMatchObject({
839
+ code: 'PARENT_WORKSPACE_HAS_ACTIVE_SESSION',
840
+ });
841
+ expect(mergeIntoWorktreeMock).not.toHaveBeenCalled();
842
+ });
843
+ it('rejects TeamRun dedicated child merge without an approved review', async () => {
844
+ const { childWorkspace, invocation } = await createTeamRunChildMergeFixture();
845
+ await expect(service.merge(childWorkspace.id, {
846
+ lockOwnerId: invocation.id,
847
+ invocationId: invocation.id,
848
+ })).rejects.toMatchObject({
849
+ code: 'REVIEW_REQUIRED',
850
+ statusCode: 409,
851
+ });
852
+ expect(mergeIntoWorktreeMock).not.toHaveBeenCalled();
853
+ });
854
+ it('rejects TeamRun dedicated child merge when the approved review SHA is stale', async () => {
855
+ const { childWorkspace, invocation } = await createTeamRunChildMergeFixture({
856
+ reviewVerdict: 'APPROVED',
857
+ reviewSha: 'old-sha',
858
+ });
859
+ await expect(service.merge(childWorkspace.id, {
860
+ lockOwnerId: invocation.id,
861
+ invocationId: invocation.id,
862
+ })).rejects.toMatchObject({
863
+ code: 'REVIEW_STALE',
864
+ statusCode: 409,
865
+ });
866
+ expect(mergeIntoWorktreeMock).not.toHaveBeenCalled();
867
+ });
868
+ it('rejects TeamRun dedicated child merge when the latest review requests changes', async () => {
869
+ const { teamRun, childWorkspace, invocation, reviewer } = await createTeamRunChildMergeFixture({
870
+ reviewVerdict: 'APPROVED',
871
+ });
872
+ const sameCreatedAt = new Date(Date.UTC(2026, 0, 1, 0, 0, 0));
873
+ await prisma.workspaceVerdict.updateMany({
874
+ where: {
875
+ workspaceId: childWorkspace.id,
876
+ kind: 'REVIEW',
877
+ verdict: 'APPROVED',
878
+ },
879
+ data: {
880
+ createdAt: sameCreatedAt,
881
+ sequence: 1,
882
+ },
883
+ });
884
+ await prisma.workspaceVerdict.create({
885
+ data: {
886
+ workspaceId: childWorkspace.id,
887
+ teamRunId: teamRun.id,
888
+ kind: 'REVIEW',
889
+ verdict: 'CHANGES_REQUESTED',
890
+ reviewedSha: 'child-head-sha',
891
+ reviewerMemberId: reviewer.id,
892
+ reason: 'needs changes',
893
+ createdAt: sameCreatedAt,
894
+ sequence: 2,
895
+ },
896
+ });
897
+ await expect(service.merge(childWorkspace.id, {
898
+ lockOwnerId: invocation.id,
899
+ invocationId: invocation.id,
900
+ })).rejects.toMatchObject({
901
+ code: 'REVIEW_REQUIRED',
902
+ statusCode: 409,
903
+ });
904
+ expect(mergeIntoWorktreeMock).not.toHaveBeenCalled();
905
+ });
906
+ it('rejects ACTIVE TeamRun dedicated child merge when the invocation identity is missing', async () => {
907
+ const { childWorkspace } = await createTeamRunChildMergeFixture({
908
+ reviewVerdict: 'APPROVED',
909
+ });
627
910
  await expect(service.merge(childWorkspace.id)).rejects.toMatchObject({
911
+ code: 'TEAM_RUN_MERGE_INVOCATION_REQUIRED',
912
+ statusCode: 403,
913
+ });
914
+ expect(mergeIntoWorktreeMock).not.toHaveBeenCalled();
915
+ });
916
+ it('rejects ACTIVE TeamRun dedicated child merge when the invocation identity is invalid', async () => {
917
+ const { childWorkspace } = await createTeamRunChildMergeFixture({
918
+ reviewVerdict: 'APPROVED',
919
+ });
920
+ await expect(service.merge(childWorkspace.id, {
921
+ lockOwnerId: 'missing-invocation',
922
+ invocationId: 'missing-invocation',
923
+ })).rejects.toMatchObject({
924
+ code: 'FORBIDDEN',
925
+ statusCode: 403,
926
+ });
927
+ expect(mergeIntoWorktreeMock).not.toHaveBeenCalled();
928
+ });
929
+ it('rejects TeamRun dedicated child merge when the owner approved their own workspace', async () => {
930
+ const { childWorkspace, invocation } = await createTeamRunChildMergeFixture({
931
+ reviewVerdict: 'APPROVED',
932
+ reviewerIsOwner: true,
933
+ });
934
+ await expect(service.merge(childWorkspace.id, {
935
+ lockOwnerId: invocation.id,
936
+ invocationId: invocation.id,
937
+ })).rejects.toMatchObject({
938
+ code: 'SELF_REVIEW_FORBIDDEN',
939
+ statusCode: 409,
940
+ });
941
+ expect(mergeIntoWorktreeMock).not.toHaveBeenCalled();
942
+ });
943
+ it('rejects TeamRun dedicated child merge when the owner has active work', async () => {
944
+ const { teamRun, owner, childWorkspace, invocation } = await createTeamRunChildMergeFixture({
945
+ reviewVerdict: 'APPROVED',
946
+ });
947
+ const ownerRequest = await prisma.workRequest.create({
948
+ data: {
949
+ teamRunId: teamRun.id,
950
+ requesterMemberId: null,
951
+ requesterType: 'user',
952
+ targetMemberId: owner.id,
953
+ triggerMessageId: await createRoomMessageId(teamRun.id),
954
+ instruction: 'continue writing',
955
+ status: 'STARTED',
956
+ },
957
+ });
958
+ await prisma.agentInvocation.create({
959
+ data: {
960
+ teamRunId: teamRun.id,
961
+ workRequestId: ownerRequest.id,
962
+ memberId: owner.id,
963
+ workspaceId: childWorkspace.id,
964
+ status: 'RUNNING',
965
+ },
966
+ });
967
+ await expect(service.merge(childWorkspace.id, {
968
+ lockOwnerId: invocation.id,
969
+ invocationId: invocation.id,
970
+ })).rejects.toMatchObject({
971
+ code: 'OWNER_HAS_ACTIVE_INVOCATION',
972
+ statusCode: 409,
973
+ });
974
+ expect(mergeIntoWorktreeMock).not.toHaveBeenCalled();
975
+ });
976
+ it('rejects TeamRun dedicated child merge when the parent has active write sessions after review passes', async () => {
977
+ const { mainWorkspace, childWorkspace, invocation } = await createTeamRunChildMergeFixture({
978
+ reviewVerdict: 'APPROVED',
979
+ });
980
+ await prisma.session.create({
981
+ data: {
982
+ workspaceId: mainWorkspace.id,
983
+ agentType: 'CODEX',
984
+ prompt: 'write on parent',
985
+ status: 'RUNNING',
986
+ },
987
+ });
988
+ await expect(service.merge(childWorkspace.id, {
989
+ lockOwnerId: invocation.id,
990
+ invocationId: invocation.id,
991
+ })).rejects.toMatchObject({
628
992
  code: 'PARENT_WORKSPACE_HAS_ACTIVE_SESSION',
993
+ statusCode: 409,
994
+ });
995
+ expect(mergeIntoWorktreeMock).not.toHaveBeenCalled();
996
+ });
997
+ it('rejects TeamRun dedicated child merge for an invocation without mergeWorkspace capability', async () => {
998
+ const { childWorkspace, invocation } = await createTeamRunChildMergeFixture({
999
+ mergerCapabilities: { mergeWorkspace: false },
1000
+ reviewVerdict: 'APPROVED',
1001
+ });
1002
+ await expect(service.merge(childWorkspace.id, {
1003
+ lockOwnerId: invocation.id,
1004
+ invocationId: invocation.id,
1005
+ })).rejects.toMatchObject({
1006
+ code: 'TEAM_RUN_MEMBER_CAPABILITY_REQUIRED',
1007
+ statusCode: 403,
1008
+ });
1009
+ expect(mergeIntoWorktreeMock).not.toHaveBeenCalled();
1010
+ });
1011
+ it('returns idempotent success when a TeamRun dedicated child workspace is already merged', async () => {
1012
+ const { childWorkspace, invocation } = await createTeamRunChildMergeFixture({
1013
+ childStatus: 'MERGED',
1014
+ });
1015
+ const sha = await service.merge(childWorkspace.id, {
1016
+ lockOwnerId: invocation.id,
1017
+ invocationId: invocation.id,
629
1018
  });
1019
+ expect(sha).toBe('child-head-sha');
630
1020
  expect(mergeIntoWorktreeMock).not.toHaveBeenCalled();
631
1021
  });
1022
+ it('lists mergeable TeamRun workspaces with review, activity, and behind warnings', async () => {
1023
+ const { teamRun, childWorkspace } = await createTeamRunChildMergeFixture({
1024
+ reviewVerdict: 'APPROVED',
1025
+ });
1026
+ getGitOperationStatusMock.mockResolvedValueOnce({
1027
+ operation: 'idle',
1028
+ conflictedFiles: [],
1029
+ conflictOp: null,
1030
+ ahead: 2,
1031
+ behind: 1,
1032
+ hasUncommittedChanges: false,
1033
+ uncommittedCount: 0,
1034
+ untrackedCount: 0,
1035
+ });
1036
+ const response = await service.listTeamRunMergeableWorkspaces(teamRun.id);
1037
+ expect(response).toMatchObject({
1038
+ teamRunId: teamRun.id,
1039
+ taskId: childWorkspace.taskId,
1040
+ workspaces: [
1041
+ expect.objectContaining({
1042
+ workspaceId: childWorkspace.id,
1043
+ headSha: 'child-head-sha',
1044
+ mergeReady: true,
1045
+ latestReview: expect.objectContaining({
1046
+ verdict: 'APPROVED',
1047
+ reviewedSha: 'child-head-sha',
1048
+ matchesHead: true,
1049
+ isSelfReview: false,
1050
+ }),
1051
+ git: expect.objectContaining({
1052
+ aheadOfMain: 2,
1053
+ behindMain: 1,
1054
+ clean: true,
1055
+ }),
1056
+ blockers: [
1057
+ expect.objectContaining({
1058
+ code: 'BEHIND_MAIN',
1059
+ severity: 'WARNING',
1060
+ }),
1061
+ ],
1062
+ }),
1063
+ ],
1064
+ });
1065
+ });
1066
+ it('reports readiness blockers for stale reviews, owner activity, and parent activity', async () => {
1067
+ const { teamRun, owner, childWorkspace, mainWorkspace } = await createTeamRunChildMergeFixture({
1068
+ reviewVerdict: 'APPROVED',
1069
+ reviewSha: 'old-sha',
1070
+ });
1071
+ const ownerRequest = await prisma.workRequest.create({
1072
+ data: {
1073
+ teamRunId: teamRun.id,
1074
+ requesterMemberId: null,
1075
+ requesterType: 'user',
1076
+ targetMemberId: owner.id,
1077
+ triggerMessageId: await createRoomMessageId(teamRun.id),
1078
+ instruction: 'continue writing',
1079
+ status: 'STARTED',
1080
+ },
1081
+ });
1082
+ await prisma.agentInvocation.create({
1083
+ data: {
1084
+ teamRunId: teamRun.id,
1085
+ workRequestId: ownerRequest.id,
1086
+ memberId: owner.id,
1087
+ workspaceId: childWorkspace.id,
1088
+ status: 'RUNNING',
1089
+ },
1090
+ });
1091
+ await prisma.session.create({
1092
+ data: {
1093
+ workspaceId: mainWorkspace.id,
1094
+ agentType: 'CODEX',
1095
+ prompt: 'write on parent',
1096
+ status: 'RUNNING',
1097
+ },
1098
+ });
1099
+ const response = await service.listTeamRunMergeableWorkspaces(teamRun.id);
1100
+ const item = response.workspaces[0];
1101
+ expect(item.mergeReady).toBe(false);
1102
+ expect(item.blockers.map((entry) => entry.code)).toEqual(expect.arrayContaining([
1103
+ 'REVIEW_STALE',
1104
+ 'OWNER_HAS_ACTIVE_INVOCATION',
1105
+ 'PARENT_WORKSPACE_HAS_ACTIVE_SESSION',
1106
+ ]));
1107
+ });
1108
+ it('dry-runs batch member merges without changing workspace state', async () => {
1109
+ const { teamRun, childWorkspace, invocation, merger } = await createTeamRunChildMergeFixture({
1110
+ reviewVerdict: 'APPROVED',
1111
+ });
1112
+ const result = await service.mergeTeamRunMembers(teamRun.id, {
1113
+ invocationId: invocation.id,
1114
+ requesterMemberId: merger.id,
1115
+ dryRun: true,
1116
+ });
1117
+ expect(result.summary).toMatchObject({
1118
+ requested: 1,
1119
+ considered: 1,
1120
+ wouldMerge: 1,
1121
+ merged: 0,
1122
+ });
1123
+ expect(result.results[0]).toMatchObject({
1124
+ workspaceId: childWorkspace.id,
1125
+ status: 'WOULD_MERGE',
1126
+ });
1127
+ expect(mergeIntoWorktreeMock).not.toHaveBeenCalled();
1128
+ await expect(prisma.workspace.findUnique({ where: { id: childWorkspace.id } })).resolves.toMatchObject({
1129
+ status: 'ACTIVE',
1130
+ });
1131
+ });
1132
+ it('treats an explicit empty workspaceIds batch request as an empty no-op', async () => {
1133
+ const { teamRun, childWorkspace, invocation, merger } = await createTeamRunChildMergeFixture({
1134
+ reviewVerdict: 'APPROVED',
1135
+ });
1136
+ const result = await service.mergeTeamRunMembers(teamRun.id, {
1137
+ invocationId: invocation.id,
1138
+ requesterMemberId: merger.id,
1139
+ workspaceIds: [],
1140
+ });
1141
+ expect(result.summary).toMatchObject({
1142
+ requested: 0,
1143
+ considered: 0,
1144
+ merged: 0,
1145
+ skipped: 0,
1146
+ conflicts: 0,
1147
+ failed: 0,
1148
+ });
1149
+ expect(result.requestedWorkspaceIds).toEqual([]);
1150
+ expect(result.results).toEqual([]);
1151
+ expect(mergeIntoWorktreeMock).not.toHaveBeenCalled();
1152
+ await expect(prisma.workspace.findUnique({ where: { id: childWorkspace.id } })).resolves.toMatchObject({
1153
+ status: 'ACTIVE',
1154
+ });
1155
+ });
1156
+ it('batch merge defaults to only merge-ready member workspaces', async () => {
1157
+ const { teamRun, childWorkspace, invocation, merger, mainWorkspace } = await createTeamRunChildMergeFixture({
1158
+ reviewVerdict: 'APPROVED',
1159
+ });
1160
+ const blockedOwner = await addTeamMember(teamRun.id, { name: 'Blocked owner' });
1161
+ const blockedWorkspace = await prisma.workspace.create({
1162
+ data: {
1163
+ taskId: childWorkspace.taskId,
1164
+ parentWorkspaceId: mainWorkspace.id,
1165
+ ownerMemberId: blockedOwner.id,
1166
+ branchName: 'blocked-child',
1167
+ baseBranch: mainWorkspace.branchName,
1168
+ worktreePath: path.join(testDir, 'blocked-child'),
1169
+ status: 'ACTIVE',
1170
+ },
1171
+ });
1172
+ const result = await service.mergeTeamRunMembers(teamRun.id, {
1173
+ invocationId: invocation.id,
1174
+ requesterMemberId: merger.id,
1175
+ });
1176
+ expect(result.summary).toMatchObject({
1177
+ requested: 2,
1178
+ considered: 2,
1179
+ merged: 1,
1180
+ skipped: 1,
1181
+ });
1182
+ expect(result.results).toEqual(expect.arrayContaining([
1183
+ expect.objectContaining({
1184
+ workspaceId: childWorkspace.id,
1185
+ status: 'MERGED',
1186
+ sha: 'child-merge-sha',
1187
+ }),
1188
+ expect.objectContaining({
1189
+ workspaceId: blockedWorkspace.id,
1190
+ status: 'SKIPPED',
1191
+ code: 'REVIEW_REQUIRED',
1192
+ }),
1193
+ ]));
1194
+ expect(mergeIntoWorktreeMock).toHaveBeenCalledTimes(1);
1195
+ });
1196
+ it('batch merge returns idempotent results for already merged workspaces', async () => {
1197
+ const { teamRun, childWorkspace, invocation, merger } = await createTeamRunChildMergeFixture({
1198
+ childStatus: 'MERGED',
1199
+ });
1200
+ const result = await service.mergeTeamRunMembers(teamRun.id, {
1201
+ invocationId: invocation.id,
1202
+ requesterMemberId: merger.id,
1203
+ });
1204
+ expect(result.summary).toMatchObject({
1205
+ requested: 1,
1206
+ considered: 1,
1207
+ alreadyMerged: 1,
1208
+ merged: 0,
1209
+ });
1210
+ expect(result.results[0]).toMatchObject({
1211
+ workspaceId: childWorkspace.id,
1212
+ status: 'ALREADY_MERGED',
1213
+ sha: 'child-head-sha',
1214
+ });
1215
+ expect(mergeIntoWorktreeMock).not.toHaveBeenCalled();
1216
+ });
1217
+ it('rejects batch member merge when invocation lacks mergeWorkspace capability', async () => {
1218
+ const { teamRun, invocation, merger } = await createTeamRunChildMergeFixture({
1219
+ mergerCapabilities: { mergeWorkspace: false },
1220
+ reviewVerdict: 'APPROVED',
1221
+ });
1222
+ await expect(service.mergeTeamRunMembers(teamRun.id, {
1223
+ invocationId: invocation.id,
1224
+ requesterMemberId: merger.id,
1225
+ })).rejects.toMatchObject({
1226
+ code: 'TEAM_RUN_MEMBER_CAPABILITY_REQUIRED',
1227
+ statusCode: 403,
1228
+ });
1229
+ expect(mergeIntoWorktreeMock).not.toHaveBeenCalled();
1230
+ });
1231
+ it('continues batch member merge after per-workspace failure', async () => {
1232
+ const { teamRun, childWorkspace, invocation, merger, mainWorkspace } = await createTeamRunChildMergeFixture({
1233
+ reviewVerdict: 'APPROVED',
1234
+ });
1235
+ const secondOwner = await addTeamMember(teamRun.id, { name: 'Second owner' });
1236
+ const secondWorkspace = await prisma.workspace.create({
1237
+ data: {
1238
+ taskId: childWorkspace.taskId,
1239
+ parentWorkspaceId: mainWorkspace.id,
1240
+ ownerMemberId: secondOwner.id,
1241
+ branchName: 'second-child',
1242
+ baseBranch: mainWorkspace.branchName,
1243
+ worktreePath: path.join(testDir, 'second-child'),
1244
+ status: 'ACTIVE',
1245
+ },
1246
+ });
1247
+ await prisma.workspaceVerdict.create({
1248
+ data: {
1249
+ workspaceId: secondWorkspace.id,
1250
+ teamRunId: teamRun.id,
1251
+ kind: 'REVIEW',
1252
+ verdict: 'APPROVED',
1253
+ reviewedSha: 'child-head-sha',
1254
+ reviewerMemberId: merger.id,
1255
+ reason: 'approved',
1256
+ sequence: 1,
1257
+ },
1258
+ });
1259
+ mergeIntoWorktreeMock
1260
+ .mockRejectedValueOnce(Object.assign(new Error('git failed'), { code: 'GIT_ERROR' }))
1261
+ .mockResolvedValueOnce({
1262
+ sha: 'second-merge-sha',
1263
+ sourceBranch: 'second-child',
1264
+ targetBranch: 'team-main',
1265
+ });
1266
+ const result = await service.mergeTeamRunMembers(teamRun.id, {
1267
+ invocationId: invocation.id,
1268
+ requesterMemberId: merger.id,
1269
+ });
1270
+ expect(result.summary).toMatchObject({
1271
+ requested: 2,
1272
+ considered: 2,
1273
+ failed: 1,
1274
+ merged: 1,
1275
+ });
1276
+ expect(result.results).toEqual(expect.arrayContaining([
1277
+ expect.objectContaining({
1278
+ workspaceId: childWorkspace.id,
1279
+ status: 'FAILED',
1280
+ code: 'GIT_ERROR',
1281
+ }),
1282
+ expect.objectContaining({
1283
+ workspaceId: secondWorkspace.id,
1284
+ status: 'MERGED',
1285
+ sha: 'second-merge-sha',
1286
+ }),
1287
+ ]));
1288
+ expect(mergeIntoWorktreeMock).toHaveBeenCalledTimes(2);
1289
+ });
1290
+ it('returns conflict item details from batch member merge', async () => {
1291
+ const { teamRun, childWorkspace, invocation, merger } = await createTeamRunChildMergeFixture({
1292
+ reviewVerdict: 'APPROVED',
1293
+ });
1294
+ const conflictError = Object.assign(new Error('Merge conflict in files: file.txt'), {
1295
+ name: 'MergeConflictError',
1296
+ code: 'MERGE_CONFLICT',
1297
+ conflictedFiles: ['file.txt'],
1298
+ sourceBranch: childWorkspace.branchName,
1299
+ targetBranch: 'team-main',
1300
+ sourceWorkspaceId: childWorkspace.id,
1301
+ targetWorkspaceId: 'main-workspace',
1302
+ });
1303
+ mergeIntoWorktreeMock.mockRejectedValueOnce(conflictError);
1304
+ const result = await service.mergeTeamRunMembers(teamRun.id, {
1305
+ invocationId: invocation.id,
1306
+ requesterMemberId: merger.id,
1307
+ stopOnConflict: true,
1308
+ });
1309
+ expect(result.summary).toMatchObject({
1310
+ conflicts: 1,
1311
+ merged: 0,
1312
+ });
1313
+ expect(result.results[0]).toMatchObject({
1314
+ workspaceId: childWorkspace.id,
1315
+ status: 'CONFLICT',
1316
+ code: 'MERGE_CONFLICT',
1317
+ conflictedFiles: ['file.txt'],
1318
+ });
1319
+ });
1320
+ it('releases the project merge lock after batch member merge failure', async () => {
1321
+ const lockService = new TeamLockService();
1322
+ service = new WorkspaceService(lockService);
1323
+ const { teamRun, invocation, merger } = await createTeamRunChildMergeFixture({
1324
+ reviewVerdict: 'APPROVED',
1325
+ });
1326
+ mergeIntoWorktreeMock.mockRejectedValueOnce(Object.assign(new Error('git failed'), { code: 'GIT_ERROR' }));
1327
+ await service.mergeTeamRunMembers(teamRun.id, {
1328
+ invocationId: invocation.id,
1329
+ requesterMemberId: merger.id,
1330
+ });
1331
+ expect(lockService.listLocks()).toEqual([]);
1332
+ });
1333
+ it('reuses an existing project merge lock held by the same invocation', async () => {
1334
+ const lockService = new TeamLockService();
1335
+ service = new WorkspaceService(lockService);
1336
+ const { task, teamRun, childWorkspace, invocation, merger } = await createTeamRunChildMergeFixture({
1337
+ reviewVerdict: 'APPROVED',
1338
+ });
1339
+ const lockKey = `project:${task.projectId}:merge`;
1340
+ expect(lockService.acquire(invocation.id, [lockKey])).toBe(true);
1341
+ const result = await service.mergeTeamRunMembers(teamRun.id, {
1342
+ invocationId: invocation.id,
1343
+ requesterMemberId: merger.id,
1344
+ });
1345
+ expect(result.results[0]).toMatchObject({
1346
+ workspaceId: childWorkspace.id,
1347
+ status: 'MERGED',
1348
+ });
1349
+ expect(lockService.isHeldBy(invocation.id, lockKey)).toBe(true);
1350
+ lockService.release(invocation.id, [lockKey]);
1351
+ });
1352
+ it('records workspace review and test verdicts for the current HEAD', async () => {
1353
+ const { teamRun, childWorkspace } = await createTeamRunChildMergeFixture();
1354
+ const reviewer = await addTeamMember(teamRun.id, {
1355
+ name: 'Review And Test',
1356
+ capabilities: { readDiff: true, runCommands: true },
1357
+ });
1358
+ const review = await service.recordVerdict(childWorkspace.id, {
1359
+ kind: 'REVIEW',
1360
+ verdict: 'APPROVED',
1361
+ reviewedSha: 'child-head-sha',
1362
+ reviewerMemberId: reviewer.id,
1363
+ reason: 'looks good',
1364
+ });
1365
+ const test = await service.recordVerdict(childWorkspace.id, {
1366
+ kind: 'TEST',
1367
+ verdict: 'PASSED',
1368
+ reviewedSha: 'child-head-sha',
1369
+ reviewerMemberId: reviewer.id,
1370
+ reason: 'unit tests passed',
1371
+ });
1372
+ expect(review).toMatchObject({
1373
+ workspaceId: childWorkspace.id,
1374
+ kind: 'REVIEW',
1375
+ verdict: 'APPROVED',
1376
+ reviewedSha: 'child-head-sha',
1377
+ reviewerMemberId: reviewer.id,
1378
+ sequence: 1,
1379
+ });
1380
+ expect(test).toMatchObject({
1381
+ workspaceId: childWorkspace.id,
1382
+ kind: 'TEST',
1383
+ verdict: 'PASSED',
1384
+ reviewedSha: 'child-head-sha',
1385
+ reviewerMemberId: reviewer.id,
1386
+ sequence: 1,
1387
+ });
1388
+ await expect(service.listVerdicts(childWorkspace.id)).resolves.toHaveLength(2);
1389
+ });
1390
+ it('resolves targeted invocation identity for verdicts recorded on the source workspace', async () => {
1391
+ const { teamRun, mainWorkspace, childWorkspace } = await createTeamRunChildMergeFixture();
1392
+ const reviewer = await addTeamMember(teamRun.id, {
1393
+ name: 'Targeted Reviewer',
1394
+ capabilities: { readDiff: true },
1395
+ });
1396
+ const executionWorkspace = await prisma.workspace.create({
1397
+ data: {
1398
+ taskId: childWorkspace.taskId,
1399
+ parentWorkspaceId: mainWorkspace.id,
1400
+ ownerMemberId: reviewer.id,
1401
+ branchName: 'targeted-review-execution',
1402
+ baseBranch: childWorkspace.branchName,
1403
+ worktreePath: path.join(testDir, 'targeted-review-execution'),
1404
+ status: 'ACTIVE',
1405
+ },
1406
+ });
1407
+ const request = await prisma.workRequest.create({
1408
+ data: {
1409
+ teamRunId: teamRun.id,
1410
+ requesterMemberId: null,
1411
+ requesterType: 'user',
1412
+ targetMemberId: reviewer.id,
1413
+ targetKind: 'WORKSPACE_COMMIT',
1414
+ targetPurpose: 'REVIEW',
1415
+ targetSourceWorkspaceId: childWorkspace.id,
1416
+ targetHeadSha: 'child-head-sha',
1417
+ targetBranchName: childWorkspace.branchName,
1418
+ triggerMessageId: await createRoomMessageId(teamRun.id),
1419
+ instruction: 'review target commit',
1420
+ status: 'STARTED',
1421
+ },
1422
+ });
1423
+ const invocation = await prisma.agentInvocation.create({
1424
+ data: {
1425
+ teamRunId: teamRun.id,
1426
+ workRequestId: request.id,
1427
+ memberId: reviewer.id,
1428
+ workspaceId: executionWorkspace.id,
1429
+ targetKind: 'WORKSPACE_COMMIT',
1430
+ targetPurpose: 'REVIEW',
1431
+ targetSourceWorkspaceId: childWorkspace.id,
1432
+ targetHeadSha: 'child-head-sha',
1433
+ targetBranchName: childWorkspace.branchName,
1434
+ targetSyncStatus: 'SYNCED',
1435
+ status: 'RUNNING',
1436
+ },
1437
+ });
1438
+ const identity = await service.resolveInvocationMemberForWorkspace(childWorkspace.id, invocation.id);
1439
+ expect(identity).toMatchObject({
1440
+ teamRunId: teamRun.id,
1441
+ memberId: reviewer.id,
1442
+ invocationId: invocation.id,
1443
+ });
1444
+ const review = await service.recordVerdict(childWorkspace.id, {
1445
+ kind: 'REVIEW',
1446
+ verdict: 'APPROVED',
1447
+ reviewedSha: 'child-head-sha',
1448
+ reviewerMemberId: identity.memberId,
1449
+ reason: 'target commit reviewed',
1450
+ });
1451
+ expect(review).toMatchObject({
1452
+ workspaceId: childWorkspace.id,
1453
+ kind: 'REVIEW',
1454
+ verdict: 'APPROVED',
1455
+ reviewedSha: 'child-head-sha',
1456
+ reviewerMemberId: reviewer.id,
1457
+ });
1458
+ });
1459
+ it('rejects targeted workspace verdicts when the source workspace HEAD moved past targetHeadSha', async () => {
1460
+ const { teamRun, mainWorkspace, childWorkspace } = await createTeamRunChildMergeFixture();
1461
+ const reviewer = await addTeamMember(teamRun.id, {
1462
+ name: 'Stale Target Reviewer',
1463
+ capabilities: { readDiff: true },
1464
+ });
1465
+ const executionWorkspace = await prisma.workspace.create({
1466
+ data: {
1467
+ taskId: childWorkspace.taskId,
1468
+ parentWorkspaceId: mainWorkspace.id,
1469
+ ownerMemberId: reviewer.id,
1470
+ branchName: 'stale-target-execution',
1471
+ baseBranch: childWorkspace.branchName,
1472
+ worktreePath: path.join(testDir, 'stale-target-execution'),
1473
+ status: 'ACTIVE',
1474
+ },
1475
+ });
1476
+ const request = await prisma.workRequest.create({
1477
+ data: {
1478
+ teamRunId: teamRun.id,
1479
+ requesterMemberId: null,
1480
+ requesterType: 'user',
1481
+ targetMemberId: reviewer.id,
1482
+ targetKind: 'WORKSPACE_COMMIT',
1483
+ targetPurpose: 'REVIEW',
1484
+ targetSourceWorkspaceId: childWorkspace.id,
1485
+ targetHeadSha: 'old-target-sha',
1486
+ targetBranchName: childWorkspace.branchName,
1487
+ triggerMessageId: await createRoomMessageId(teamRun.id),
1488
+ instruction: 'review old target',
1489
+ status: 'STARTED',
1490
+ },
1491
+ });
1492
+ const invocation = await prisma.agentInvocation.create({
1493
+ data: {
1494
+ teamRunId: teamRun.id,
1495
+ workRequestId: request.id,
1496
+ memberId: reviewer.id,
1497
+ workspaceId: executionWorkspace.id,
1498
+ targetKind: 'WORKSPACE_COMMIT',
1499
+ targetPurpose: 'REVIEW',
1500
+ targetSourceWorkspaceId: childWorkspace.id,
1501
+ targetHeadSha: 'old-target-sha',
1502
+ targetBranchName: childWorkspace.branchName,
1503
+ targetSyncStatus: 'SYNCED',
1504
+ status: 'RUNNING',
1505
+ },
1506
+ });
1507
+ const identity = await service.resolveInvocationMemberForWorkspace(childWorkspace.id, invocation.id);
1508
+ await expect(service.recordVerdict(childWorkspace.id, {
1509
+ kind: 'REVIEW',
1510
+ verdict: 'APPROVED',
1511
+ reviewedSha: 'child-head-sha',
1512
+ reviewerMemberId: identity.memberId,
1513
+ expectedTargetHeadSha: identity.targetHeadSha,
1514
+ })).rejects.toMatchObject({
1515
+ code: 'TARGET_VERDICT_STALE',
1516
+ statusCode: 409,
1517
+ });
1518
+ });
1519
+ it('rejects targeted workspace verdicts when reviewedSha differs from targetHeadSha', async () => {
1520
+ const { teamRun, mainWorkspace, childWorkspace } = await createTeamRunChildMergeFixture();
1521
+ const reviewer = await addTeamMember(teamRun.id, {
1522
+ name: 'Target Mismatch Reviewer',
1523
+ capabilities: { readDiff: true },
1524
+ });
1525
+ const executionWorkspace = await prisma.workspace.create({
1526
+ data: {
1527
+ taskId: childWorkspace.taskId,
1528
+ parentWorkspaceId: mainWorkspace.id,
1529
+ ownerMemberId: reviewer.id,
1530
+ branchName: 'target-mismatch-execution',
1531
+ baseBranch: childWorkspace.branchName,
1532
+ worktreePath: path.join(testDir, 'target-mismatch-execution'),
1533
+ status: 'ACTIVE',
1534
+ },
1535
+ });
1536
+ const request = await prisma.workRequest.create({
1537
+ data: {
1538
+ teamRunId: teamRun.id,
1539
+ requesterMemberId: null,
1540
+ requesterType: 'user',
1541
+ targetMemberId: reviewer.id,
1542
+ targetKind: 'WORKSPACE_COMMIT',
1543
+ targetPurpose: 'REVIEW',
1544
+ targetSourceWorkspaceId: childWorkspace.id,
1545
+ targetHeadSha: 'child-head-sha',
1546
+ targetBranchName: childWorkspace.branchName,
1547
+ triggerMessageId: await createRoomMessageId(teamRun.id),
1548
+ instruction: 'review target',
1549
+ status: 'STARTED',
1550
+ },
1551
+ });
1552
+ const invocation = await prisma.agentInvocation.create({
1553
+ data: {
1554
+ teamRunId: teamRun.id,
1555
+ workRequestId: request.id,
1556
+ memberId: reviewer.id,
1557
+ workspaceId: executionWorkspace.id,
1558
+ targetKind: 'WORKSPACE_COMMIT',
1559
+ targetPurpose: 'REVIEW',
1560
+ targetSourceWorkspaceId: childWorkspace.id,
1561
+ targetHeadSha: 'child-head-sha',
1562
+ targetBranchName: childWorkspace.branchName,
1563
+ targetSyncStatus: 'SYNCED',
1564
+ status: 'RUNNING',
1565
+ },
1566
+ });
1567
+ const identity = await service.resolveInvocationMemberForWorkspace(childWorkspace.id, invocation.id);
1568
+ await expect(service.recordVerdict(childWorkspace.id, {
1569
+ kind: 'REVIEW',
1570
+ verdict: 'APPROVED',
1571
+ reviewedSha: 'different-sha',
1572
+ reviewerMemberId: identity.memberId,
1573
+ expectedTargetHeadSha: identity.targetHeadSha,
1574
+ })).rejects.toMatchObject({
1575
+ code: 'TARGET_VERDICT_SHA_MISMATCH',
1576
+ statusCode: 409,
1577
+ });
1578
+ });
1579
+ it('rejects workspace verdicts when the submitted SHA does not match HEAD', async () => {
1580
+ const { childWorkspace, reviewer } = await createTeamRunChildMergeFixture();
1581
+ await expect(service.recordVerdict(childWorkspace.id, {
1582
+ kind: 'REVIEW',
1583
+ verdict: 'APPROVED',
1584
+ reviewedSha: 'old-sha',
1585
+ reviewerMemberId: reviewer.id,
1586
+ })).rejects.toMatchObject({
1587
+ code: 'WORKSPACE_VERDICT_SHA_MISMATCH',
1588
+ statusCode: 409,
1589
+ });
1590
+ });
1591
+ it('rejects workspace verdicts when the current member lacks the required capability', async () => {
1592
+ const { teamRun, childWorkspace } = await createTeamRunChildMergeFixture();
1593
+ const memberWithoutReview = await addTeamMember(teamRun.id, {
1594
+ name: 'No Review Capability',
1595
+ capabilities: { runCommands: true },
1596
+ });
1597
+ const memberWithoutTest = await addTeamMember(teamRun.id, {
1598
+ name: 'No Test Capability',
1599
+ capabilities: { readDiff: true },
1600
+ });
1601
+ await expect(service.recordVerdict(childWorkspace.id, {
1602
+ kind: 'REVIEW',
1603
+ verdict: 'APPROVED',
1604
+ reviewedSha: 'child-head-sha',
1605
+ reviewerMemberId: memberWithoutReview.id,
1606
+ })).rejects.toMatchObject({
1607
+ code: 'TEAM_RUN_MEMBER_CAPABILITY_REQUIRED',
1608
+ statusCode: 403,
1609
+ });
1610
+ await expect(service.recordVerdict(childWorkspace.id, {
1611
+ kind: 'TEST',
1612
+ verdict: 'PASSED',
1613
+ reviewedSha: 'child-head-sha',
1614
+ reviewerMemberId: memberWithoutTest.id,
1615
+ })).rejects.toMatchObject({
1616
+ code: 'TEAM_RUN_MEMBER_CAPABILITY_REQUIRED',
1617
+ statusCode: 403,
1618
+ });
1619
+ });
632
1620
  it('rejects final TeamRun main workspace merge while dedicated children are not final', async () => {
633
1621
  const { task, teamRun, member } = await createTeamRunWithMember();
634
1622
  await prisma.task.update({