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
@@ -11,12 +11,24 @@ import { promisify } from 'node:util';
11
11
  import { randomUUID } from 'node:crypto';
12
12
  import fs from 'node:fs/promises';
13
13
  import path from 'node:path';
14
- import { ensureProjectIsMutable, ensureProjectSupportsGit, hasGitMetadata } from './project-guards.js';
14
+ import { ensureProjectIsMutable, ensureProjectSupportsWorktrees, hasGitMetadata, } from './project-guards.js';
15
15
  import { ensureTaskNotDeleted } from './deleted-task-guard.js';
16
16
  import { isMainDirectoryWorkspace, isWorktreeWorkspace, } from './workspace-kind.js';
17
17
  const DEFAULT_IDLE_THRESHOLD_HOURS = 24;
18
18
  const WORKSPACE_READY_RETRY_COUNT = 20;
19
19
  const WORKSPACE_READY_RETRY_DELAY_MS = 50;
20
+ const DEFAULT_TEAM_MEMBER_CAPABILITIES = {
21
+ readRoom: false,
22
+ postRoomMessage: false,
23
+ mentionMembers: false,
24
+ stopMemberWork: false,
25
+ markReadyForReview: false,
26
+ readFiles: false,
27
+ writeFiles: false,
28
+ runCommands: false,
29
+ readDiff: false,
30
+ mergeWorkspace: false,
31
+ };
20
32
  const execAsync = promisify(exec);
21
33
  /** 过滤条件:只返回用户可见的 CHAT session,且不在 workspace 热路径携带 prompt/logSnapshot。 */
22
34
  const visibleSessionsFilter = {
@@ -35,6 +47,7 @@ const visibleSessionsFilter = {
35
47
  },
36
48
  };
37
49
  const activeSessionStatuses = [SessionStatus.PENDING, SessionStatus.RUNNING];
50
+ const activeInvocationStatuses = ['QUEUED', 'RUNNING', 'SESSION_ENDED', 'WAITING_ROOM_REPLY'];
38
51
  const finalChildWorkspaceStatuses = [WorkspaceStatus.MERGED, WorkspaceStatus.ABANDONED];
