agent-tower 0.5.3 → 0.5.4-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/dist/mcp/http-client.d.ts +31 -1
  2. package/dist/mcp/http-client.d.ts.map +1 -1
  3. package/dist/mcp/http-client.js +14 -2
  4. package/dist/mcp/http-client.js.map +1 -1
  5. package/dist/mcp/server.d.ts.map +1 -1
  6. package/dist/mcp/server.js +62 -8
  7. package/dist/mcp/server.js.map +1 -1
  8. package/dist/mcp/tools/__tests__/workspaces.test.d.ts +2 -0
  9. package/dist/mcp/tools/__tests__/workspaces.test.d.ts.map +1 -0
  10. package/dist/mcp/tools/__tests__/workspaces.test.js +72 -0
  11. package/dist/mcp/tools/__tests__/workspaces.test.js.map +1 -0
  12. package/dist/mcp/tools/workspaces.d.ts +9 -1
  13. package/dist/mcp/tools/workspaces.d.ts.map +1 -1
  14. package/dist/mcp/tools/workspaces.js +96 -2
  15. package/dist/mcp/tools/workspaces.js.map +1 -1
  16. package/dist/mcp/types.d.ts +46 -0
  17. package/dist/mcp/types.d.ts.map +1 -1
  18. package/dist/mcp/types.js +23 -0
  19. package/dist/mcp/types.js.map +1 -1
  20. package/dist/routes/__tests__/team-runs-merge.test.d.ts +2 -0
  21. package/dist/routes/__tests__/team-runs-merge.test.d.ts.map +1 -0
  22. package/dist/routes/__tests__/team-runs-merge.test.js +156 -0
  23. package/dist/routes/__tests__/team-runs-merge.test.js.map +1 -0
  24. package/dist/routes/__tests__/team-runs-private-messages.test.d.ts +2 -0
  25. package/dist/routes/__tests__/team-runs-private-messages.test.d.ts.map +1 -0
  26. package/dist/routes/__tests__/team-runs-private-messages.test.js +82 -0
  27. package/dist/routes/__tests__/team-runs-private-messages.test.js.map +1 -0
  28. package/dist/routes/team-runs.d.ts +2 -0
  29. package/dist/routes/team-runs.d.ts.map +1 -1
  30. package/dist/routes/team-runs.js +48 -1
  31. package/dist/routes/team-runs.js.map +1 -1
  32. package/dist/routes/workspaces.d.ts.map +1 -1
  33. package/dist/routes/workspaces.js +43 -4
  34. package/dist/routes/workspaces.js.map +1 -1
  35. package/dist/services/__tests__/session-manager.team-run.test.js +75 -0
  36. package/dist/services/__tests__/session-manager.team-run.test.js.map +1 -1
  37. package/dist/services/__tests__/team-reconciler.service.test.js +127 -8
  38. package/dist/services/__tests__/team-reconciler.service.test.js.map +1 -1
  39. package/dist/services/__tests__/team-run.service.test.js +172 -0
  40. package/dist/services/__tests__/team-run.service.test.js.map +1 -1
  41. package/dist/services/__tests__/team-scheduler.service.test.js +397 -0
  42. package/dist/services/__tests__/team-scheduler.service.test.js.map +1 -1
  43. package/dist/services/__tests__/workspace.service.test.js +959 -3
  44. package/dist/services/__tests__/workspace.service.test.js.map +1 -1
  45. package/dist/services/session-manager.d.ts.map +1 -1
  46. package/dist/services/session-manager.js +16 -0
  47. package/dist/services/session-manager.js.map +1 -1
  48. package/dist/services/team-run.service.d.ts +5 -1
  49. package/dist/services/team-run.service.d.ts.map +1 -1
  50. package/dist/services/team-run.service.js +63 -2
  51. package/dist/services/team-run.service.js.map +1 -1
  52. package/dist/services/team-scheduler.service.d.ts +21 -1
  53. package/dist/services/team-scheduler.service.d.ts.map +1 -1
  54. package/dist/services/team-scheduler.service.js +230 -8
  55. package/dist/services/team-scheduler.service.js.map +1 -1
  56. package/dist/services/workspace.service.d.ts +56 -1
  57. package/dist/services/workspace.service.d.ts.map +1 -1
  58. package/dist/services/workspace.service.js +837 -4
  59. package/dist/services/workspace.service.js.map +1 -1
  60. package/node_modules/@agent-tower/shared/dist/types.d.ts +168 -0
  61. package/node_modules/@agent-tower/shared/dist/types.d.ts.map +1 -1
  62. package/node_modules/@agent-tower/shared/dist/types.js.map +1 -1
  63. package/node_modules/@prisma/client/.prisma/client/edge.js +39 -5
  64. package/node_modules/@prisma/client/.prisma/client/index-browser.js +34 -0
  65. package/node_modules/@prisma/client/.prisma/client/index.d.ts +2871 -158
  66. package/node_modules/@prisma/client/.prisma/client/index.js +39 -5
  67. package/node_modules/@prisma/client/.prisma/client/package.json +1 -1
  68. package/node_modules/@prisma/client/.prisma/client/schema.prisma +81 -30
  69. package/node_modules/@prisma/client/.prisma/client/wasm.js +34 -0
  70. package/package.json +1 -1
  71. package/prisma/migrations/20260618010000_add_workspace_verdicts/migration.sql +27 -0
  72. package/prisma/migrations/20260702000000_add_teamrun_target_commit_fields/migration.sql +38 -0
  73. package/prisma/schema.prisma +51 -0