39
52
  function normalizeCreateOptions(input) {
40
53
  if (typeof input === 'string') {
@@ -76,6 +89,14 @@ function branchFromOptions(workspaceId, options) {
76
89
  function teamRunBranchPrefix(teamRunId) {
77
90
  return `at/team/${teamRunId.slice(0, 8)}`;
78
91
  }
92
+ function targetExecutionBranchName(input) {
93
+ return `${teamRunBranchPrefix(input.teamRunId)}/target/${input.targetPurpose.toLowerCase()}-${input.memberId.slice(0, 8)}-${input.targetHeadSha.slice(0, 12)}`;
94
+ }
95
+ function assertFullCommitSha(value) {
96
+ if (!/^[0-9a-f]{40}$/i.test(value)) {
97
+ throw new ServiceError('targetHeadSha must be a full 40-character commit SHA', 'TARGET_HEAD_SHA_INVALID', 400);
98
+ }
99
+ }
79
100
  function sleep(ms) {
80
101
  return new Promise((resolve) => setTimeout(resolve, ms));
81
102
  }
@@ -91,6 +112,70 @@ function mainDirectoryGitStatus() {
91
112
  untrackedCount: 0,
92
113
  };
93
114
  }
115
+ function toIso(date) {
116
+ return date.toISOString();
117
+ }
118
+ function serializeWorkspaceVerdict(verdict) {
119
+ return {
120
+ id: verdict.id,
121
+ workspaceId: verdict.workspaceId,
122
+ teamRunId: verdict.teamRunId,
123
+ kind: verdict.kind,
124
+ verdict: verdict.verdict,
125
+ reviewedSha: verdict.reviewedSha,
126
+ reviewerMemberId: verdict.reviewerMemberId,
127
+ reason: verdict.reason,
128
+ sequence: verdict.sequence,
129
+ createdAt: toIso(verdict.createdAt),
130
+ };
131
+ }
132
+ function parseCapabilities(value) {
133
+ if (!value) {
134
+ return DEFAULT_TEAM_MEMBER_CAPABILITIES;
135
+ }
136
+ try {
137
+ return {
138
+ ...DEFAULT_TEAM_MEMBER_CAPABILITIES,
139
+ ...JSON.parse(value),
140
+ };
141
+ }
142
+ catch {
143
+ return DEFAULT_TEAM_MEMBER_CAPABILITIES;
144
+ }
145
+ }
146
+ function blocker(code, severity, message, details) {
147
+ return details
148
+ ? { code, severity, message, details }
149
+ : { code, severity, message };
150
+ }
151
+ function serializeLatestVerdict(verdict, currentHeadSha, ownerMemberId) {
152
+ if (!verdict) {
153
+ return null;
154
+ }
155
+ return {
156
+ id: verdict.id,
157
+ verdict: verdict.verdict,
158
+ reviewedSha: verdict.reviewedSha,
159
+ reviewerMemberId: verdict.reviewerMemberId,
160
+ reason: verdict.reason,
161
+ sequence: verdict.sequence,
162
+ createdAt: toIso(verdict.createdAt),
163
+ matchesHead: currentHeadSha !== null && verdict.reviewedSha === currentHeadSha,
164
+ isSelfReview: verdict.reviewerMemberId === ownerMemberId,
165
+ };
166
+ }
167
+ function resultSummary(results, requested = results.length) {
168
+ return {
169
+ requested,
170
+ considered: results.length,
171
+ merged: results.filter((item) => item.status === 'MERGED').length,
172
+ alreadyMerged: results.filter((item) => item.status === 'ALREADY_MERGED').length,
173
+ wouldMerge: results.filter((item) => item.status === 'WOULD_MERGE').length,
174
+ skipped: results.filter((item) => item.status === 'SKIPPED').length,
175
+ conflicts: results.filter((item) => item.status === 'CONFLICT').length,
176
+ failed: results.filter((item) => item.status === 'FAILED').length,
177
+ };
178
+ }
94
179
  export class WorkspaceService {
95
180
  lockService;
96
181
  static mainWorkspaceClaims = new Map();
@@ -138,6 +223,593 @@ export class WorkspaceService {
138
223
  orderBy: { createdAt: 'desc' },
139
224
  });
140
225
  }
226
+ async listVerdicts(workspaceId) {
227
+ const workspace = await prisma.workspace.findUnique({
228
+ where: { id: workspaceId },
229
+ include: { task: true },
230
+ });
231
+ if (!workspace) {
232
+ throw new NotFoundError('Workspace', workspaceId);
233
+ }
234
+ ensureTaskNotDeleted(workspace.task);
235
+ const verdicts = await prisma.workspaceVerdict.findMany({
236
+ where: { workspaceId },
237
+ orderBy: { createdAt: 'desc' },
238
+ });
239
+ return verdicts.map(serializeWorkspaceVerdict);
240
+ }
241
+ async listTeamRunMergeableWorkspaces(teamRunId) {
242
+ const context = await this.loadTeamRunMergeableContext(teamRunId);
243
+ return this.buildMergeableWorkspacesResponse(context);
244
+ }
245
+ async mergeTeamRunMembers(teamRunId, options) {
246
+ await this.assertTeamRunMergeInvocation(teamRunId, options.invocationId, options.requesterMemberId);
247
+ const initialContext = await this.loadTeamRunMergeableContext(teamRunId);
248
+ ensureProjectIsMutable(initialContext.project, 'merge TeamRun member workspaces');
249
+ const dryRun = options.dryRun === true;
250
+ const stopOnConflict = options.stopOnConflict === true;
251
+ const hasExplicitWorkspaceIds = Array.isArray(options.workspaceIds);
252
+ const requestedWorkspaceIds = hasExplicitWorkspaceIds
253
+ ? [...new Set(options.workspaceIds)]
254
+ : undefined;
255
+ const requestedCount = requestedWorkspaceIds?.length ?? initialContext.workspaces.length;
256
+ const run = async () => {
257
+ const context = await this.loadTeamRunMergeableContext(teamRunId);
258
+ const response = await this.buildMergeableWorkspacesResponse(context);
259
+ const results = [];
260
+ const selected = [];
261
+ if (requestedWorkspaceIds) {
262
+ const itemsById = new Map(response.workspaces.map((item) => [item.workspaceId, item]));
263
+ for (const workspaceId of requestedWorkspaceIds) {
264
+ const item = itemsById.get(workspaceId);
265
+ if (item) {
266
+ selected.push(item);
267
+ }
268
+ else {
269
+ results.push({
270
+ workspaceId,
271
+ ownerMemberId: null,
272
+ status: 'SKIPPED',
273
+ code: 'INVALID_WORKSPACE_STATE',
274
+ message: 'Workspace is not a mergeable dedicated TeamRun child workspace',
275
+ });
276
+ }
277
+ }
278
+ }
279
+ else {
280
+ selected.push(...response.workspaces);
281
+ }
282
+ for (const item of selected) {
283
+ if (item.status === WorkspaceStatus.MERGED) {
284
+ results.push({
285
+ workspaceId: item.workspaceId,
286
+ ownerMemberId: item.owner.memberId,
287
+ status: dryRun ? 'ALREADY_MERGED' : 'ALREADY_MERGED',
288
+ sha: item.headSha ?? undefined,
289
+ });
290
+ continue;
291
+ }
292
+ if (!item.mergeReady) {
293
+ const firstBlocking = item.blockers.find((entry) => entry.severity === 'BLOCKING') ?? item.blockers[0];
294
+ results.push({
295
+ workspaceId: item.workspaceId,
296
+ ownerMemberId: item.owner.memberId,
297
+ status: 'SKIPPED',
298
+ code: firstBlocking?.code ?? 'WORKSPACE_NOT_ACTIVE',
299
+ message: firstBlocking?.message ?? 'Workspace is not ready to merge',
300
+ blockers: item.blockers,
301
+ });
302
+ continue;
303
+ }
304
+ if (dryRun) {
305
+ results.push({
306
+ workspaceId: item.workspaceId,
307
+ ownerMemberId: item.owner.memberId,
308
+ status: 'WOULD_MERGE',
309
+ sha: item.headSha ?? undefined,
310
+ });
311
+ continue;
312
+ }
313
+ try {
314
+ const sha = await this.merge(item.workspaceId, {
315
+ invocationId: options.invocationId,
316
+ lockOwnerId: options.invocationId,
317
+ requesterMemberId: options.requesterMemberId,
318
+ });
319
+ results.push({
320
+ workspaceId: item.workspaceId,
321
+ ownerMemberId: item.owner.memberId,
322
+ status: 'MERGED',
323
+ sha,
324
+ });
325
+ }
326
+ catch (error) {
327
+ const result = this.mergeErrorToResult(item, error);
328
+ results.push(result);
329
+ if (stopOnConflict && result.status === 'CONFLICT') {
330
+ break;
331
+ }
332
+ }
333
+ }
334
+ return {
335
+ teamRunId: response.teamRunId,
336
+ taskId: response.taskId,
337
+ projectId: response.projectId,
338
+ mainWorkspaceId: response.mainWorkspace.id,
339
+ dryRun,
340
+ stopOnConflict,
341
+ requestedWorkspaceIds,
342
+ summary: resultSummary(results, requestedCount),
343
+ results,
344
+ };
345
+ };
346
+ if (dryRun) {
347
+ return run();
348
+ }
349
+ return this.withProjectMergeLock(initialContext.projectId, options.invocationId, run);
350
+ }
351
+ async recordVerdict(workspaceId, input) {
352
+ if (input.kind === 'REVIEW' && !['APPROVED', 'CHANGES_REQUESTED'].includes(input.verdict)) {
353
+ throw new ServiceError('Invalid review verdict', 'INVALID_WORKSPACE_VERDICT', 400);
354
+ }
355
+ if (input.kind === 'TEST' && !['PASSED', 'FAILED'].includes(input.verdict)) {
356
+ throw new ServiceError('Invalid test verdict', 'INVALID_WORKSPACE_VERDICT', 400);
357
+ }
358
+ if (!input.reviewedSha.trim()) {
359
+ throw new ServiceError('reviewedSha is required', 'VALIDATION_ERROR', 400);
360
+ }
361
+ const workspace = await prisma.workspace.findUnique({
362
+ where: { id: workspaceId },
363
+ include: { task: { include: { teamRun: true } } },
364
+ });
365
+ if (!workspace) {
366
+ throw new NotFoundError('Workspace', workspaceId);
367
+ }
368
+ ensureTaskNotDeleted(workspace.task);
369
+ const teamRun = workspace.task.teamRun;
370
+ if (!teamRun) {
371
+ throw new ServiceError('Workspace verdicts are only supported for TeamRun workspaces', 'WORKSPACE_VERDICT_TEAM_RUN_REQUIRED', 400);
372
+ }
373
+ this.assertWorktreeWorkspace(workspace);
374
+ const reviewedSha = input.reviewedSha.trim();
375
+ const currentHeadSha = await this.getHeadSha(workspace.worktreePath);
376
+ const expectedTargetHeadSha = input.expectedTargetHeadSha?.trim() || null;
377
+ if (expectedTargetHeadSha && currentHeadSha !== expectedTargetHeadSha) {
378
+ throw new ServiceError('Targeted workspace verdict is stale because the source workspace HEAD changed', 'TARGET_VERDICT_STALE', 409);
379
+ }
380
+ if (expectedTargetHeadSha && reviewedSha !== expectedTargetHeadSha) {
381
+ throw new ServiceError('Targeted workspace verdict SHA must match the invocation targetHeadSha', 'TARGET_VERDICT_SHA_MISMATCH', 409);
382
+ }
383
+ if (reviewedSha !== currentHeadSha) {
384
+ throw new ServiceError('Workspace verdict SHA must match the current workspace HEAD', 'WORKSPACE_VERDICT_SHA_MISMATCH', 409);
385
+ }
386
+ const reviewer = await prisma.teamMember.findFirst({
387
+ where: {
388
+ id: input.reviewerMemberId,
389
+ teamRunId: teamRun.id,
390
+ membershipStatus: { not: 'REMOVED' },
391
+ },
392
+ select: { id: true, capabilities: true },
393
+ });
394
+ if (!reviewer) {
395
+ throw new ServiceError('Reviewer is not an active member of this TeamRun', 'WORKSPACE_VERDICT_REVIEWER_INVALID', 403);
396
+ }
397
+ const reviewerCapabilities = parseCapabilities(reviewer.capabilities);
398
+ const requiredCapability = input.kind === 'REVIEW' ? 'readDiff' : 'runCommands';
399
+ if (reviewerCapabilities[requiredCapability] !== true) {
400
+ throw new ServiceError(`Current TeamRun member lacks required capabilities: ${requiredCapability}`, 'TEAM_RUN_MEMBER_CAPABILITY_REQUIRED', 403);
401
+ }
402
+ const verdict = await prisma.$transaction(async (tx) => {
403
+ const latest = await tx.workspaceVerdict.findFirst({
404
+ where: {
405
+ workspaceId,
406
+ kind: input.kind,
407
+ },
408
+ orderBy: { sequence: 'desc' },
409
+ select: { sequence: true },
410
+ });
411
+ return tx.workspaceVerdict.create({
412
+ data: {
413
+ workspaceId,
414
+ teamRunId: teamRun.id,
415
+ kind: input.kind,
416
+ verdict: input.verdict,
417
+ reviewedSha,
418
+ reviewerMemberId: input.reviewerMemberId,
419
+ reason: input.reason ?? null,
420
+ sequence: (latest?.sequence ?? 0) + 1,
421
+ },
422
+ });
423
+ });
424
+ return serializeWorkspaceVerdict(verdict);
425
+ }
426
+ async resolveInvocationMemberForWorkspace(workspaceId, invocationId) {
427
+ if (!invocationId) {
428
+ return null;
429
+ }
430
+ const workspace = await prisma.workspace.findUnique({
431
+ where: { id: workspaceId },
432
+ include: { task: { include: { teamRun: true } } },
433
+ });
434
+ if (!workspace) {
435
+ throw new NotFoundError('Workspace', workspaceId);
436
+ }
437
+ ensureTaskNotDeleted(workspace.task);
438
+ const teamRunId = workspace.task.teamRun?.id;
439
+ if (!teamRunId) {
440
+ return null;
441
+ }
442
+ const invocation = await prisma.agentInvocation.findFirst({
443
+ where: {
444
+ id: invocationId,
445
+ teamRunId,
446
+ OR: [
447
+ { workspaceId },
448
+ { targetSourceWorkspaceId: workspaceId },
449
+ ],
450
+ },
451
+ select: {
452
+ id: true,
453
+ memberId: true,
454
+ targetHeadSha: true,
455
+ targetSourceWorkspaceId: true,
456
+ },
457
+ });
458
+ return invocation
459
+ ? {
460
+ teamRunId,
461
+ memberId: invocation.memberId,
462
+ invocationId: invocation.id,
463
+ targetHeadSha: invocation.targetSourceWorkspaceId === workspaceId
464
+ ? invocation.targetHeadSha
465
+ : null,
466
+ targetSourceWorkspaceId: invocation.targetSourceWorkspaceId,
467
+ }
468
+ : null;
469
+ }
470
+ async resolveInvocationMemberForTeamRun(teamRunId, invocationId) {
471
+ if (!invocationId) {
472
+ return null;
473
+ }
474
+ const invocation = await prisma.agentInvocation.findFirst({
475
+ where: {
476
+ id: invocationId,
477
+ teamRunId,
478
+ },
479
+ select: {
480
+ id: true,
481
+ memberId: true,
482
+ },
483
+ });
484
+ return invocation
485
+ ? { teamRunId, memberId: invocation.memberId, invocationId: invocation.id }
486
+ : null;
487
+ }
488
+ async assertTeamRunMergeInvocation(teamRunId, invocationId, requesterMemberId) {
489
+ if (!invocationId) {
490
+ throw new ServiceError('TeamRun member merge requires an agent invocation identity', 'TEAM_RUN_MERGE_INVOCATION_REQUIRED', 403);
491
+ }
492
+ const identity = await this.resolveInvocationMemberForTeamRun(teamRunId, invocationId);
493
+ if (!identity) {
494
+ throw new ServiceError('Agent invocation identity is invalid for this TeamRun', 'FORBIDDEN', 403);
495
+ }
496
+ if (requesterMemberId && requesterMemberId !== identity.memberId) {
497
+ throw new ServiceError('Requester member does not match the agent invocation identity', 'FORBIDDEN', 403);
498
+ }
499
+ const member = await prisma.teamMember.findFirst({
500
+ where: {
501
+ id: identity.memberId,
502
+ teamRunId,
503
+ membershipStatus: { not: 'REMOVED' },
504
+ },
505
+ select: {
506
+ capabilities: true,
507
+ },
508
+ });
509
+ if (!member) {
510
+ throw new ServiceError('Current TeamRun member was not found', 'FORBIDDEN', 403);
511
+ }
512
+ const capabilities = parseCapabilities(member.capabilities);
513
+ if (capabilities.mergeWorkspace !== true) {
514
+ throw new ServiceError('Current TeamRun member lacks required capabilities: mergeWorkspace', 'TEAM_RUN_MEMBER_CAPABILITY_REQUIRED', 403);
515
+ }
516
+ }
517
+ async loadTeamRunMergeableContext(teamRunId) {
518
+ const teamRun = await prisma.teamRun.findUnique({
519
+ where: { id: teamRunId },
520
+ include: {
521
+ task: { include: { project: true } },
522
+ mainWorkspace: true,
523
+ },
524
+ });
525
+ if (!teamRun) {
526
+ throw new NotFoundError('TeamRun', teamRunId);
527
+ }
528
+ ensureTaskNotDeleted(teamRun.task);
529
+ const mainWorkspace = teamRun.mainWorkspace;
530
+ const workspaces = mainWorkspace
531
+ ? await prisma.workspace.findMany({
532
+ where: {
533
+ taskId: teamRun.taskId,
534
+ parentWorkspaceId: mainWorkspace.id,
535
+ ownerMemberId: { not: null },
536
+ },
537
+ include: {
538
+ ownerMember: true,
539
+ task: { include: { project: true, teamRun: true } },
540
+ },
541
+ orderBy: [{ createdAt: 'asc' }, { id: 'asc' }],
542
+ })
543
+ : [];
544
+ const dedicatedWorkspaces = workspaces.filter((workspace) => {
545
+ return Boolean(workspace.ownerMember
546
+ && workspace.ownerMemberId
547
+ && workspace.parentWorkspaceId
548
+ && workspace.task.teamRun);
549
+ });
550
+ const workspaceIds = dedicatedWorkspaces.map((workspace) => workspace.id);
551
+ const ownerMemberIds = dedicatedWorkspaces.map((workspace) => workspace.ownerMemberId);
552
+ const [verdicts, ownerInvocations, parentActiveWriteSession, mainHeadSha] = await Promise.all([
553
+ workspaceIds.length > 0
554
+ ? prisma.workspaceVerdict.findMany({
555
+ where: {
556
+ teamRunId,
557
+ workspaceId: { in: workspaceIds },
558
+ kind: { in: ['REVIEW', 'TEST'] },
559
+ },
560
+ orderBy: [
561
+ { workspaceId: 'asc' },
562
+ { kind: 'asc' },
563
+ { sequence: 'desc' },
564
+ { createdAt: 'desc' },
565
+ { id: 'desc' },
566
+ ],
567
+ })
568
+ : Promise.resolve([]),
569
+ ownerMemberIds.length > 0
570
+ ? prisma.agentInvocation.findMany({
571
+ where: {
572
+ teamRunId,
573
+ memberId: { in: ownerMemberIds },
574
+ status: { in: activeInvocationStatuses },
575
+ },
576
+ select: { memberId: true },
577
+ })
578
+ : Promise.resolve([]),
579
+ mainWorkspace
580
+ ? prisma.session.findFirst({
581
+ where: {
582
+ workspaceId: mainWorkspace.id,
583
+ status: { in: activeSessionStatuses },
584
+ purpose: SessionPurpose.CHAT,
585
+ },
586
+ select: { id: true },
587
+ })
588
+ : Promise.resolve(null),
589
+ mainWorkspace?.worktreePath
590
+ ? this.getHeadSha(mainWorkspace.worktreePath).catch(() => null)
591
+ : Promise.resolve(null),
592
+ ]);
593
+ const latestReviews = new Map();
594
+ const latestTests = new Map();
595
+ for (const verdict of verdicts) {
596
+ const key = verdict.workspaceId;
597
+ if (verdict.kind === 'REVIEW' && !latestReviews.has(key)) {
598
+ latestReviews.set(key, verdict);
599
+ }
600
+ if (verdict.kind === 'TEST' && !latestTests.has(key)) {
601
+ latestTests.set(key, verdict);
602
+ }
603
+ }
604
+ return {
605
+ teamRunId,
606
+ taskId: teamRun.taskId,
607
+ projectId: teamRun.task.projectId,
608
+ project: {
609
+ name: teamRun.task.project.name,
610
+ repoPath: teamRun.task.project.repoPath,
611
+ mainBranch: teamRun.task.project.mainBranch,
612
+ archivedAt: teamRun.task.project.archivedAt,
613
+ repoDeletedAt: teamRun.task.project.repoDeletedAt,
614
+ },
615
+ mainWorkspace: {
616
+ id: mainWorkspace?.id ?? null,
617
+ branchName: mainWorkspace?.branchName ?? null,
618
+ status: mainWorkspace?.status ?? null,
619
+ worktreePath: mainWorkspace?.worktreePath ?? null,
620
+ headSha: mainHeadSha,
621
+ hasActiveWriteSession: Boolean(parentActiveWriteSession),
622
+ },
623
+ generatedAt: new Date().toISOString(),
624
+ workspaces: dedicatedWorkspaces,
625
+ latestReviews,
626
+ latestTests,
627
+ ownerActiveMemberIds: new Set(ownerInvocations.map((invocation) => invocation.memberId)),
628
+ };
629
+ }
630
+ async buildMergeableWorkspacesResponse(context) {
631
+ const workspaceOrder = new Map(context.workspaces.map((workspace, index) => [workspace.id, index]));
632
+ const items = (await Promise.all(context.workspaces.map((workspace) => this.buildMergeableWorkspaceItem(context, workspace))))
633
+ .sort((left, right) => {
634
+ const leftBehind = left.git.behindMain ?? Number.MAX_SAFE_INTEGER;
635
+ const rightBehind = right.git.behindMain ?? Number.MAX_SAFE_INTEGER;
636
+ if (leftBehind !== rightBehind)
637
+ return leftBehind - rightBehind;
638
+ const leftOrder = workspaceOrder.get(left.workspaceId) ?? Number.MAX_SAFE_INTEGER;
639
+ const rightOrder = workspaceOrder.get(right.workspaceId) ?? Number.MAX_SAFE_INTEGER;
640
+ if (leftOrder !== rightOrder)
641
+ return leftOrder - rightOrder;
642
+ return left.workspaceId.localeCompare(right.workspaceId);
643
+ });
644
+ return {
645
+ teamRunId: context.teamRunId,
646
+ taskId: context.taskId,
647
+ projectId: context.projectId,
648
+ mainWorkspace: {
649
+ id: context.mainWorkspace.id,
650
+ branchName: context.mainWorkspace.branchName,
651
+ status: context.mainWorkspace.status,
652
+ headSha: context.mainWorkspace.headSha,
653
+ hasActiveWriteSession: context.mainWorkspace.hasActiveWriteSession,
654
+ },
655
+ generatedAt: context.generatedAt,
656
+ workspaces: items,
657
+ };
658
+ }
659
+ async buildMergeableWorkspaceItem(context, workspace) {
660
+ const blockers = [];
661
+ let headSha = null;
662
+ let gitStatus = null;
663
+ let gitStatusAvailable = false;
664
+ const worktreeManager = new WorktreeManager(context.project.repoPath);
665
+ const isWorktree = workspace.workspaceKind === WorkspaceKind.WORKTREE;
666
+ if (!isWorktree) {
667
+ blockers.push(blocker('WORKSPACE_GIT_UNAVAILABLE', 'BLOCKING', 'Workspace git operations are unavailable for this workspace type'));
668
+ }
669
+ else if (!workspace.worktreePath) {
670
+ blockers.push(blocker('MISSING_HEAD_SHA', 'BLOCKING', 'Workspace worktree path is missing'));
671
+ }
672
+ else {
673
+ try {
674
+ headSha = await this.getHeadSha(workspace.worktreePath);
675
+ }
676
+ catch {
677
+ blockers.push(blocker('MISSING_HEAD_SHA', 'BLOCKING', 'Cannot read workspace HEAD SHA'));
678
+ }
679
+ try {
680
+ gitStatus = await worktreeManager.getGitOperationStatus(workspace.worktreePath, context.mainWorkspace.branchName ?? workspace.baseBranch ?? context.project.mainBranch);
681
+ gitStatusAvailable = true;
682
+ }
683
+ catch (error) {
684
+ blockers.push(blocker('GIT_STATUS_UNAVAILABLE', 'BLOCKING', error instanceof Error ? error.message : 'Cannot read workspace git status'));
685
+ }
686
+ }
687
+ const latestReview = serializeLatestVerdict(context.latestReviews.get(workspace.id), headSha, workspace.ownerMemberId);
688
+ const latestTest = serializeLatestVerdict(context.latestTests.get(workspace.id), headSha, workspace.ownerMemberId);
689
+ const ownerHasActiveInvocation = context.ownerActiveMemberIds.has(workspace.ownerMemberId);
690
+ const clean = gitStatusAvailable && gitStatus
691
+ ? gitStatus.operation === 'idle'
692
+ && !gitStatus.hasUncommittedChanges
693
+ && gitStatus.untrackedCount === 0
694
+ : null;
695
+ if (workspace.status === WorkspaceStatus.MERGED) {
696
+ blockers.push(blocker('WORKSPACE_ALREADY_MERGED', 'WARNING', 'Workspace has already been merged'));
697
+ }
698
+ else if (workspace.status === WorkspaceStatus.ABANDONED) {
699
+ blockers.push(blocker('WORKSPACE_ABANDONED', 'BLOCKING', 'Workspace has been abandoned'));
700
+ }
701
+ else if (workspace.status === WorkspaceStatus.HIBERNATED) {
702
+ blockers.push(blocker('WORKSPACE_HIBERNATED', 'BLOCKING', 'Workspace is hibernated'));
703
+ }
704
+ else if (workspace.status !== WorkspaceStatus.ACTIVE) {
705
+ blockers.push(blocker('INVALID_WORKSPACE_STATE', 'BLOCKING', `Workspace is in ${workspace.status} status`));
706
+ }
707
+ if (context.mainWorkspace.id !== workspace.parentWorkspaceId) {
708
+ blockers.push(blocker('INVALID_PARENT_WORKSPACE', 'BLOCKING', 'Workspace parent is not the TeamRun main workspace'));
709
+ }
710
+ if (context.mainWorkspace.status !== WorkspaceStatus.ACTIVE) {
711
+ blockers.push(blocker('INVALID_PARENT_WORKSPACE_STATE', 'BLOCKING', `TeamRun main workspace is in ${context.mainWorkspace.status ?? 'missing'} status`));
712
+ }
713
+ if (workspace.status === WorkspaceStatus.ACTIVE) {
714
+ if (!latestReview || latestReview.verdict !== 'APPROVED') {
715
+ blockers.push(blocker('REVIEW_REQUIRED', 'BLOCKING', 'TeamRun workspace merge requires an approved review'));
716
+ }
717
+ else if (!latestReview.matchesHead) {
718
+ blockers.push(blocker('REVIEW_STALE', 'BLOCKING', 'Approved review is stale because the workspace HEAD changed'));
719
+ }
720
+ else if (!latestReview.reviewerMemberId || latestReview.isSelfReview) {
721
+ blockers.push(blocker('SELF_REVIEW_FORBIDDEN', 'BLOCKING', 'Workspace owner cannot approve their own workspace for merge'));
722
+ }
723
+ if (ownerHasActiveInvocation) {
724
+ blockers.push(blocker('OWNER_HAS_ACTIVE_INVOCATION', 'BLOCKING', 'Cannot merge while the workspace owner has active work'));
725
+ }
726
+ if (context.mainWorkspace.hasActiveWriteSession) {
727
+ blockers.push(blocker('PARENT_WORKSPACE_HAS_ACTIVE_SESSION', 'BLOCKING', 'Cannot merge into parent workspace while it has an active write session'));
728
+ }
729
+ if (gitStatus?.operation === 'rebase') {
730
+ blockers.push(blocker('REBASE_IN_PROGRESS', 'BLOCKING', 'Workspace has a rebase in progress'));
731
+ }
732
+ if (gitStatus?.operation === 'merge') {
733
+ blockers.push(blocker('MERGE_CONFLICT', 'BLOCKING', 'Workspace has a merge in progress', {
734
+ conflictedFiles: gitStatus.conflictedFiles,
735
+ }));
736
+ }
737
+ if (gitStatus && (gitStatus.hasUncommittedChanges || gitStatus.untrackedCount > 0)) {
738
+ blockers.push(blocker('WORKTREE_DIRTY', 'BLOCKING', 'Workspace has uncommitted or untracked changes', {
739
+ uncommittedCount: gitStatus.uncommittedCount,
740
+ untrackedCount: gitStatus.untrackedCount,
741
+ }));
742
+ }
743
+ if ((gitStatus?.behind ?? 0) > 0) {
744
+ blockers.push(blocker('BEHIND_MAIN', 'WARNING', 'Workspace branch is behind the TeamRun main workspace', {
745
+ behindMain: gitStatus?.behind ?? 0,
746
+ aheadOfMain: gitStatus?.ahead ?? 0,
747
+ }));
748
+ }
749
+ }
750
+ return {
751
+ workspaceId: workspace.id,
752
+ owner: {
753
+ memberId: workspace.ownerMemberId,
754
+ name: workspace.ownerMember.name,
755
+ membershipStatus: workspace.ownerMember.membershipStatus,
756
+ },
757
+ status: workspace.status,
758
+ branchName: workspace.branchName,
759
+ baseBranch: workspace.baseBranch,
760
+ parentWorkspaceId: workspace.parentWorkspaceId,
761
+ headSha,
762
+ git: {
763
+ clean,
764
+ aheadOfMain: gitStatus?.ahead ?? null,
765
+ behindMain: gitStatus?.behind ?? null,
766
+ operation: gitStatus?.operation ?? null,
767
+ conflictedFiles: gitStatus?.conflictedFiles ?? [],
768
+ hasUncommittedChanges: gitStatus?.hasUncommittedChanges ?? null,
769
+ uncommittedCount: gitStatus?.uncommittedCount ?? null,
770
+ untrackedCount: gitStatus?.untrackedCount ?? null,
771
+ statusAvailable: gitStatusAvailable,
772
+ },
773
+ activity: {
774
+ ownerHasActiveInvocation,
775
+ parentHasActiveWriteSession: context.mainWorkspace.hasActiveWriteSession,
776
+ },
777
+ latestReview,
778
+ latestTest,
779
+ mergeReady: workspace.status === WorkspaceStatus.ACTIVE
780
+ && blockers.every((entry) => entry.severity !== 'BLOCKING'),
781
+ blockers,
782
+ };
783
+ }
784
+ mergeErrorToResult(item, error) {
785
+ const errorCode = typeof error === 'object' && error !== null && 'code' in error
786
+ ? String(error.code ?? '')
787
+ : '';
788
+ if (error instanceof MergeConflictError || errorCode === 'MERGE_CONFLICT') {
789
+ const conflict = error;
790
+ return {
791
+ workspaceId: item.workspaceId,
792
+ ownerMemberId: item.owner.memberId,
793
+ status: 'CONFLICT',
794
+ code: errorCode || 'MERGE_CONFLICT',
795
+ message: error instanceof Error ? error.message : 'Merge conflict',
796
+ conflictedFiles: Array.isArray(conflict.conflictedFiles) ? conflict.conflictedFiles : [],
797
+ sourceBranch: conflict.sourceBranch,
798
+ targetBranch: conflict.targetBranch,
799
+ sourceWorkspaceId: conflict.sourceWorkspaceId,
800
+ targetWorkspaceId: conflict.targetWorkspaceId,
801
+ };
802
+ }
803
+ const code = errorCode || 'UNKNOWN';
804
+ const message = error instanceof Error ? error.message : String(error);
805
+ return {
806
+ workspaceId: item.workspaceId,
807
+ ownerMemberId: item.owner.memberId,
808
+ status: 'FAILED',
809
+ code,
810
+ message,
811
+ };
812
+ }
141
813
  // ── Create ───────────────────────────────────────────────────────────────────
142
814
  /**
143
815
  * 创建 Workspace
@@ -162,7 +834,7 @@ export class WorkspaceService {
162
834
  if (options.workspaceKind === WorkspaceKind.MAIN_DIRECTORY) {
163
835
  return this.createMainDirectoryWorkspace(taskId, task, options);
164
836
  }
165
- ensureProjectSupportsGit(task.project, 'create a worktree workspace');
837
+ await ensureProjectSupportsWorktrees(task.project, 'create a worktree workspace');
166
838
  const worktreeManager = new WorktreeManager(task.project.repoPath);
167
839
  // 查找可复用的 MERGED 或 HIBERNATED workspace
168
840
  if (!options.branchName && options.reuseInactive) {
@@ -326,7 +998,7 @@ export class WorkspaceService {
326
998
  }
327
999
  ensureTaskNotDeleted(teamRun.task);
328
1000
  ensureProjectIsMutable(teamRun.task.project, 'create workspaces');
329
- ensureProjectSupportsGit(teamRun.task.project, 'create TeamRun workspaces');
1001
+ await ensureProjectSupportsWorktrees(teamRun.task.project, 'create TeamRun workspaces');
330
1002
  if (teamRun.mainWorkspace
331
1003
  && teamRun.mainWorkspace.taskId === teamRun.taskId
332
1004
  && teamRun.mainWorkspace.parentWorkspaceId == null
@@ -382,7 +1054,7 @@ export class WorkspaceService {
382
1054
  throw new NotFoundError('TeamMember', memberId);
383
1055
  }
384
1056
  ensureProjectIsMutable(teamRun.task.project, 'create workspaces');
385
- ensureProjectSupportsGit(teamRun.task.project, 'create TeamRun workspaces');
1057
+ await ensureProjectSupportsWorktrees(teamRun.task.project, 'create TeamRun workspaces');
386
1058
  const existing = await this.findDedicatedWorkspace(mainWorkspace.id, memberId);
387
1059
  if (existing) {
388
1060
  return this.activateDedicatedWorkspace(existing);
@@ -407,6 +1079,71 @@ export class WorkspaceService {
407
1079
  return this.activateDedicatedWorkspace(await this.waitForWorkspaceReady(raced));
408
1080
  }
409
1081
  }
1082
+ async prepareTargetedExecutionWorkspace(input) {
1083
+ assertFullCommitSha(input.targetHeadSha);
1084
+ const [teamRun, sourceWorkspace, executionWorkspace] = await Promise.all([
1085
+ prisma.teamRun.findUnique({
1086
+ where: { id: input.teamRunId },
1087
+ include: { task: { include: { project: true } }, mainWorkspace: true },
1088
+ }),
1089
+ prisma.workspace.findFirst({
1090
+ where: {
1091
+ id: input.targetSourceWorkspaceId,
1092
+ task: { teamRun: { id: input.teamRunId } },
1093
+ },
1094
+ include: { task: { include: { project: true } } },
1095
+ }),
1096
+ prisma.workspace.findFirst({
1097
+ where: {
1098
+ id: input.executionWorkspaceId,
1099
+ ownerMemberId: input.memberId,
1100
+ task: { teamRun: { id: input.teamRunId } },
1101
+ },
1102
+ include: { task: { include: { project: true } } },
1103
+ }),
1104
+ ]);
1105
+ if (!teamRun) {
1106
+ throw new NotFoundError('TeamRun', input.teamRunId);
1107
+ }
1108
+ ensureTaskNotDeleted(teamRun.task);
1109
+ await ensureProjectSupportsWorktrees(teamRun.task.project, 'prepare targeted TeamRun workspace');
1110
+ if (!sourceWorkspace) {
1111
+ throw new ServiceError('Target source workspace must belong to this TeamRun', 'TARGET_SOURCE_WORKSPACE_INVALID', 400);
1112
+ }
1113
+ if (!executionWorkspace) {
1114
+ throw new ServiceError('Execution workspace must be a dedicated workspace owned by the target member', 'TARGET_EXECUTION_WORKSPACE_INVALID', 400);
1115
+ }
1116
+ this.assertWorktreeWorkspace(sourceWorkspace);
1117
+ this.assertWorktreeWorkspace(executionWorkspace);
1118
+ if (!teamRun.mainWorkspaceId || executionWorkspace.parentWorkspaceId !== teamRun.mainWorkspaceId) {
1119
+ throw new ServiceError('Targeted review/test requires a dedicated TeamRun execution workspace', 'TARGET_EXECUTION_WORKSPACE_NOT_DEDICATED', 400);
1120
+ }
1121
+ if (executionWorkspace.id === sourceWorkspace.id) {
1122
+ throw new ServiceError('Execution workspace cannot be the source workspace being reviewed or tested', 'TARGET_EXECUTION_WORKSPACE_SOURCE_CONFLICT', 400);
1123
+ }
1124
+ const repoPath = teamRun.task.project.repoPath;
1125
+ await execGit(repoPath, ['cat-file', '-e', `${input.targetHeadSha}^{commit}`]);
1126
+ const clean = await new WorktreeManager(repoPath).isWorktreeClean(executionWorkspace.worktreePath);
1127
+ if (!clean) {
1128
+ throw new ServiceError('Execution workspace has uncommitted changes; targeted sync was not applied', 'TARGET_EXECUTION_WORKSPACE_DIRTY', 409);
1129
+ }
1130
+ const executionBranch = targetExecutionBranchName(input);
1131
+ await execGit(executionWorkspace.worktreePath, ['checkout', '-B', executionBranch, input.targetHeadSha]);
1132
+ await execGit(executionWorkspace.worktreePath, ['reset', '--hard', input.targetHeadSha]);
1133
+ const actualHead = (await execGit(executionWorkspace.worktreePath, ['rev-parse', 'HEAD'])).trim();
1134
+ if (actualHead !== input.targetHeadSha) {
1135
+ throw new ServiceError('Execution workspace HEAD does not match targetHeadSha after sync', 'TARGET_SYNC_HEAD_MISMATCH', 500);
1136
+ }
1137
+ await prisma.workspace.update({
1138
+ where: { id: executionWorkspace.id },
1139
+ data: {
1140
+ branchName: executionBranch,
1141
+ baseBranch: input.targetBranchName,
1142
+ },
1143
+ });
1144
+ this.refreshGitWatcher(executionWorkspace.id);
1145
+ return { executionBranch };
1146
+ }
410
1147
  async findDedicatedWorkspace(mainWorkspaceId, memberId) {
411
1148
  return prisma.workspace.findFirst({
412
1149
  where: {
@@ -625,16 +1362,112 @@ export class WorkspaceService {
625
1362
  }
626
1363
  ensureProjectIsMutable(workspace.task.project, 'merge workspaces');
627
1364
  this.assertWorktreeWorkspace(workspace);
628
- return this.withProjectMergeLock(workspace.task.projectId, options.lockOwnerId, () => this.mergeWithLock(workspace, options.commitMessage));
1365
+ return this.withProjectMergeLock(workspace.task.projectId, options.lockOwnerId, () => this.mergeWithLock(workspace, options));
629
1366
  }
630
- async mergeWithLock(workspace, commitMessage) {
1367
+ async mergeWithLock(workspace, options) {
1368
+ if (this.isTeamRunDedicatedChildWorkspace(workspace) && workspace.status === WorkspaceStatus.MERGED) {
1369
+ if (!workspace.worktreePath) {
1370
+ return '';
1371
+ }
1372
+ try {
1373
+ return await this.getHeadSha(workspace.worktreePath);
1374
+ }
1375
+ catch {
1376
+ return '';
1377
+ }
1378
+ }
631
1379
  if (workspace.status !== WorkspaceStatus.ACTIVE) {
632
1380
  throw new ServiceError(`Cannot merge workspace in ${workspace.status} status`, 'INVALID_WORKSPACE_STATE', 400);
633
1381
  }
634
1382
  if (workspace.parentWorkspaceId) {
635
- return this.mergeChildIntoParent(workspace, commitMessage);
1383
+ if (this.isTeamRunDedicatedChildWorkspace(workspace)) {
1384
+ await this.assertTeamRunChildMergeGate(workspace, options);
1385
+ }
1386
+ return this.mergeChildIntoParent(workspace, options.commitMessage);
1387
+ }
1388
+ return this.mergeRootWorkspaceToMain(workspace, options.commitMessage);
1389
+ }
1390
+ isTeamRunDedicatedChildWorkspace(workspace) {
1391
+ return Boolean(workspace.task.teamRun
1392
+ && workspace.task.teamRun.mainWorkspaceId === workspace.parentWorkspaceId
1393
+ && workspace.parentWorkspaceId
1394
+ && workspace.ownerMemberId);
1395
+ }
1396
+ async getHeadSha(worktreePath) {
1397
+ return (await execGit(worktreePath, ['rev-parse', 'HEAD'])).trim();
1398
+ }
1399
+ async assertTeamRunChildMergeGate(workspace, options) {
1400
+ const invocationId = options.invocationId ?? options.lockOwnerId;
1401
+ if (!invocationId) {
1402
+ throw new ServiceError('TeamRun workspace merge requires an agent invocation identity', 'TEAM_RUN_MERGE_INVOCATION_REQUIRED', 403);
1403
+ }
1404
+ const invocation = await prisma.agentInvocation.findFirst({
1405
+ where: {
1406
+ id: invocationId,
1407
+ teamRunId: workspace.task.teamRun.id,
1408
+ },
1409
+ select: {
1410
+ id: true,
1411
+ memberId: true,
1412
+ },
1413
+ });
1414
+ if (!invocation) {
1415
+ throw new ServiceError('Agent invocation identity is invalid for this TeamRun', 'FORBIDDEN', 403);
1416
+ }
1417
+ if (options.requesterMemberId && options.requesterMemberId !== invocation.memberId) {
1418
+ throw new ServiceError('Requester member does not match the agent invocation identity', 'FORBIDDEN', 403);
1419
+ }
1420
+ const member = await prisma.teamMember.findFirst({
1421
+ where: {
1422
+ id: invocation.memberId,
1423
+ teamRunId: workspace.task.teamRun.id,
1424
+ membershipStatus: { not: 'REMOVED' },
1425
+ },
1426
+ select: {
1427
+ id: true,
1428
+ capabilities: true,
1429
+ },
1430
+ });
1431
+ if (!member) {
1432
+ throw new ServiceError('Current TeamRun member was not found', 'FORBIDDEN', 403);
1433
+ }
1434
+ const capabilities = parseCapabilities(member.capabilities);
1435
+ if (capabilities.mergeWorkspace !== true) {
1436
+ throw new ServiceError('Current TeamRun member lacks required capabilities: mergeWorkspace', 'TEAM_RUN_MEMBER_CAPABILITY_REQUIRED', 403);
1437
+ }
1438
+ const currentHeadSha = await this.getHeadSha(workspace.worktreePath);
1439
+ const latestReview = await prisma.workspaceVerdict.findFirst({
1440
+ where: {
1441
+ workspaceId: workspace.id,
1442
+ teamRunId: workspace.task.teamRun.id,
1443
+ kind: 'REVIEW',
1444
+ },
1445
+ orderBy: [
1446
+ { sequence: 'desc' },
1447
+ { createdAt: 'desc' },
1448
+ { id: 'desc' },
1449
+ ],
1450
+ });
1451
+ if (!latestReview || latestReview.verdict !== 'APPROVED') {
1452
+ throw new ServiceError('TeamRun workspace merge requires an approved review', 'REVIEW_REQUIRED', 409);
1453
+ }
1454
+ if (latestReview.reviewedSha !== currentHeadSha) {
1455
+ throw new ServiceError('Approved review is stale because the workspace HEAD changed', 'REVIEW_STALE', 409);
1456
+ }
1457
+ if (!latestReview.reviewerMemberId || latestReview.reviewerMemberId === workspace.ownerMemberId) {
1458
+ throw new ServiceError('Workspace owner cannot approve their own workspace for merge', 'SELF_REVIEW_FORBIDDEN', 409);
1459
+ }
1460
+ const ownerActiveInvocation = await prisma.agentInvocation.findFirst({
1461
+ where: {
1462
+ teamRunId: workspace.task.teamRun.id,
1463
+ memberId: workspace.ownerMemberId,
1464
+ status: { in: ['QUEUED', 'RUNNING', 'SESSION_ENDED', 'WAITING_ROOM_REPLY'] },
1465
+ },
1466
+ select: { id: true },
1467
+ });
1468
+ if (ownerActiveInvocation) {
1469
+ throw new ServiceError('Cannot merge while the workspace owner has active work', 'OWNER_HAS_ACTIVE_INVOCATION', 409);
636
1470
  }
637
- return this.mergeRootWorkspaceToMain(workspace, commitMessage);
638
1471
  }
639
1472
  async mergeChildIntoParent(workspace, commitMessage) {
640
1473
  this.assertWorktreeWorkspace(workspace);