@@ -41,6 +41,20 @@ const commandCapabilities = {
41
41
  function stringifyJson(value) {
42
42
  return JSON.stringify(value);
43
43
  }
44
+ function git(cwd, args) {
45
+ return execFileSync('git', args, { cwd, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'] });
46
+ }
47
+ function initGitRepo(repoPath) {
48
+ fs.mkdirSync(repoPath, { recursive: true });
49
+ git(repoPath, ['init']);
50
+ git(repoPath, ['checkout', '-B', 'main']);
51
+ git(repoPath, ['config', 'user.email', 'test@example.com']);
52
+ git(repoPath, ['config', 'user.name', 'Test User']);
53
+ fs.writeFileSync(path.join(repoPath, 'README.md'), '# test\n');
54
+ git(repoPath, ['add', 'README.md']);
55
+ git(repoPath, ['commit', '-m', 'initial commit']);
56
+ return git(repoPath, ['rev-parse', 'HEAD']).trim();
57
+ }
44
58
  async function createTask(title = 'Team scheduler task', status = TaskStatus.TODO) {
45
59
  const project = await prisma.project.create({
46
60
  data: {
@@ -103,6 +117,7 @@ async function createWorkRequest(options) {
103
117
  requesterMemberId: null,
104
118
  requesterType: 'user',
105
119
  targetMemberId: options.targetMemberId,
120
+ ...(options.target ?? {}),
106
121
  triggerMessageId: options.triggerMessageId ?? `message-${Math.random().toString(16).slice(2)}`,
107
122
  instruction: options.instruction ?? 'Please do the work',
108
123
  ifBusy: options.ifBusy ?? 'queue',
@@ -1001,6 +1016,308 @@ describe('TeamSchedulerService', () => {
1001
1016
  members[1].id,
1002
1017
  ]));
1003
1018
  });
1019
+ it('marks targeted review/test requests for non-dedicated members as failed', async () => {
1020
+ const { workspace: sourceWorkspace, teamRun, members } = await createTeamRunFixture({
1021
+ memberCapabilities: [commandCapabilities],
1022
+ workspacePolicies: ['shared'],
1023
+ });
1024
+ const request = await createWorkRequest({
1025
+ teamRunId: teamRun.id,
1026
+ targetMemberId: members[0].id,
1027
+ target: {
1028
+ targetKind: 'WORKSPACE_COMMIT',
1029
+ targetPurpose: 'REVIEW',
1030
+ targetSourceWorkspaceId: sourceWorkspace.id,
1031
+ targetHeadSha: 'a'.repeat(40),
1032
+ targetBranchName: sourceWorkspace.branchName,
1033
+ },
1034
+ });
1035
+ const sessionManager = createSessionManagerMock();
1036
+ service = new TeamSchedulerService(lockService, {
1037
+ workspaceService: createWorkspaceServiceMock(),
1038
+ sessionManager,
1039
+ getProviderById: createProviderLookup(),
1040
+ });
1041
+ await expect(service.planNext(teamRun.id)).resolves.toEqual([
1042
+ expect.objectContaining({
1043
+ workRequestId: request.id,
1044
+ canStart: false,
1045
+ blockedReason: 'unsupported_workspace_policy',
1046
+ }),
1047
+ ]);
1048
+ const invocations = await service.startNextSessions(teamRun.id);
1049
+ expect(invocations).toEqual([]);
1050
+ expect(sessionManager.create).not.toHaveBeenCalled();
1051
+ const invocation = await prisma.agentInvocation.findFirst({ where: { workRequestId: request.id } });
1052
+ expect(invocation).toMatchObject({
1053
+ status: 'FAILED',
1054
+ targetSyncError: expect.stringContaining('workspacePolicy=dedicated'),
1055
+ targetKind: 'WORKSPACE_COMMIT',
1056
+ targetPurpose: 'REVIEW',
1057
+ });
1058
+ expect(invocation?.targetSyncError).toContain('workspacePolicy=shared');
1059
+ await expect(prisma.roomMessage.findFirst({ where: { senderInvocationId: invocation?.id ?? '' } })).resolves.toMatchObject({
1060
+ senderType: 'system',
1061
+ kind: 'system',
1062
+ content: expect.stringContaining('change this TeamMember instance to workspacePolicy=dedicated'),
1063
+ workRequestIds: JSON.stringify([request.id]),
1064
+ });
1065
+ await expect(prisma.workRequest.findUnique({ where: { id: request.id } })).resolves.toMatchObject({
1066
+ status: 'STARTED',
1067
+ });
1068
+ });
1069
+ it('syncs a targeted dedicated execution workspace to the target commit before starting', async () => {
1070
+ const repoPath = path.join(testDir, 'target-sync-success-repo');
1071
+ const targetSha = initGitRepo(repoPath);
1072
+ const { project, task } = await createTask('Target sync success task');
1073
+ await prisma.project.update({ where: { id: project.id }, data: { repoPath } });
1074
+ const teamRun = await prisma.teamRun.create({ data: { taskId: task.id, mode: 'AUTO' } });
1075
+ const mainWorkspace = await prisma.workspace.create({
1076
+ data: {
1077
+ taskId: task.id,
1078
+ branchName: 'main',
1079
+ baseBranch: 'main',
1080
+ worktreePath: repoPath,
1081
+ workingDir: repoPath,
1082
+ status: 'ACTIVE',
1083
+ },
1084
+ });
1085
+ await prisma.teamRun.update({ where: { id: teamRun.id }, data: { mainWorkspaceId: mainWorkspace.id } });
1086
+ const sourceWorkspace = await prisma.workspace.create({
1087
+ data: {
1088
+ taskId: task.id,
1089
+ parentWorkspaceId: mainWorkspace.id,
1090
+ ownerMemberId: null,
1091
+ branchName: 'source-delivery',
1092
+ baseBranch: 'main',
1093
+ worktreePath: repoPath,
1094
+ workingDir: repoPath,
1095
+ status: 'ACTIVE',
1096
+ },
1097
+ });
1098
+ const member = await prisma.teamMember.create({
1099
+ data: {
1100
+ teamRunId: teamRun.id,
1101
+ presetId: null,
1102
+ name: 'Tester',
1103
+ aliases: stringifyJson(['tester']),
1104
+ providerId: 'provider-1',
1105
+ rolePrompt: 'Test role',
1106
+ capabilities: stringifyJson(commandCapabilities),
1107
+ workspacePolicy: 'dedicated',
1108
+ triggerPolicy: 'MENTION_ONLY',
1109
+ sessionPolicy: 'new_per_request',
1110
+ queueManagementPolicy: 'own_only',
1111
+ avatar: null,
1112
+ },
1113
+ });
1114
+ const request = await createWorkRequest({
1115
+ teamRunId: teamRun.id,
1116
+ targetMemberId: member.id,
1117
+ target: {
1118
+ targetKind: 'WORKSPACE_COMMIT',
1119
+ targetPurpose: 'TEST',
1120
+ targetSourceWorkspaceId: sourceWorkspace.id,
1121
+ targetHeadSha: targetSha,
1122
+ targetBranchName: sourceWorkspace.branchName,
1123
+ targetPlanItemId: 'plan-sync',
1124
+ },
1125
+ });
1126
+ const sessionManager = createSessionManagerMock();
1127
+ service = new TeamSchedulerService(lockService, {
1128
+ sessionManager,
1129
+ getProviderById: createProviderLookup(),
1130
+ });
1131
+ const invocations = await service.startNextSessions(teamRun.id);
1132
+ expect(invocations).toHaveLength(1);
1133
+ const invocation = await prisma.agentInvocation.findUniqueOrThrow({ where: { id: invocations[0].id } });
1134
+ expect(invocation).toMatchObject({
1135
+ workRequestId: request.id,
1136
+ targetKind: 'WORKSPACE_COMMIT',
1137
+ targetPurpose: 'TEST',
1138
+ targetSourceWorkspaceId: sourceWorkspace.id,
1139
+ targetHeadSha: targetSha,
1140
+ targetSyncStatus: 'SYNCED',
1141
+ targetSyncError: null,
1142
+ targetPlanItemId: 'plan-sync',
1143
+ targetPort: expect.any(Number),
1144
+ targetVitePort: expect.any(Number),
1145
+ targetE2EPort: expect.any(Number),
1146
+ });
1147
+ expect(invocation.targetExecutionBranch).toBe(`at/team/${teamRun.id.slice(0, 8)}/target/test-${member.id.slice(0, 8)}-${targetSha.slice(0, 12)}`);
1148
+ const executionWorkspace = await prisma.workspace.findUniqueOrThrow({ where: { id: invocation.workspaceId } });
1149
+ expect(executionWorkspace.branchName).toBe(invocation.targetExecutionBranch);
1150
+ expect(git(executionWorkspace.worktreePath, ['rev-parse', 'HEAD']).trim()).toBe(targetSha);
1151
+ expect(sessionManager.create).toHaveBeenCalledWith(executionWorkspace.id, AgentType.CODEX, buildExpectedSessionPrompt('Test role', [
1152
+ 'Target sync success task',
1153
+ [
1154
+ 'Target commit handoff:',
1155
+ '- purpose: TEST',
1156
+ `- sourceWorkspaceId: ${sourceWorkspace.id}`,
1157
+ `- targetHeadSha: ${targetSha}`,
1158
+ `- sourceBranch: ${sourceWorkspace.branchName}`,
1159
+ '- planItemId: plan-sync',
1160
+ '- The execution workspace is synced to targetHeadSha before this session starts.',
1161
+ '- Record review/test verdicts against sourceWorkspaceId with reviewed_sha=targetHeadSha.',
1162
+ ].join('\n'),
1163
+ 'Work request summary:\nPlease do the work',
1164
+ ].join('\n\n')), 'DEFAULT', member.providerId);
1165
+ }, 15_000);
1166
+ it('fails targeted sync when the execution workspace is dirty', async () => {
1167
+ const repoPath = path.join(testDir, 'target-sync-dirty-repo');
1168
+ const targetSha = initGitRepo(repoPath);
1169
+ const { project, task } = await createTask('Target sync dirty task');
1170
+ await prisma.project.update({ where: { id: project.id }, data: { repoPath } });
1171
+ const teamRun = await prisma.teamRun.create({ data: { taskId: task.id, mode: 'AUTO' } });
1172
+ const mainWorkspace = await prisma.workspace.create({
1173
+ data: {
1174
+ taskId: task.id,
1175
+ branchName: 'main',
1176
+ baseBranch: 'main',
1177
+ worktreePath: repoPath,
1178
+ workingDir: repoPath,
1179
+ status: 'ACTIVE',
1180
+ },
1181
+ });
1182
+ await prisma.teamRun.update({ where: { id: teamRun.id }, data: { mainWorkspaceId: mainWorkspace.id } });
1183
+ const sourceWorkspace = await prisma.workspace.create({
1184
+ data: {
1185
+ taskId: task.id,
1186
+ parentWorkspaceId: mainWorkspace.id,
1187
+ ownerMemberId: null,
1188
+ branchName: 'source-delivery-dirty',
1189
+ baseBranch: 'main',
1190
+ worktreePath: repoPath,
1191
+ workingDir: repoPath,
1192
+ status: 'ACTIVE',
1193
+ },
1194
+ });
1195
+ const member = await prisma.teamMember.create({
1196
+ data: {
1197
+ teamRunId: teamRun.id,
1198
+ presetId: null,
1199
+ name: 'Reviewer',
1200
+ aliases: stringifyJson(['reviewer']),
1201
+ providerId: 'provider-1',
1202
+ rolePrompt: 'Review role',
1203
+ capabilities: stringifyJson(commandCapabilities),
1204
+ workspacePolicy: 'dedicated',
1205
+ triggerPolicy: 'MENTION_ONLY',
1206
+ sessionPolicy: 'new_per_request',
1207
+ queueManagementPolicy: 'own_only',
1208
+ avatar: null,
1209
+ },
1210
+ });
1211
+ const executionWorkspace = await prisma.workspace.create({
1212
+ data: {
1213
+ taskId: task.id,
1214
+ parentWorkspaceId: mainWorkspace.id,
1215
+ ownerMemberId: member.id,
1216
+ branchName: `at/team/${teamRun.id.slice(0, 8)}/member-${member.id.slice(0, 8)}`,
1217
+ baseBranch: mainWorkspace.branchName,
1218
+ worktreePath: await import('../../git/worktree.manager.js').then(async ({ WorktreeManager }) => {
1219
+ const manager = new WorktreeManager(repoPath);
1220
+ return manager.create(`at/team/${teamRun.id.slice(0, 8)}/member-${member.id.slice(0, 8)}`);
1221
+ }),
1222
+ status: 'ACTIVE',
1223
+ },
1224
+ });
1225
+ fs.writeFileSync(path.join(executionWorkspace.worktreePath, 'dirty.txt'), 'dirty');
1226
+ const request = await createWorkRequest({
1227
+ teamRunId: teamRun.id,
1228
+ targetMemberId: member.id,
1229
+ target: {
1230
+ targetKind: 'WORKSPACE_COMMIT',
1231
+ targetPurpose: 'REVIEW',
1232
+ targetSourceWorkspaceId: sourceWorkspace.id,
1233
+ targetHeadSha: targetSha,
1234
+ targetBranchName: sourceWorkspace.branchName,
1235
+ },
1236
+ });
1237
+ const sessionManager = createSessionManagerMock();
1238
+ service = new TeamSchedulerService(lockService, {
1239
+ sessionManager,
1240
+ getProviderById: createProviderLookup(),
1241
+ });
1242
+ const invocations = await service.startNextSessions(teamRun.id);
1243
+ expect(invocations).toEqual([]);
1244
+ expect(sessionManager.create).not.toHaveBeenCalled();
1245
+ await expect(prisma.agentInvocation.findFirst({ where: { workRequestId: request.id } })).resolves.toMatchObject({
1246
+ workspaceId: executionWorkspace.id,
1247
+ status: 'FAILED',
1248
+ targetSyncStatus: 'FAILED',
1249
+ targetSyncError: expect.stringContaining('Execution workspace has uncommitted changes'),
1250
+ });
1251
+ }, 15_000);
1252
+ it('fails targeted sync when the target commit does not exist', async () => {
1253
+ const repoPath = path.join(testDir, 'target-sync-missing-sha-repo');
1254
+ initGitRepo(repoPath);
1255
+ const { project, task } = await createTask('Target sync missing sha task');
1256
+ await prisma.project.update({ where: { id: project.id }, data: { repoPath } });
1257
+ const teamRun = await prisma.teamRun.create({ data: { taskId: task.id, mode: 'AUTO' } });
1258
+ const mainWorkspace = await prisma.workspace.create({
1259
+ data: {
1260
+ taskId: task.id,
1261
+ branchName: 'main',
1262
+ baseBranch: 'main',
1263
+ worktreePath: repoPath,
1264
+ workingDir: repoPath,
1265
+ status: 'ACTIVE',
1266
+ },
1267
+ });
1268
+ await prisma.teamRun.update({ where: { id: teamRun.id }, data: { mainWorkspaceId: mainWorkspace.id } });
1269
+ const sourceWorkspace = await prisma.workspace.create({
1270
+ data: {
1271
+ taskId: task.id,
1272
+ parentWorkspaceId: mainWorkspace.id,
1273
+ ownerMemberId: null,
1274
+ branchName: 'source-delivery-missing',
1275
+ baseBranch: 'main',
1276
+ worktreePath: repoPath,
1277
+ workingDir: repoPath,
1278
+ status: 'ACTIVE',
1279
+ },
1280
+ });
1281
+ const member = await prisma.teamMember.create({
1282
+ data: {
1283
+ teamRunId: teamRun.id,
1284
+ presetId: null,
1285
+ name: 'Reviewer',
1286
+ aliases: stringifyJson(['reviewer']),
1287
+ providerId: 'provider-1',
1288
+ rolePrompt: 'Review role',
1289
+ capabilities: stringifyJson(commandCapabilities),
1290
+ workspacePolicy: 'dedicated',
1291
+ triggerPolicy: 'MENTION_ONLY',
1292
+ sessionPolicy: 'new_per_request',
1293
+ queueManagementPolicy: 'own_only',
1294
+ avatar: null,
1295
+ },
1296
+ });
1297
+ const request = await createWorkRequest({
1298
+ teamRunId: teamRun.id,
1299
+ targetMemberId: member.id,
1300
+ target: {
1301
+ targetKind: 'WORKSPACE_COMMIT',
1302
+ targetPurpose: 'REVIEW',
1303
+ targetSourceWorkspaceId: sourceWorkspace.id,
1304
+ targetHeadSha: 'f'.repeat(40),
1305
+ targetBranchName: sourceWorkspace.branchName,
1306
+ },
1307
+ });
1308
+ const sessionManager = createSessionManagerMock();
1309
+ service = new TeamSchedulerService(lockService, {
1310
+ sessionManager,
1311
+ getProviderById: createProviderLookup(),
1312
+ });
1313
+ await expect(service.startNextSessions(teamRun.id)).resolves.toEqual([]);
1314
+ expect(sessionManager.create).not.toHaveBeenCalled();
1315
+ await expect(prisma.agentInvocation.findFirst({ where: { workRequestId: request.id } })).resolves.toMatchObject({
1316
+ status: 'FAILED',
1317
+ targetSyncStatus: 'FAILED',
1318
+ targetSyncError: expect.stringContaining('git cat-file -e'),
1319
+ });
1320
+ }, 15_000);
1004
1321
  it('records the dedicated child workspace for queued no-session invocations', async () => {
1005
1322
  const { workspace: mainWorkspace, teamRun, members } = await createTeamRunFixture({
1006
1323
  memberCapabilities: [writeCapabilities],
@@ -1299,6 +1616,86 @@ describe('TeamSchedulerService', () => {
1299
1616
  status: 'RUNNING',
1300
1617
  });
1301
1618
  });
1619
+ it('does not resume targeted requests across different targetHeadSha values', async () => {
1620
+ const { workspace, teamRun, members } = await createTeamRunFixture({
1621
+ sessionPolicies: ['resume_last'],
1622
+ });
1623
+ const previousRequest = await createWorkRequest({
1624
+ teamRunId: teamRun.id,
1625
+ targetMemberId: members[0].id,
1626
+ status: 'STARTED',
1627
+ target: {
1628
+ targetKind: 'WORKSPACE_COMMIT',
1629
+ targetPurpose: 'REVIEW',
1630
+ targetSourceWorkspaceId: workspace.id,
1631
+ targetHeadSha: '1'.repeat(40),
1632
+ targetBranchName: workspace.branchName,
1633
+ },
1634
+ });
1635
+ const previousSession = await prisma.session.create({
1636
+ data: {
1637
+ workspaceId: workspace.id,
1638
+ agentType: AgentType.CODEX,
1639
+ providerId: members[0].providerId,
1640
+ prompt: 'previous prompt',
1641
+ status: 'COMPLETED',
1642
+ logSnapshot: JSON.stringify({ sessionId: 'agent-native-session-previous-target', entries: [] }),
1643
+ },
1644
+ });
1645
+ await prisma.agentInvocation.create({
1646
+ data: {
1647
+ teamRunId: teamRun.id,
1648
+ workRequestId: previousRequest.id,
1649
+ memberId: members[0].id,
1650
+ workspaceId: workspace.id,
1651
+ sessionId: previousSession.id,
1652
+ targetKind: 'WORKSPACE_COMMIT',
1653
+ targetPurpose: 'REVIEW',
1654
+ targetSourceWorkspaceId: workspace.id,
1655
+ targetHeadSha: '1'.repeat(40),
1656
+ targetBranchName: workspace.branchName,
1657
+ targetSyncStatus: 'SYNCED',
1658
+ status: 'COMPLETED',
1659
+ },
1660
+ });
1661
+ const nextRequest = await createWorkRequest({
1662
+ teamRunId: teamRun.id,
1663
+ targetMemberId: members[0].id,
1664
+ instruction: 'Review a different target',
1665
+ target: {
1666
+ targetKind: 'WORKSPACE_COMMIT',
1667
+ targetPurpose: 'REVIEW',
1668
+ targetSourceWorkspaceId: workspace.id,
1669
+ targetHeadSha: '2'.repeat(40),
1670
+ targetBranchName: workspace.branchName,
1671
+ },
1672
+ });
1673
+ const sessionManager = createSessionManagerMock();
1674
+ service = new TeamSchedulerService(lockService, {
1675
+ workspaceService: {
1676
+ create: vi.fn(),
1677
+ getOrCreateDedicatedWorkspace: vi.fn(async () => workspace),
1678
+ prepareTargetedExecutionWorkspace: vi.fn(async () => ({
1679
+ executionBranch: 'target-review-branch',
1680
+ })),
1681
+ },
1682
+ sessionManager,
1683
+ getProviderById: createProviderLookup(),
1684
+ });
1685
+ await prisma.teamMember.update({
1686
+ where: { id: members[0].id },
1687
+ data: { workspacePolicy: 'dedicated' },
1688
+ });
1689
+ const invocations = await service.startNextSessions(teamRun.id);
1690
+ expect(invocations).toHaveLength(1);
1691
+ expect(invocations[0]).toMatchObject({
1692
+ workRequestId: nextRequest.id,
1693
+ targetHeadSha: '2'.repeat(40),
1694
+ targetSyncStatus: 'SYNCED',
1695
+ });
1696
+ expect(sessionManager.start).toHaveBeenCalledWith(invocations[0].sessionId);
1697
+ expect(sessionManager.startFollowUp).not.toHaveBeenCalled();
1698
+ });
1302
1699
  it('starts new_per_request members without resuming previous native context', async () => {
1303
1700
  const { workspace, teamRun, members } = await createTeamRunFixture({
1304
1701
  sessionPolicies: ['new_per_request'],