@akira-tl/forgerelay 0.6.1 → 0.6.2

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 (31) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +38 -0
  3. package/dist/activity/mcp-query-tools.js +1 -0
  4. package/dist/activity/query-service.js +1 -0
  5. package/dist/composite-activity.js +155 -0
  6. package/dist/composite-workspaces.js +197 -0
  7. package/dist/remote-workspace-relay.js +16 -0
  8. package/dist/server.js +673 -144
  9. package/dist/ui/.vite/manifest.json +33 -33
  10. package/dist/ui/activity-panel-app.html +3 -3
  11. package/dist/ui/assets/{activity-panel-app-CjZVvVNc.js → activity-panel-app-E1ju2dqI.js} +1 -1
  12. package/dist/ui/assets/{heavy-payload-vGgBRvNX.js → heavy-payload-CeW-n9w5.js} +1 -1
  13. package/dist/ui/assets/{review-payload-4erWKckt.js → review-payload-B9CO298v.js} +1 -1
  14. package/dist/ui/assets/{scrollbar-CaOPzUJd.js → scrollbar-C2twAENW.js} +1 -1
  15. package/dist/ui/assets/workspace-app-BztEvZIC.js +5 -0
  16. package/dist/ui/assets/{workspace-app-DkAiSl_0.js → workspace-app-CwbJnb_w.js} +1 -1
  17. package/dist/ui/assets/workspace-app-YnUST8IP.css +1 -0
  18. package/dist/ui/assets/workspace-app-rKuhdae8.js +1 -0
  19. package/dist/ui/assets/workspace-lifecycle-app-CEfMdudP.js +1 -0
  20. package/dist/ui/workspace-app.html +4 -4
  21. package/dist/ui/workspace-lifecycle-app.html +4 -4
  22. package/docs/configuration.md +23 -0
  23. package/docs/debugging.md +7 -0
  24. package/package.json +1 -1
  25. package/scripts/debug/runtime.mjs +23 -1
  26. package/scripts/debug/runtime.test.mjs +14 -2
  27. package/scripts/debug/serve.mjs +4 -4
  28. package/dist/ui/assets/workspace-app-CcrHAUIn.css +0 -1
  29. package/dist/ui/assets/workspace-app-DJmkPYJC.js +0 -1
  30. package/dist/ui/assets/workspace-app-QyauBrJX.js +0 -5
  31. package/dist/ui/assets/workspace-lifecycle-app-BIXEo53I.js +0 -1
package/dist/server.js CHANGED
@@ -45,6 +45,8 @@ import { createCoreOperationExecutor, } from "./operations/core-operation-execut
45
45
  import { McpTransportRegistry, } from "./mcp-sessions.js";
46
46
  import { ProcessManager, resolveProcessId, } from "./process-sessions.js";
47
47
  import { createReviewCheckpointManager } from "./review-checkpoints.js";
48
+ import { CompositeActivityCoordinator } from "./composite-activity.js";
49
+ import { CompositeWorkspaceRegistry } from "./composite-workspaces.js";
48
50
  import { RemoteWorkspaceRelay } from "./remote-workspace-relay.js";
49
51
  import { hostConversationScopeId, openAiConversationScopeId } from "./request-meta.js";
50
52
  import { ACTIVITY_PANEL_APP_LEGACY_URI, ACTIVITY_PANEL_APP_URI_TEMPLATE, MCP_APP_RESOURCE_TEMPLATE_REVISION, readActivityPanelAppManifestEntry, readWorkspaceAppManifestEntry, readWorkspaceLifecycleAppManifestEntry, resolveActivityPanelAppIdentity, resolveWorkspaceAppIdentity, resolveWorkspaceLifecycleAppIdentity, WORKSPACE_APP_LEGACY_URI, WORKSPACE_APP_URI_TEMPLATE, WORKSPACE_LIFECYCLE_APP_LEGACY_URI, WORKSPACE_LIFECYCLE_APP_URI_TEMPLATE, } from "./mcp-app-template.js";
@@ -876,6 +878,39 @@ function toolResultContent(result) {
876
878
  const content = result.content;
877
879
  return Array.isArray(content) ? content : [];
878
880
  }
881
+ function remapCompositeToolResult(result, executionWorkspaceId, compositeWorkspaceId, member) {
882
+ if (typeof result !== "object" || result === null)
883
+ return result;
884
+ const record = result;
885
+ const remapped = replaceWorkspaceIdentity(record, executionWorkspaceId, compositeWorkspaceId);
886
+ const meta = typeof remapped._meta === "object" && remapped._meta !== null
887
+ ? { ...remapped._meta }
888
+ : undefined;
889
+ if (meta) {
890
+ const card = typeof meta.card === "object" && meta.card !== null
891
+ ? { ...meta.card, workspaceId: compositeWorkspaceId, member }
892
+ : undefined;
893
+ if (card)
894
+ meta.card = card;
895
+ remapped._meta = meta;
896
+ }
897
+ const structured = typeof remapped.structuredContent === "object" && remapped.structuredContent !== null
898
+ ? { ...remapped.structuredContent, member }
899
+ : undefined;
900
+ if (structured)
901
+ remapped.structuredContent = structured;
902
+ return remapped;
903
+ }
904
+ function replaceWorkspaceIdentity(value, from, to) {
905
+ if (typeof value === "string")
906
+ return value.split(from).join(to);
907
+ if (Array.isArray(value))
908
+ return value.map((entry) => replaceWorkspaceIdentity(entry, from, to));
909
+ if (!value || typeof value !== "object")
910
+ return value;
911
+ return Object.fromEntries(Object.entries(value)
912
+ .map(([key, entry]) => [key, replaceWorkspaceIdentity(entry, from, to)]));
913
+ }
879
914
  function toolResultAgentsFiles(result) {
880
915
  if (typeof result !== "object" || result === null)
881
916
  return [];
@@ -956,6 +991,14 @@ function activityRelationFor(context) {
956
991
  ...(context.turnId ? { turnId: context.turnId } : {}),
957
992
  };
958
993
  }
994
+ function activityRequestFor(input, context) {
995
+ if (!context.activityMember || !input || typeof input !== "object" || Array.isArray(input))
996
+ return input;
997
+ return {
998
+ ...input,
999
+ member: context.activityMember,
1000
+ };
1001
+ }
959
1002
  function runActivityTool(lifecycle, workspace, conversationScopeId, tool, request, operation, outcome = standardActivityOutcome, relation = {}) {
960
1003
  return lifecycle.run({
961
1004
  tool,
@@ -970,13 +1013,14 @@ function runActivityTool(lifecycle, workspace, conversationScopeId, tool, reques
970
1013
  function runActivityToolWithHooks(lifecycle, hooks, workspace, conversationScopeId, request, hookOptions, relation = {}) {
971
1014
  return runActivityTool(lifecycle, workspace, conversationScopeId, hookOptions.tool, request, () => runToolWithHooks(hooks, hookOptions), standardActivityOutcome, relation);
972
1015
  }
973
- function registerProcessTools(server, config, workspaces, processSessions, hooks, activityLifecycle, bashOutputStore, shellRun) {
1016
+ function registerProcessTools(server, config, workspaces, processSessions, hooks, activityLifecycle, bashOutputStore, shellRun, routing) {
974
1017
  if (config.toolMode === "codex") {
975
1018
  registerAppTool(server, "exec_command", {
976
1019
  title: "Execute command",
977
1020
  description: `Run a command inside an open workspace. Returns its result when it exits during the yield window, otherwise returns a processId for write_stdin. Use this for file inspection, tests, builds, package scripts, generators, formatters, and long-running processes. ${buildShellMutationPolicy()} Call open_workspace first and pass workspaceId.`,
978
1021
  inputSchema: {
979
1022
  workspaceId: z.string().describe("Workspace identifier returned by open_workspace."),
1023
+ member: z.string().optional().describe("Required for a Composite Workspace; explicit member name that owns this process."),
980
1024
  cmd: z.string().min(1).describe("Shell command to execute."),
981
1025
  tty: z
982
1026
  .boolean()
@@ -1013,22 +1057,34 @@ function registerProcessTools(server, config, workspaces, processSessions, hooks
1013
1057
  outputSchema: processOutputSchema(),
1014
1058
  ...toolWidgetDescriptorMeta(config, "shell"),
1015
1059
  annotations: SHELL_TOOL_ANNOTATIONS,
1016
- }, async ({ workspaceId, cmd, tty, columns, rows, workingDirectory, yieldTimeMs, timeoutMs, maxOutputTokens }, extra) => shellRun({
1017
- workspaceId,
1018
- command: cmd,
1019
- surface: "exec_command",
1020
- tty,
1021
- columns,
1022
- rows,
1023
- workingDirectory,
1024
- yieldTimeMs,
1025
- timeoutMs,
1026
- maxOutputTokens,
1027
- }, {
1028
- requestMeta: extra._meta,
1029
- signal: extra.signal,
1030
- sessionId: extra.sessionId,
1031
- }));
1060
+ }, async ({ workspaceId, member, cmd, tty, columns, rows, workingDirectory, yieldTimeMs, timeoutMs, maxOutputTokens }, extra) => {
1061
+ const target = routing.resolve(workspaceId, member);
1062
+ const context = await routing.prepare(target, extra._meta, extra.signal, extra.sessionId);
1063
+ if (routing.isRemote(target.executionWorkspaceId)) {
1064
+ return routing.present(await routing.execCommandRemote(target.executionWorkspaceId, {
1065
+ cmd,
1066
+ ...(tty !== undefined ? { tty } : {}),
1067
+ ...(columns !== undefined ? { columns } : {}),
1068
+ ...(rows !== undefined ? { rows } : {}),
1069
+ ...(workingDirectory !== undefined ? { workingDirectory } : {}),
1070
+ ...(yieldTimeMs !== undefined ? { yieldTimeMs } : {}),
1071
+ ...(timeoutMs !== undefined ? { timeoutMs } : {}),
1072
+ ...(maxOutputTokens !== undefined ? { maxOutputTokens } : {}),
1073
+ }, routing.hostScopeIdFor(extra._meta, extra.sessionId)), target);
1074
+ }
1075
+ return routing.present(await shellRun({
1076
+ workspaceId: target.executionWorkspaceId,
1077
+ command: cmd,
1078
+ surface: "exec_command",
1079
+ tty,
1080
+ columns,
1081
+ rows,
1082
+ workingDirectory,
1083
+ yieldTimeMs,
1084
+ timeoutMs,
1085
+ maxOutputTokens,
1086
+ }, context), target);
1087
+ });
1032
1088
  }
1033
1089
  if (config.toolMode !== "codex")
1034
1090
  return;
@@ -1037,6 +1093,7 @@ function registerProcessTools(server, config, workspaces, processSessions, hooks
1037
1093
  description: "Poll or write characters to a running process returned by exec_command, or retrieve complete durable process output by outputId. Omit chars or pass an empty string to poll. Waiting never kills the process; pass \\u0003 to explicitly send Ctrl-C.",
1038
1094
  inputSchema: {
1039
1095
  workspaceId: z.string().describe("Workspace identifier used to start the process."),
1096
+ member: z.string().optional().describe("Required for a Composite Workspace; explicit member name that owns the process."),
1040
1097
  processId: z.number().int().positive().optional().describe("Canonical process identifier returned by bash or exec_command."),
1041
1098
  sessionId: z.number().int().positive().optional().describe("Deprecated alias for processId. Retained for compatibility."),
1042
1099
  outputId: z.string().optional().describe("Stable output identifier returned by exec_command. When supplied, retrieve the complete durable output instead of controlling a process."),
@@ -1061,8 +1118,23 @@ function registerProcessTools(server, config, workspaces, processSessions, hooks
1061
1118
  outputSchema: processOutputSchema(),
1062
1119
  ...toolWidgetDescriptorMeta(config, "shell"),
1063
1120
  annotations: SHELL_TOOL_ANNOTATIONS,
1064
- }, async ({ workspaceId, processId, sessionId, outputId, chars, columns, rows, yieldTimeMs, maxOutputTokens }, extra) => {
1065
- const workspace = workspaces.getWorkspace(workspaceId);
1121
+ }, async ({ workspaceId, member, processId, sessionId, outputId, chars, columns, rows, yieldTimeMs, maxOutputTokens }, extra) => {
1122
+ const target = routing.resolve(workspaceId, member);
1123
+ await routing.prepare(target, extra._meta, extra.signal, extra.sessionId);
1124
+ if (routing.isRemote(target.executionWorkspaceId)) {
1125
+ return routing.present(await routing.writeStdinRemote(target.executionWorkspaceId, {
1126
+ ...(processId !== undefined ? { processId } : {}),
1127
+ ...(sessionId !== undefined ? { sessionId } : {}),
1128
+ ...(outputId !== undefined ? { outputId } : {}),
1129
+ ...(chars !== undefined ? { chars } : {}),
1130
+ ...(columns !== undefined ? { columns } : {}),
1131
+ ...(rows !== undefined ? { rows } : {}),
1132
+ ...(yieldTimeMs !== undefined ? { yieldTimeMs } : {}),
1133
+ ...(maxOutputTokens !== undefined ? { maxOutputTokens } : {}),
1134
+ }, routing.hostScopeIdFor(extra._meta, extra.sessionId)), target);
1135
+ }
1136
+ const executionWorkspaceId = target.executionWorkspaceId;
1137
+ const workspace = workspaces.getWorkspace(executionWorkspaceId);
1066
1138
  if (outputId !== undefined) {
1067
1139
  if (processId !== undefined || sessionId !== undefined || chars !== undefined || columns !== undefined ||
1068
1140
  rows !== undefined || yieldTimeMs !== undefined || maxOutputTokens !== undefined) {
@@ -1073,7 +1145,7 @@ function registerProcessTools(server, config, workspaces, processSessions, hooks
1073
1145
  tool: "write_stdin",
1074
1146
  invocation: workspaceHookInvocation(workspace),
1075
1147
  payload: { outputId },
1076
- operation: async () => durableOutputResponse("write_stdin", workspaceId, readWorkspaceBashOutput(bashOutputStore, workspaceId, outputId)),
1148
+ operation: async () => durableOutputResponse("write_stdin", executionWorkspaceId, readWorkspaceBashOutput(bashOutputStore, executionWorkspaceId, outputId)),
1077
1149
  });
1078
1150
  }
1079
1151
  const resolvedProcessId = resolveProcessId(processId, sessionId);
@@ -1090,7 +1162,7 @@ function registerProcessTools(server, config, workspaces, processSessions, hooks
1090
1162
  operation: async () => {
1091
1163
  const startedAt = performance.now();
1092
1164
  const snapshot = await processSessions.write({
1093
- workspaceId,
1165
+ workspaceId: executionWorkspaceId,
1094
1166
  processId: resolvedProcessId,
1095
1167
  chars,
1096
1168
  columns,
@@ -1108,7 +1180,7 @@ function registerProcessTools(server, config, workspaces, processSessions, hooks
1108
1180
  success: snapshot.running || snapshot.exitCode === 0,
1109
1181
  durationMs: Math.round(performance.now() - startedAt),
1110
1182
  });
1111
- const response = processToolResponse("write_stdin", workspaceId, snapshot, {
1183
+ const response = processToolResponse("write_stdin", executionWorkspaceId, snapshot, {
1112
1184
  processId: resolvedProcessId,
1113
1185
  charactersWritten: chars?.length ?? 0,
1114
1186
  running: snapshot.running,
@@ -1120,13 +1192,57 @@ function registerProcessTools(server, config, workspaces, processSessions, hooks
1120
1192
  }
1121
1193
  return response;
1122
1194
  },
1123
- });
1195
+ }).then((result) => routing.present(result, target));
1124
1196
  });
1125
1197
  }
1126
1198
  export function createMcpServer(config, workspaces, reviewCheckpoints, processSessions, localAgentProviders, incomingArtifactAdapters, codeIntelligence, activityLifecycle, bashOutputStore, activityQueries) {
1127
1199
  const connectionScopeId = `mcp-connection:${randomUUID()}`;
1128
1200
  const remoteWorkspaces = new RemoteWorkspaceRelay(config.configDir, config.stateDir);
1201
+ const compositeWorkspaces = new CompositeWorkspaceRegistry(config.stateDir);
1202
+ const compositeActivity = new CompositeActivityCoordinator(compositeWorkspaces, activityQueries, remoteWorkspaces);
1203
+ const resolveExecutionTarget = (workspaceId, memberName) => {
1204
+ if (!compositeWorkspaces.has(workspaceId)) {
1205
+ if (memberName !== undefined) {
1206
+ throw new Error(`Workspace ${workspaceId} is not composite and does not accept member.`);
1207
+ }
1208
+ return { executionWorkspaceId: workspaceId };
1209
+ }
1210
+ if (!memberName) {
1211
+ throw new Error(`Composite Workspace ${workspaceId} requires member for this operation.`);
1212
+ }
1213
+ const member = compositeWorkspaces.member(workspaceId, memberName);
1214
+ try {
1215
+ if (!remoteWorkspaces.has(member.workspaceId))
1216
+ workspaces.getWorkspace(member.workspaceId);
1217
+ }
1218
+ catch (error) {
1219
+ throw new Error(`Composite Workspace ${workspaceId} member ${member.name} is unavailable: ${error instanceof Error ? error.message : String(error)}`);
1220
+ }
1221
+ return {
1222
+ executionWorkspaceId: member.workspaceId,
1223
+ compositeWorkspaceId: workspaceId,
1224
+ memberName: member.name,
1225
+ };
1226
+ };
1227
+ const presentExecutionResult = (result, target) => {
1228
+ if (!target.compositeWorkspaceId || !target.memberName)
1229
+ return result;
1230
+ return remapCompositeToolResult(result, target.executionWorkspaceId, target.compositeWorkspaceId, target.memberName);
1231
+ };
1129
1232
  const hostScopeIdFor = (requestMeta, transportSessionId) => hostConversationScopeId(requestMeta, transportSessionId, connectionScopeId);
1233
+ const prepareExecutionContext = async (target, requestMeta, signal, sessionId) => {
1234
+ const conversationScopeId = hostScopeIdFor(requestMeta, sessionId);
1235
+ const turnId = target.compositeWorkspaceId && target.memberName
1236
+ ? await compositeActivity.prepareMember(target.compositeWorkspaceId, target.memberName, target.executionWorkspaceId, conversationScopeId)
1237
+ : undefined;
1238
+ return {
1239
+ requestMeta,
1240
+ signal,
1241
+ sessionId,
1242
+ ...(turnId ? { turnId } : {}),
1243
+ ...(target.memberName ? { activityMember: target.memberName } : {}),
1244
+ };
1245
+ };
1130
1246
  const toolDescriptions = buildToolDescriptions(config);
1131
1247
  const hooks = new HookRunner(config.hooks, config.logging, process.env, (workspaceId, result) => attachCompletedProcessNotices(processSessions, workspaceId, result, (snapshot) => recordBashCompletion(activityLifecycle, bashOutputStore, snapshot.outputId)));
1132
1248
  const incomingArtifactRegistry = new IncomingArtifactAdapterRegistry(incomingArtifactAdapters);
@@ -1224,11 +1340,59 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
1224
1340
  },
1225
1341
  },
1226
1342
  });
1343
+ const loadCompositeMemberContext = async (compositeWorkspaceId, memberName, contextPolicy, conversationScopeId, protectedWorkspaceIds) => {
1344
+ const target = resolveExecutionTarget(compositeWorkspaceId, memberName);
1345
+ if (remoteWorkspaces.has(target.executionWorkspaceId)) {
1346
+ const resumed = await remoteWorkspaces.resumeWorkspace(target.executionWorkspaceId, contextPolicy, conversationScopeId);
1347
+ const presented = presentExecutionResult(resumed, target);
1348
+ return {
1349
+ member: memberName,
1350
+ ...(presented.structuredContent ?? {}),
1351
+ };
1352
+ }
1353
+ const opened = await workspaces.openWorkspace({ workspaceId: target.executionWorkspaceId, context: contextPolicy }, { conversationScopeId, protectedWorkspaceIds });
1354
+ const workspace = opened.workspace;
1355
+ const capabilityFingerprint = buildCapabilityFingerprint(config, FORGERELAY_VERSION, {
1356
+ artifactDownloadSupported: isArtifactDownloadSupportedPlatform(),
1357
+ });
1358
+ const capabilityCatalog = capabilityRegistry.catalog(capabilityContextFor(workspace));
1359
+ const agentsFiles = opened.agentsFiles.map((file) => ({
1360
+ path: formatAgentsPath(file.path, workspace.root),
1361
+ content: file.content,
1362
+ }));
1363
+ const availableAgentsFiles = opened.availableAgentsFiles.map((file) => ({
1364
+ path: formatAgentsPath(file.path, workspace.root),
1365
+ }));
1366
+ const skills = workspace.skills
1367
+ .filter((skill) => !skill.disableModelInvocation)
1368
+ .map((skill) => ({ name: skill.name, description: skill.description }));
1369
+ return {
1370
+ member: memberName,
1371
+ workspaceId: compositeWorkspaceId,
1372
+ root: workspace.root,
1373
+ mode: workspace.mode,
1374
+ contextFingerprint: opened.contextFingerprint,
1375
+ capabilityFingerprint,
1376
+ capabilityCatalog,
1377
+ includeBootstrapContext: opened.includeBootstrapContext,
1378
+ ...(opened.includeBootstrapContext
1379
+ ? {
1380
+ agentsFiles,
1381
+ availableAgentsFiles,
1382
+ skills,
1383
+ skillDiagnostics: redactSkillDiagnosticPaths(workspace.skillDiagnostics),
1384
+ }
1385
+ : {}),
1386
+ instruction: opened.includeBootstrapContext
1387
+ ? `Bootstrap context for Composite member ${memberName}. Keep using Composite workspaceId ${compositeWorkspaceId} and pass member=${memberName} for work operations.`
1388
+ : `Composite member ${memberName} context was already delivered for this Host context; keep using Composite workspaceId ${compositeWorkspaceId} with member=${memberName}.`,
1389
+ };
1390
+ };
1227
1391
  const coreOperations = createCoreOperationExecutor({
1228
1392
  read: async (input, context) => {
1229
1393
  const { workspaceId, ...readInput } = input;
1230
1394
  const workspace = workspaces.getWorkspace(workspaceId);
1231
- return runActivityToolWithHooks(activityLifecycle, hooks, workspace, hostScopeIdFor(context.requestMeta, context.sessionId), input, {
1395
+ return runActivityToolWithHooks(activityLifecycle, hooks, workspace, hostScopeIdFor(context.requestMeta, context.sessionId), activityRequestFor(input, context), {
1232
1396
  signal: context.signal,
1233
1397
  tool: toolNames.read,
1234
1398
  invocation: workspaceHookInvocation(workspace),
@@ -1300,7 +1464,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
1300
1464
  write: async (input, context) => {
1301
1465
  const { workspaceId, ...writeInput } = input;
1302
1466
  const workspace = workspaces.getWorkspace(workspaceId);
1303
- return runActivityToolWithHooks(activityLifecycle, hooks, workspace, hostScopeIdFor(context.requestMeta, context.sessionId), input, {
1467
+ return runActivityToolWithHooks(activityLifecycle, hooks, workspace, hostScopeIdFor(context.requestMeta, context.sessionId), activityRequestFor(input, context), {
1304
1468
  signal: context.signal,
1305
1469
  tool: toolNames.write,
1306
1470
  invocation: workspaceHookInvocation(workspace),
@@ -1361,7 +1525,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
1361
1525
  edit: async (input, context) => {
1362
1526
  const { workspaceId, ...editInput } = input;
1363
1527
  const workspace = workspaces.getWorkspace(workspaceId);
1364
- return runActivityToolWithHooks(activityLifecycle, hooks, workspace, hostScopeIdFor(context.requestMeta, context.sessionId), input, {
1528
+ return runActivityToolWithHooks(activityLifecycle, hooks, workspace, hostScopeIdFor(context.requestMeta, context.sessionId), activityRequestFor(input, context), {
1365
1529
  signal: context.signal,
1366
1530
  tool: toolNames.edit,
1367
1531
  invocation: workspaceHookInvocation(workspace),
@@ -1423,7 +1587,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
1423
1587
  rename: async (input, context) => {
1424
1588
  const { workspaceId, path, newPath } = input;
1425
1589
  const workspace = workspaces.getWorkspace(workspaceId);
1426
- return runActivityToolWithHooks(activityLifecycle, hooks, workspace, hostScopeIdFor(context.requestMeta, context.sessionId), input, {
1590
+ return runActivityToolWithHooks(activityLifecycle, hooks, workspace, hostScopeIdFor(context.requestMeta, context.sessionId), activityRequestFor(input, context), {
1427
1591
  signal: context.signal,
1428
1592
  tool: toolNames.rename,
1429
1593
  invocation: workspaceHookInvocation(workspace),
@@ -1482,7 +1646,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
1482
1646
  delete: async (input, context) => {
1483
1647
  const { workspaceId, path, recursive } = input;
1484
1648
  const workspace = workspaces.getWorkspace(workspaceId);
1485
- return runActivityToolWithHooks(activityLifecycle, hooks, workspace, hostScopeIdFor(context.requestMeta, context.sessionId), input, {
1649
+ return runActivityToolWithHooks(activityLifecycle, hooks, workspace, hostScopeIdFor(context.requestMeta, context.sessionId), activityRequestFor(input, context), {
1486
1650
  signal: context.signal,
1487
1651
  tool: toolNames.delete,
1488
1652
  invocation: workspaceHookInvocation(workspace),
@@ -1566,7 +1730,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
1566
1730
  maxOutputTokens,
1567
1731
  };
1568
1732
  let undeliveredProcessId;
1569
- const activityResult = await runActivityTool(activityLifecycle, workspace, hostScopeIdFor(context.requestMeta, context.sessionId), surface, activityRequest, async (activityContext) => {
1733
+ const activityResult = await runActivityTool(activityLifecycle, workspace, hostScopeIdFor(context.requestMeta, context.sessionId), surface, activityRequestFor(activityRequest, context), async (activityContext) => {
1570
1734
  try {
1571
1735
  const result = await runToolWithHooks(hooks, {
1572
1736
  signal: context.signal,
@@ -1640,7 +1804,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
1640
1804
  const { workspaceId, name, arguments: capabilityArguments, file } = input;
1641
1805
  const workspace = workspaces.getWorkspace(workspaceId);
1642
1806
  let changedPaths = [];
1643
- return runActivityToolWithHooks(activityLifecycle, hooks, workspace, hostScopeIdFor(context.requestMeta, context.sessionId), { workspaceId, name, action: "run", arguments: capabilityArguments, file }, {
1807
+ return runActivityToolWithHooks(activityLifecycle, hooks, workspace, hostScopeIdFor(context.requestMeta, context.sessionId), activityRequestFor({ workspaceId, name, action: "run", arguments: capabilityArguments, file }, context), {
1644
1808
  signal: context.signal,
1645
1809
  tool: toolNames.capability,
1646
1810
  invocation: workspaceHookInvocation(workspace),
@@ -1794,16 +1958,44 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
1794
1958
  server.registerResource("ForgeRelay Activity Panel compatibility", new ResourceTemplate(ACTIVITY_PANEL_APP_URI_TEMPLATE, { list: undefined }), { ...activityPanelResourceMetadata, mimeType: RESOURCE_MIME_TYPE }, async (uri, _variables, extra) => readActivityPanelAppResource(config, uri.toString(), extra.sessionId));
1795
1959
  registerAppTool(server, "open_workspace", {
1796
1960
  title: "Open workspace",
1797
- description: "Open or resume a coding workspace. Defaults to local execution; for a new workspace, relay may name a registered direct remote ForgeRelay. Reuse the returned Gateway workspaceId for later calls. Default to checkout; use mode=\"worktree\" only for explicitly isolated or parallel Git work. Bootstrap context is delivered automatically only when needed and can be suppressed or refreshed.",
1961
+ description: "Open or resume a ForgeRelay Workspace. Ordinary workspaces default to local execution; relay may name a registered remote ForgeRelay. Composite Workspaces use the same open lifecycle but have kind=\"composite\" and a name instead of a mounted root. Reuse the returned workspaceId for later calls. Bootstrap context is delivered automatically only when needed and can be suppressed or refreshed.",
1798
1962
  inputSchema: {
1799
1963
  action: z
1800
- .enum(["open", "list"])
1964
+ .enum(["open", "list", "member"])
1965
+ .optional()
1966
+ .describe("Defaults to open. Use list to inspect logical workspaces. Use member to add/remove a named execution member on an existing Composite Workspace."),
1967
+ memberAction: z
1968
+ .enum(["add", "update", "remove"])
1969
+ .optional()
1970
+ .describe("Required with action=member."),
1971
+ member: z.object({
1972
+ name: z.string().describe("Stable member name such as code or compute. For update/remove this identifies the existing member."),
1973
+ newName: z.string().optional().describe("Optional replacement member name for memberAction=update."),
1974
+ purpose: z.string().optional().describe("Agent-facing purpose. Required when adding a member; optional replacement when updating."),
1975
+ workspaceId: z.string().optional().describe("Existing ordinary or relayed Workspace to mount. Mutually exclusive with path."),
1976
+ path: z.string().optional().describe("Workspace path to open internally and mount. Mutually exclusive with workspaceId."),
1977
+ relay: z.string().optional().describe("Optional registered remote ForgeRelay alias for a path-backed member."),
1978
+ mode: z.enum(["checkout", "worktree"]).optional(),
1979
+ baseRef: z.string().optional(),
1980
+ newWorktree: z.boolean().optional(),
1981
+ newWorkspace: z.boolean().optional(),
1982
+ }).optional().describe("Composite member definition used by action=member."),
1983
+ kind: z
1984
+ .enum(["workspace", "composite"])
1985
+ .optional()
1986
+ .describe("Workspace kind for action=open. Defaults to workspace. Use composite with name and no path to create or reopen a named Composite Workspace."),
1987
+ name: z
1988
+ .string()
1801
1989
  .optional()
1802
- .describe("Defaults to open. Use list only when you need to inspect or choose logical workspaces before resuming or cleaning them up."),
1990
+ .describe("Composite Workspace name. Used only with kind=\"composite\" when creating/opening by name."),
1991
+ memberName: z
1992
+ .string()
1993
+ .optional()
1994
+ .describe("For action=open on a Composite Workspace, load bootstrap context for this named member without making it an implicit current member."),
1803
1995
  path: z
1804
1996
  .string()
1805
1997
  .optional()
1806
- .describe("Project path to open. Required for action=open unless workspaceId is supplied. With mode=\"worktree\", this may also be a managed worktree path previously returned by ForgeRelay."),
1998
+ .describe("Project path to open for an ordinary Workspace. Required for action=open unless workspaceId is supplied or kind=\"composite\" with name is used. With mode=\"worktree\", this may also be a managed worktree path previously returned by ForgeRelay."),
1807
1999
  relay: z
1808
2000
  .string()
1809
2001
  .optional()
@@ -1863,8 +2055,17 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
1863
2055
  .describe("For action=list, maximum records to return. Defaults to 50; maximum 100."),
1864
2056
  },
1865
2057
  outputSchema: {
1866
- action: z.enum(["open", "list"]),
2058
+ action: z.enum(["open", "list", "member"]),
1867
2059
  workspaceId: z.string().optional(),
2060
+ memberAction: z.enum(["add", "update", "remove"]).optional(),
2061
+ kind: z.enum(["workspace", "composite"]).optional(),
2062
+ name: z.string().optional(),
2063
+ members: z.array(z.object({
2064
+ name: z.string(),
2065
+ purpose: z.string(),
2066
+ workspaceId: z.string(),
2067
+ })).optional(),
2068
+ memberContext: z.unknown().optional(),
1868
2069
  root: z.string().optional(),
1869
2070
  mode: z.enum(["checkout", "worktree"]).optional(),
1870
2071
  sourceRoot: z.string().optional(),
@@ -1911,6 +2112,18 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
1911
2112
  agents: z.array(workspaceLocalAgentOutputSchema).optional(),
1912
2113
  skillDiagnostics: z.array(workspaceSkillDiagnosticOutputSchema).optional(),
1913
2114
  workspaces: z.array(workspaceInventoryEntryOutputSchema).optional(),
2115
+ compositeWorkspaces: z.array(z.object({
2116
+ workspaceId: z.string(),
2117
+ kind: z.literal("composite"),
2118
+ name: z.string(),
2119
+ members: z.array(z.object({
2120
+ name: z.string(),
2121
+ purpose: z.string(),
2122
+ workspaceId: z.string(),
2123
+ })),
2124
+ createdAt: z.string(),
2125
+ lastUsedAt: z.string(),
2126
+ })).optional(),
1914
2127
  summary: workspaceInventorySummaryOutputSchema.optional(),
1915
2128
  page: workspaceInventoryPageOutputSchema.optional(),
1916
2129
  instruction: z.string(),
@@ -1922,26 +2135,198 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
1922
2135
  idempotentHint: false,
1923
2136
  openWorldHint: false,
1924
2137
  },
1925
- }, async ({ action = "open", path, relay, workspaceId, mode, baseRef, newWorktree, newWorkspace, context, root, status, state, staleOnly, offset, limit, }, { _meta, sessionId }) => {
2138
+ }, async ({ action = "open", memberAction, member, kind, name, memberName, path, relay, workspaceId, mode, baseRef, newWorktree, newWorkspace, context, root, status, state, staleOnly, offset, limit, }, { _meta, sessionId }) => {
1926
2139
  const startedAt = performance.now();
1927
2140
  const conversationScopeId = openAiConversationScopeId(_meta);
1928
2141
  const protectedWorkspaceIds = processSessions.activeWorkspaceIds();
2142
+ if (action === "member") {
2143
+ if (!workspaceId || !compositeWorkspaces.has(workspaceId)) {
2144
+ throw new Error("open_workspace action=member requires an existing Composite Workspace workspaceId.");
2145
+ }
2146
+ if (!memberAction || !member) {
2147
+ throw new Error("open_workspace action=member requires memberAction and member.");
2148
+ }
2149
+ if (kind !== undefined || name !== undefined || memberName !== undefined || path !== undefined || relay !== undefined || mode !== undefined ||
2150
+ baseRef !== undefined || newWorktree !== undefined || newWorkspace !== undefined || context !== undefined ||
2151
+ root !== undefined || status !== undefined || state !== undefined || staleOnly !== undefined ||
2152
+ offset !== undefined || limit !== undefined) {
2153
+ throw new Error("open_workspace action=member accepts workspaceId, memberAction, and member only. Put any Workspace open definition inside member.");
2154
+ }
2155
+ const resolveMemberTargetWorkspaceId = async () => {
2156
+ const byWorkspaceId = typeof member.workspaceId === "string" && member.workspaceId.length > 0;
2157
+ const byPath = typeof member.path === "string" && member.path.length > 0;
2158
+ if (byWorkspaceId === byPath) {
2159
+ throw new Error("A Composite Workspace member target requires exactly one of member.workspaceId or member.path.");
2160
+ }
2161
+ if (byWorkspaceId) {
2162
+ const targetWorkspaceId = member.workspaceId;
2163
+ if (compositeWorkspaces.has(targetWorkspaceId)) {
2164
+ throw new Error("A Composite Workspace cannot be mounted as a Composite Workspace member.");
2165
+ }
2166
+ if (member.relay !== undefined || member.mode !== undefined || member.baseRef !== undefined ||
2167
+ member.newWorktree !== undefined || member.newWorkspace !== undefined) {
2168
+ throw new Error("member.workspaceId cannot be combined with relay/mode/baseRef/newWorktree/newWorkspace.");
2169
+ }
2170
+ if (!remoteWorkspaces.has(targetWorkspaceId))
2171
+ workspaces.getWorkspace(targetWorkspaceId);
2172
+ return targetWorkspaceId;
2173
+ }
2174
+ if (member.relay !== undefined) {
2175
+ const opened = await remoteWorkspaces.openWorkspace(member.relay, {
2176
+ path: member.path,
2177
+ ...(member.mode ? { mode: member.mode } : {}),
2178
+ ...(member.baseRef ? { baseRef: member.baseRef } : {}),
2179
+ ...(member.newWorktree !== undefined ? { newWorktree: member.newWorktree } : {}),
2180
+ ...(member.newWorkspace !== undefined ? { newWorkspace: member.newWorkspace } : {}),
2181
+ context: "none",
2182
+ });
2183
+ return opened.workspaceId;
2184
+ }
2185
+ const opened = await workspaces.openWorkspace({
2186
+ path: member.path,
2187
+ ...(member.mode ? { mode: member.mode } : {}),
2188
+ ...(member.baseRef ? { baseRef: member.baseRef } : {}),
2189
+ ...(member.newWorktree !== undefined ? { newWorktree: member.newWorktree } : {}),
2190
+ ...(member.newWorkspace !== undefined ? { newWorkspace: member.newWorkspace } : {}),
2191
+ context: "none",
2192
+ }, { protectedWorkspaceIds });
2193
+ return opened.workspace.id;
2194
+ };
2195
+ let composite;
2196
+ if (memberAction === "add") {
2197
+ if (member.newName !== undefined) {
2198
+ throw new Error("Adding a Composite Workspace member does not accept member.newName.");
2199
+ }
2200
+ const purpose = member.purpose?.trim();
2201
+ if (!purpose)
2202
+ throw new Error("Adding a Composite Workspace member requires member.purpose.");
2203
+ const targetWorkspaceId = await resolveMemberTargetWorkspaceId();
2204
+ composite = compositeWorkspaces.addMember(workspaceId, {
2205
+ name: member.name,
2206
+ purpose,
2207
+ workspaceId: targetWorkspaceId,
2208
+ });
2209
+ }
2210
+ else if (memberAction === "update") {
2211
+ const targetFieldsPresent = member.workspaceId !== undefined || member.path !== undefined || member.relay !== undefined ||
2212
+ member.mode !== undefined || member.baseRef !== undefined || member.newWorktree !== undefined ||
2213
+ member.newWorkspace !== undefined;
2214
+ if (member.newName === undefined && member.purpose === undefined && !targetFieldsPresent) {
2215
+ throw new Error("Updating a Composite Workspace member requires newName, purpose, or a replacement Workspace target.");
2216
+ }
2217
+ const targetWorkspaceId = targetFieldsPresent
2218
+ ? await resolveMemberTargetWorkspaceId()
2219
+ : undefined;
2220
+ composite = compositeWorkspaces.updateMember(workspaceId, member.name, {
2221
+ ...(member.newName !== undefined ? { name: member.newName } : {}),
2222
+ ...(member.purpose !== undefined ? { purpose: member.purpose } : {}),
2223
+ ...(targetWorkspaceId !== undefined ? { workspaceId: targetWorkspaceId } : {}),
2224
+ });
2225
+ }
2226
+ else {
2227
+ if (member.newName !== undefined || member.purpose !== undefined || member.workspaceId !== undefined || member.path !== undefined ||
2228
+ member.relay !== undefined || member.mode !== undefined || member.baseRef !== undefined ||
2229
+ member.newWorktree !== undefined || member.newWorkspace !== undefined) {
2230
+ throw new Error("Removing a Composite Workspace member accepts only member.name.");
2231
+ }
2232
+ composite = compositeWorkspaces.removeMember(workspaceId, member.name);
2233
+ }
2234
+ const memberActionVerb = memberAction === "add"
2235
+ ? "Added"
2236
+ : memberAction === "update"
2237
+ ? "Updated"
2238
+ : "Removed";
2239
+ const memberActionPreposition = memberAction === "remove" ? "from" : "in";
2240
+ const instruction = [
2241
+ `${memberActionVerb} member ${member.name} ${memberActionPreposition} Composite Workspace ${composite.name} (${composite.id}).`,
2242
+ composite.members.length > 0
2243
+ ? `Members: ${composite.members.map((entry) => `${entry.name} — ${entry.purpose}`).join("; ")}.`
2244
+ : "This Composite Workspace currently has no members.",
2245
+ "Use the Composite workspaceId as the top-level handle. Work operations on it require an explicit member name; ForgeRelay never infers a member from tool type or purpose.",
2246
+ ].join("\n");
2247
+ const response = {
2248
+ content: [textBlock(instruction)],
2249
+ _meta: {
2250
+ tool: "open_workspace",
2251
+ card: {
2252
+ workspaceId: composite.id,
2253
+ kind: "composite",
2254
+ name: composite.name,
2255
+ path: composite.name,
2256
+ members: composite.members,
2257
+ instruction,
2258
+ summary: { members: composite.members.length },
2259
+ },
2260
+ },
2261
+ structuredContent: {
2262
+ action: "member",
2263
+ workspaceId: composite.id,
2264
+ memberAction,
2265
+ kind: "composite",
2266
+ name: composite.name,
2267
+ members: composite.members,
2268
+ instruction,
2269
+ },
2270
+ };
2271
+ rememberWorkspacePanelState(composite.id, response);
2272
+ return response;
2273
+ }
1929
2274
  if (action === "list") {
1930
- if (path !== undefined || relay !== undefined || baseRef !== undefined || newWorktree !== undefined ||
2275
+ if (path !== undefined || relay !== undefined || name !== undefined || memberName !== undefined || baseRef !== undefined || newWorktree !== undefined ||
1931
2276
  newWorkspace !== undefined || context !== undefined) {
1932
- throw new Error("open_workspace action=list does not accept path, relay, baseRef, newWorktree, newWorkspace, or context. Use root/workspaceId/mode/status/state/staleOnly for inventory filters.");
2277
+ throw new Error("open_workspace action=list does not accept path, relay, name, memberName, baseRef, newWorktree, newWorkspace, or context. Use kind/root/workspaceId/mode/status/state/staleOnly for inventory filters.");
2278
+ }
2279
+ if (kind === "composite") {
2280
+ if (root !== undefined || mode !== undefined || status !== undefined || state !== undefined ||
2281
+ staleOnly !== undefined || offset !== undefined || limit !== undefined) {
2282
+ throw new Error("Composite Workspace inventory does not accept root/mode/status/state/staleOnly/offset/limit filters; use workspaceId when selecting one Composite Workspace.");
2283
+ }
2284
+ const composites = compositeWorkspaces.list()
2285
+ .filter((entry) => workspaceId === undefined || entry.id === workspaceId)
2286
+ .map((entry) => ({
2287
+ workspaceId: entry.id,
2288
+ kind: entry.kind,
2289
+ name: entry.name,
2290
+ members: entry.members,
2291
+ createdAt: entry.createdAt,
2292
+ lastUsedAt: entry.lastUsedAt,
2293
+ }));
2294
+ const instruction = "Resume a Composite Workspace with open_workspace(action=\"open\", workspaceId=...). Use close_workspace only when the user chooses to dissolve it.";
2295
+ const result = [
2296
+ `Composite Workspace inventory: ${composites.length} matching record${composites.length === 1 ? "" : "s"}.`,
2297
+ ...composites.map((entry) => `${entry.name} [${entry.workspaceId}] members=${entry.members.length} last-used=${entry.lastUsedAt}`),
2298
+ instruction,
2299
+ ].join("\n");
2300
+ return {
2301
+ content: [textBlock(result)],
2302
+ structuredContent: {
2303
+ action: "list",
2304
+ compositeWorkspaces: composites,
2305
+ instruction,
2306
+ },
2307
+ };
1933
2308
  }
1934
2309
  const inventory = await workspaces.listWorkspaces({ workspaceId, mode, root, status, state, staleOnly, offset, limit }, { conversationScopeId, protectedWorkspaceIds });
2310
+ const composites = kind === "workspace"
2311
+ ? []
2312
+ : compositeWorkspaces.list().map((entry) => ({
2313
+ workspaceId: entry.id,
2314
+ kind: entry.kind,
2315
+ name: entry.name,
2316
+ members: entry.members,
2317
+ createdAt: entry.createdAt,
2318
+ lastUsedAt: entry.lastUsedAt,
2319
+ }));
1935
2320
  const nextOffset = inventory.page.offset + inventory.page.limit;
1936
2321
  const instruction = [
1937
2322
  "Resume a selected workspaceId with open_workspace(action=\"open\", workspaceId=...).",
1938
- "Use close_workspace only after the user chooses cleanup; never close inventory entries automatically.",
2323
+ "Use close_workspace only after the user chooses cleanup or Composite dissolution; never close inventory entries automatically.",
1939
2324
  inventory.page.hasMore
1940
2325
  ? `More matching workspaces are available; continue with offset=${nextOffset}.`
1941
2326
  : undefined,
1942
2327
  ].filter(Boolean).join(" ");
1943
2328
  const result = [
1944
- `Logical workspace inventory: ${inventory.summary.matching} matching of ${inventory.summary.total} stored records.`,
2329
+ `Logical workspace inventory: ${inventory.summary.matching} matching ordinary records; ${composites.length} Composite Workspace record${composites.length === 1 ? "" : "s"}.`,
1945
2330
  `States: active=${inventory.summary.active}, stale=${inventory.summary.stale}, invalid=${inventory.summary.invalid}, closed=${inventory.summary.closed}.`,
1946
2331
  ...inventory.workspaces.map((entry) => [
1947
2332
  entry.label,
@@ -1953,6 +2338,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
1953
2338
  `root=${entry.root}`,
1954
2339
  `last-used=${entry.lastUsedAt}`,
1955
2340
  ].filter(Boolean).join(" ")),
2341
+ ...composites.map((entry) => `${entry.name} [${entry.workspaceId}] kind=composite members=${entry.members.length}`),
1956
2342
  instruction,
1957
2343
  ].join("\n");
1958
2344
  logToolCall(config, {
@@ -1967,6 +2353,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
1967
2353
  structuredContent: {
1968
2354
  action: "list",
1969
2355
  ...inventory,
2356
+ ...(composites.length > 0 ? { compositeWorkspaces: composites } : {}),
1970
2357
  instruction,
1971
2358
  },
1972
2359
  };
@@ -1975,6 +2362,70 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
1975
2362
  staleOnly !== undefined || offset !== undefined || limit !== undefined) {
1976
2363
  throw new Error("open_workspace inventory filters root, status, state, staleOnly, offset, and limit are only valid with action=list.");
1977
2364
  }
2365
+ const openingComposite = kind === "composite" ||
2366
+ (workspaceId !== undefined && compositeWorkspaces.has(workspaceId));
2367
+ if (openingComposite) {
2368
+ if (relay !== undefined || path !== undefined || mode !== undefined || baseRef !== undefined ||
2369
+ newWorktree !== undefined || newWorkspace !== undefined) {
2370
+ throw new Error("Composite Workspace open accepts name/workspaceId/context only; members are attached separately and keep their own Workspace definitions.");
2371
+ }
2372
+ if (workspaceId !== undefined && kind === "workspace") {
2373
+ throw new Error(`${workspaceId} is a Composite Workspace, not an ordinary Workspace.`);
2374
+ }
2375
+ const composite = workspaceId !== undefined
2376
+ ? compositeWorkspaces.open(workspaceId)
2377
+ : compositeWorkspaces.create(name ?? "");
2378
+ const memberContext = memberName
2379
+ ? await loadCompositeMemberContext(composite.id, memberName, context ?? "auto", conversationScopeId, protectedWorkspaceIds)
2380
+ : undefined;
2381
+ const instruction = [
2382
+ `This is Composite Workspace ${composite.name} (${composite.id}).`,
2383
+ "It has no mounted working directory of its own. Use the Composite workspaceId as the top-level Workspace handle and explicitly select one named member for member-scoped work operations.",
2384
+ composite.members.length > 0
2385
+ ? `Members: ${composite.members.map((member) => `${member.name} — ${member.purpose}`).join("; ")}.`
2386
+ : "This Composite Workspace currently has no members.",
2387
+ "Member names and purposes are structural context and are always returned when this Composite Workspace is opened. context=auto/full/none controls only heavy member bootstrap context, not this Composite identity.",
2388
+ composite.members.length > 0
2389
+ ? "Before first work on a member, reopen this Composite Workspace with memberName=<member> and context=auto to receive that member's project bootstrap without creating an implicit current member."
2390
+ : undefined,
2391
+ "Use close_workspace only when the user chooses to dissolve this Composite Workspace; dissolution does not close or clean up member Workspaces.",
2392
+ ].join("\n\n");
2393
+ const response = {
2394
+ content: [textBlock(instruction)],
2395
+ _meta: {
2396
+ tool: "open_workspace",
2397
+ card: {
2398
+ workspaceId: composite.id,
2399
+ kind: "composite",
2400
+ name: composite.name,
2401
+ path: composite.name,
2402
+ members: composite.members,
2403
+ instruction,
2404
+ summary: { members: composite.members.length },
2405
+ },
2406
+ },
2407
+ structuredContent: {
2408
+ action: "open",
2409
+ workspaceId: composite.id,
2410
+ kind: "composite",
2411
+ name: composite.name,
2412
+ members: composite.members,
2413
+ ...(memberContext ? { memberContext } : {}),
2414
+ instruction,
2415
+ },
2416
+ };
2417
+ logToolCall(config, {
2418
+ tool: "open_workspace",
2419
+ action: "composite",
2420
+ success: true,
2421
+ durationMs: Math.round(performance.now() - startedAt),
2422
+ });
2423
+ rememberWorkspacePanelState(composite.id, response);
2424
+ return response;
2425
+ }
2426
+ if (name !== undefined || memberName !== undefined) {
2427
+ throw new Error("open_workspace name/memberName are only valid for a Composite Workspace.");
2428
+ }
1978
2429
  if (relay !== undefined) {
1979
2430
  if (workspaceId !== undefined) {
1980
2431
  throw new Error("Relayed open_workspace requires a path; resuming a relayed workspace is not available in this tracer bullet.");
@@ -2014,6 +2465,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
2014
2465
  tool: "open_workspace",
2015
2466
  card: {
2016
2467
  workspaceId: opened.workspaceId,
2468
+ kind: "workspace",
2017
2469
  root: opened.root,
2018
2470
  path: opened.root,
2019
2471
  mode: opened.mode,
@@ -2025,6 +2477,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
2025
2477
  structuredContent: {
2026
2478
  action: "open",
2027
2479
  workspaceId: opened.workspaceId,
2480
+ kind: "workspace",
2028
2481
  root: opened.root,
2029
2482
  mode: opened.mode,
2030
2483
  ...(opened.sourceRoot ? { sourceRoot: opened.sourceRoot } : {}),
@@ -2202,6 +2655,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
2202
2655
  tool: "open_workspace",
2203
2656
  card: {
2204
2657
  workspaceId: workspace.id,
2658
+ kind: "workspace",
2205
2659
  root: workspace.root,
2206
2660
  path: workspace.root,
2207
2661
  mode: workspace.mode,
@@ -2234,6 +2688,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
2234
2688
  structuredContent: {
2235
2689
  action: "open",
2236
2690
  workspaceId: workspace.id,
2691
+ kind: "workspace",
2237
2692
  root: workspace.root,
2238
2693
  mode: workspace.mode,
2239
2694
  sourceRoot: workspace.sourceRoot,
@@ -2261,18 +2716,40 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
2261
2716
  return response;
2262
2717
  });
2263
2718
  registerActivityQueryTools(server, activityQueries, connectionScopeId, toolWidgetDescriptorMeta(config, "activity")._meta, config.activityPanelExpanded, config.logging, workspacePanelState, {
2264
- panel: async (workspaceId, conversationScopeId) => remoteWorkspaces.has(workspaceId)
2265
- ? remoteWorkspaces.activityPanel(workspaceId, conversationScopeId)
2266
- : undefined,
2267
- snapshot: (input, conversationScopeId) => remoteWorkspaces.activitySnapshot(input, conversationScopeId),
2268
- detail: (turnId, activityId, conversationScopeId) => remoteWorkspaces.activityDetail(turnId, activityId, conversationScopeId),
2269
- output: (turnId, outputId, conversationScopeId) => remoteWorkspaces.activityOutput(turnId, outputId, conversationScopeId),
2719
+ panel: async (workspaceId, conversationScopeId) => {
2720
+ if (compositeWorkspaces.has(workspaceId)) {
2721
+ return compositeActivity.beginPanel(workspaceId, conversationScopeId);
2722
+ }
2723
+ return remoteWorkspaces.has(workspaceId)
2724
+ ? remoteWorkspaces.activityPanel(workspaceId, conversationScopeId)
2725
+ : undefined;
2726
+ },
2727
+ snapshot: async (input, conversationScopeId) => {
2728
+ const compositeTurnId = input.turnId ?? (input.workspaceId && compositeWorkspaces.has(input.workspaceId)
2729
+ ? compositeActivity.currentTurnId(conversationScopeId, input.workspaceId)
2730
+ : undefined);
2731
+ if (compositeTurnId) {
2732
+ const composite = await compositeActivity.snapshot(compositeTurnId, input.knownRevision);
2733
+ if (composite)
2734
+ return composite;
2735
+ }
2736
+ return remoteWorkspaces.activitySnapshot(input, conversationScopeId);
2737
+ },
2738
+ detail: async (turnId, activityId, conversationScopeId) => {
2739
+ const composite = await compositeActivity.detail(turnId, activityId);
2740
+ return composite ?? remoteWorkspaces.activityDetail(turnId, activityId, conversationScopeId);
2741
+ },
2742
+ output: async (turnId, outputId, conversationScopeId) => {
2743
+ const composite = await compositeActivity.output(turnId, outputId);
2744
+ return composite ?? remoteWorkspaces.activityOutput(turnId, outputId, conversationScopeId);
2745
+ },
2270
2746
  });
2271
2747
  registerAppTool(server, toolNames.capability, {
2272
2748
  title: "Use optional capability",
2273
2749
  description: "Describe or run one optional ForgeRelay capability advertised by open_workspace. Use describe when the capability contract is unfamiliar, then read its advertised guide if needed. Run dispatches only explicitly registered capabilities; it cannot invoke arbitrary shell commands, URLs, or methods.",
2274
2750
  inputSchema: {
2275
2751
  workspaceId: z.string().describe("Workspace identifier returned by open_workspace."),
2752
+ member: z.string().optional().describe("Required for a Composite Workspace; explicit member name whose capability surface is used."),
2276
2753
  name: z
2277
2754
  .string()
2278
2755
  .regex(/^[a-z][a-z0-9-]*(?:\.[a-z][a-z0-9-]*)+$/)
@@ -2304,17 +2781,20 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
2304
2781
  idempotentHint: false,
2305
2782
  openWorldHint: true,
2306
2783
  },
2307
- }, async ({ workspaceId, name, action, arguments: capabilityArguments, file }, extra) => {
2308
- if (remoteWorkspaces.has(workspaceId)) {
2309
- return remoteWorkspaces.capability(workspaceId, {
2784
+ }, async ({ workspaceId, member, name, action, arguments: capabilityArguments, file }, extra) => {
2785
+ const target = resolveExecutionTarget(workspaceId, member);
2786
+ const executionWorkspaceId = target.executionWorkspaceId;
2787
+ const executionContext = await prepareExecutionContext(target, extra._meta, extra.signal, extra.sessionId);
2788
+ if (remoteWorkspaces.has(executionWorkspaceId)) {
2789
+ return presentExecutionResult(await remoteWorkspaces.capability(executionWorkspaceId, {
2310
2790
  name,
2311
2791
  action,
2312
2792
  ...(capabilityArguments !== undefined ? { arguments: capabilityArguments } : {}),
2313
2793
  ...(file !== undefined ? { file } : {}),
2314
- }, hostScopeIdFor(extra._meta, extra.sessionId));
2794
+ }, hostScopeIdFor(extra._meta, extra.sessionId)), target);
2315
2795
  }
2316
2796
  if (action === "run" && name === "batch.execute") {
2317
- const workspace = workspaces.getWorkspace(workspaceId);
2797
+ const workspace = workspaces.getWorkspace(executionWorkspaceId);
2318
2798
  const startedAt = performance.now();
2319
2799
  try {
2320
2800
  const execution = await capabilityRegistry.run(name, capabilityArguments ?? {}, capabilityContextFor(workspace), {
@@ -2335,7 +2815,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
2335
2815
  success: true,
2336
2816
  durationMs: Math.round(performance.now() - startedAt),
2337
2817
  });
2338
- return result;
2818
+ return presentExecutionResult(result, target);
2339
2819
  }
2340
2820
  catch (error) {
2341
2821
  if (extra.signal.aborted)
@@ -2358,18 +2838,14 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
2358
2838
  capability: name,
2359
2839
  action,
2360
2840
  }, result.content, startedAt);
2361
- return result;
2841
+ return presentExecutionResult(result, target);
2362
2842
  }
2363
2843
  }
2364
2844
  if (action === "run") {
2365
- return coreOperations.capabilityRun({ workspaceId, name, arguments: capabilityArguments, file }, {
2366
- requestMeta: extra._meta,
2367
- signal: extra.signal,
2368
- sessionId: extra.sessionId,
2369
- });
2845
+ return presentExecutionResult(await coreOperations.capabilityRun({ workspaceId: executionWorkspaceId, name, arguments: capabilityArguments, file }, executionContext), target);
2370
2846
  }
2371
- const workspace = workspaces.getWorkspace(workspaceId);
2372
- return runActivityToolWithHooks(activityLifecycle, hooks, workspace, hostScopeIdFor(extra._meta, extra.sessionId), { workspaceId, name, action, arguments: capabilityArguments, file }, {
2847
+ const workspace = workspaces.getWorkspace(executionWorkspaceId);
2848
+ return runActivityToolWithHooks(activityLifecycle, hooks, workspace, hostScopeIdFor(extra._meta, extra.sessionId), activityRequestFor({ workspaceId: executionWorkspaceId, name, action, arguments: capabilityArguments, file }, executionContext), {
2373
2849
  signal: extra.signal,
2374
2850
  tool: toolNames.capability,
2375
2851
  invocation: workspaceHookInvocation(workspace),
@@ -2422,11 +2898,11 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
2422
2898
  return result;
2423
2899
  }
2424
2900
  },
2425
- });
2901
+ }, activityRelationFor(executionContext)).then((result) => presentExecutionResult(result, target));
2426
2902
  });
2427
2903
  registerAppTool(server, toolNames.closeWorkspace, {
2428
2904
  title: "Close workspace",
2429
- description: "Close one workspace after the user chooses cleanup. Checkout-backed workspaces release only the logical handle. Managed-worktree-backed workspaces finalize the existing safe worktree lifecycle, including hooks, commit/integration, and cleanup; provide commitMessage for that mode. Running processes prevent closure; completed background results are delivered with the close response when available.",
2905
+ description: "Close one workspace after the user chooses cleanup. Composite Workspaces dissolve here: only the Composite identity and member links are removed; member Workspaces, files, processes, worktrees, and relay routes remain intact. Checkout-backed workspaces release only the logical handle. Managed-worktree-backed workspaces run the safe finalize lifecycle (hooks, commit, fast-forward integration, cleanup) and require commitMessage. Running processes block ordinary Workspace closure.",
2430
2906
  inputSchema: {
2431
2907
  workspaceId: z.string().describe("Workspace identifier to close."),
2432
2908
  commitMessage: z
@@ -2437,7 +2913,15 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
2437
2913
  },
2438
2914
  outputSchema: resultOutputSchema({
2439
2915
  workspaceId: z.string(),
2440
- mode: z.enum(["checkout", "worktree"]),
2916
+ kind: z.enum(["workspace", "composite"]).optional(),
2917
+ mode: z.enum(["checkout", "worktree"]).optional(),
2918
+ name: z.string().optional(),
2919
+ members: z.array(z.object({
2920
+ name: z.string(),
2921
+ purpose: z.string(),
2922
+ workspaceId: z.string(),
2923
+ })).optional(),
2924
+ dissolved: z.boolean().optional(),
2441
2925
  sourceRoot: z.string().optional(),
2442
2926
  branch: z.string().optional(),
2443
2927
  targetBranch: z.string().optional(),
@@ -2449,6 +2933,43 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
2449
2933
  _meta: {},
2450
2934
  annotations: WRITE_TOOL_ANNOTATIONS,
2451
2935
  }, async ({ workspaceId, commitMessage }, extra) => {
2936
+ if (compositeWorkspaces.has(workspaceId)) {
2937
+ if (commitMessage !== undefined) {
2938
+ throw new Error("close_workspace commitMessage is not valid when dissolving a Composite Workspace.");
2939
+ }
2940
+ const composite = compositeWorkspaces.dissolve(workspaceId);
2941
+ compositeActivity.forgetComposite(workspaceId);
2942
+ workspacePanelStates.delete(workspaceId);
2943
+ const result = [
2944
+ `Dissolved Composite Workspace ${composite.name} (${workspaceId}).`,
2945
+ composite.members.length > 0
2946
+ ? `Preserved member Workspaces: ${composite.members.map((member) => `${member.name} [${member.workspaceId}]`).join(", ")}.`
2947
+ : "The Composite Workspace had no members.",
2948
+ "Member Workspace handles, managed worktrees, processes, files, and Workspace Relay routes were not closed or cleaned up.",
2949
+ ].join("\n");
2950
+ return {
2951
+ content: [textBlock(result)],
2952
+ _meta: {
2953
+ tool: toolNames.closeWorkspace,
2954
+ card: {
2955
+ workspaceId,
2956
+ kind: "composite",
2957
+ name: composite.name,
2958
+ members: composite.members,
2959
+ dissolved: true,
2960
+ payload: { content: [textBlock(result)] },
2961
+ },
2962
+ },
2963
+ structuredContent: {
2964
+ result,
2965
+ workspaceId,
2966
+ kind: "composite",
2967
+ name: composite.name,
2968
+ members: composite.members,
2969
+ dissolved: true,
2970
+ },
2971
+ };
2972
+ }
2452
2973
  if (remoteWorkspaces.has(workspaceId)) {
2453
2974
  const response = await remoteWorkspaces.closeWorkspace(workspaceId, commitMessage, hostScopeIdFor(extra._meta, extra.sessionId));
2454
2975
  workspacePanelStates.delete(workspaceId);
@@ -2565,6 +3086,10 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
2565
3086
  workspaceId: z
2566
3087
  .string()
2567
3088
  .describe("Workspace identifier returned by open_workspace."),
3089
+ member: z
3090
+ .string()
3091
+ .optional()
3092
+ .describe("Required for a Composite Workspace; explicit member name that owns this operation."),
2568
3093
  path: z
2569
3094
  .string()
2570
3095
  .optional()
@@ -2602,30 +3127,27 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
2602
3127
  }),
2603
3128
  ...toolWidgetDescriptorMeta(config, "read"),
2604
3129
  annotations: { readOnlyHint: true },
2605
- }, async ({ workspaceId, path, paths, offset, limit }, extra) => {
3130
+ }, async ({ workspaceId, member, path, paths, offset, limit }, extra) => {
2606
3131
  if ((path === undefined) === (paths === undefined)) {
2607
3132
  throw new Error("read requires exactly one of path or paths.");
2608
3133
  }
2609
- if (remoteWorkspaces.has(workspaceId)) {
2610
- return remoteWorkspaces.read(workspaceId, { path, paths, offset, limit }, hostScopeIdFor(extra._meta, extra.sessionId));
3134
+ const target = resolveExecutionTarget(workspaceId, member);
3135
+ const executionWorkspaceId = target.executionWorkspaceId;
3136
+ const executionContext = await prepareExecutionContext(target, extra._meta, extra.signal, extra.sessionId);
3137
+ if (remoteWorkspaces.has(executionWorkspaceId)) {
3138
+ return presentExecutionResult(await remoteWorkspaces.read(executionWorkspaceId, { path, paths, offset, limit }, hostScopeIdFor(extra._meta, extra.sessionId)), target);
2611
3139
  }
2612
3140
  if (path !== undefined) {
2613
- return coreOperations.read({ workspaceId, path, offset, limit }, {
2614
- requestMeta: extra._meta,
2615
- signal: extra.signal,
2616
- sessionId: extra.sessionId,
2617
- });
3141
+ return presentExecutionResult(await coreOperations.read({ workspaceId: executionWorkspaceId, path, offset, limit }, executionContext), target);
2618
3142
  }
2619
- const workspace = workspaces.getWorkspace(workspaceId);
3143
+ const workspace = workspaces.getWorkspace(executionWorkspaceId);
2620
3144
  let response;
2621
- await runActivityTool(activityLifecycle, workspace, hostScopeIdFor(extra._meta, extra.sessionId), toolNames.read, { workspaceId, paths, offset, limit }, async (parentContext) => {
3145
+ await runActivityTool(activityLifecycle, workspace, hostScopeIdFor(extra._meta, extra.sessionId), toolNames.read, activityRequestFor({ workspaceId: executionWorkspaceId, paths, offset, limit }, executionContext), async (parentContext) => {
2622
3146
  const execution = await executeBulkRead({
2623
3147
  paths: paths,
2624
3148
  signal: extra.signal,
2625
- run: (childPath) => coreOperations.read({ workspaceId, path: childPath, offset, limit }, {
2626
- requestMeta: extra._meta,
2627
- signal: extra.signal,
2628
- sessionId: extra.sessionId,
3149
+ run: (childPath) => coreOperations.read({ workspaceId: executionWorkspaceId, path: childPath, offset, limit }, {
3150
+ ...executionContext,
2629
3151
  parentActivityId: parentContext.activityId,
2630
3152
  turnId: parentContext.turnId,
2631
3153
  }),
@@ -2666,10 +3188,10 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
2666
3188
  };
2667
3189
  }, (summary) => summary.failed > 0
2668
3190
  ? { type: "failed", error: `${summary.failed} of ${summary.childCount} child Reads failed.` }
2669
- : { type: "succeeded" });
3191
+ : { type: "succeeded" }, activityRelationFor(executionContext));
2670
3192
  if (!response)
2671
3193
  throw new Error("Bulk Read completed without a response.");
2672
- return response;
3194
+ return presentExecutionResult(response, target);
2673
3195
  });
2674
3196
  if (config.toolMode !== "codex") {
2675
3197
  registerAppTool(server, toolNames.write, {
@@ -2679,6 +3201,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
2679
3201
  workspaceId: z
2680
3202
  .string()
2681
3203
  .describe("Workspace identifier returned by open_workspace."),
3204
+ member: z.string().optional().describe("Required for a Composite Workspace; explicit member name that owns this operation."),
2682
3205
  path: z
2683
3206
  .string()
2684
3207
  .describe("File path to write, relative to the workspace root or absolute inside the OS temp directory."),
@@ -2687,15 +3210,14 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
2687
3210
  outputSchema: resultOutputSchema(),
2688
3211
  ...toolWidgetDescriptorMeta(config, "write"),
2689
3212
  annotations: WRITE_TOOL_ANNOTATIONS,
2690
- }, async ({ workspaceId, ...input }, extra) => {
2691
- if (remoteWorkspaces.has(workspaceId)) {
2692
- return remoteWorkspaces.write(workspaceId, input, hostScopeIdFor(extra._meta, extra.sessionId));
3213
+ }, async ({ workspaceId, member, ...input }, extra) => {
3214
+ const target = resolveExecutionTarget(workspaceId, member);
3215
+ const executionWorkspaceId = target.executionWorkspaceId;
3216
+ const executionContext = await prepareExecutionContext(target, extra._meta, extra.signal, extra.sessionId);
3217
+ if (remoteWorkspaces.has(executionWorkspaceId)) {
3218
+ return presentExecutionResult(await remoteWorkspaces.write(executionWorkspaceId, input, hostScopeIdFor(extra._meta, extra.sessionId)), target);
2693
3219
  }
2694
- return coreOperations.write({ workspaceId, ...input }, {
2695
- requestMeta: extra._meta,
2696
- signal: extra.signal,
2697
- sessionId: extra.sessionId,
2698
- });
3220
+ return presentExecutionResult(await coreOperations.write({ workspaceId: executionWorkspaceId, ...input }, executionContext), target);
2699
3221
  });
2700
3222
  registerAppTool(server, toolNames.edit, {
2701
3223
  title: "Edit file",
@@ -2704,6 +3226,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
2704
3226
  workspaceId: z
2705
3227
  .string()
2706
3228
  .describe("Workspace identifier returned by open_workspace."),
3229
+ member: z.string().optional().describe("Required for a Composite Workspace; explicit member name that owns this operation."),
2707
3230
  path: z
2708
3231
  .string()
2709
3232
  .optional()
@@ -2737,25 +3260,20 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
2737
3260
  }),
2738
3261
  ...toolWidgetDescriptorMeta(config, "edit"),
2739
3262
  annotations: EDIT_TOOL_ANNOTATIONS,
2740
- }, async ({ workspaceId, path, paths, edits }, extra) => {
3263
+ }, async ({ workspaceId, member, path, paths, edits }, extra) => {
2741
3264
  if ((path === undefined) === (paths === undefined)) {
2742
3265
  throw new Error("edit requires exactly one of path or paths.");
2743
3266
  }
2744
- if (remoteWorkspaces.has(workspaceId)) {
2745
- return remoteWorkspaces.edit(workspaceId, { path, paths, edits }, hostScopeIdFor(extra._meta, extra.sessionId));
3267
+ const target = resolveExecutionTarget(workspaceId, member);
3268
+ const executionWorkspaceId = target.executionWorkspaceId;
3269
+ const executionContext = await prepareExecutionContext(target, extra._meta, extra.signal, extra.sessionId);
3270
+ if (remoteWorkspaces.has(executionWorkspaceId)) {
3271
+ return presentExecutionResult(await remoteWorkspaces.edit(executionWorkspaceId, { path, paths, edits }, hostScopeIdFor(extra._meta, extra.sessionId)), target);
2746
3272
  }
2747
3273
  if (path !== undefined) {
2748
- return coreOperations.edit({ workspaceId, path, edits }, {
2749
- requestMeta: extra._meta,
2750
- signal: extra.signal,
2751
- sessionId: extra.sessionId,
2752
- });
3274
+ return presentExecutionResult(await coreOperations.edit({ workspaceId: executionWorkspaceId, path, edits }, executionContext), target);
2753
3275
  }
2754
- return nativeBulkMutations.edit({ workspaceId, paths: paths, edits }, {
2755
- requestMeta: extra._meta,
2756
- signal: extra.signal,
2757
- sessionId: extra.sessionId,
2758
- });
3276
+ return presentExecutionResult(await nativeBulkMutations.edit({ workspaceId: executionWorkspaceId, paths: paths, edits }, executionContext), target);
2759
3277
  });
2760
3278
  }
2761
3279
  registerAppTool(server, toolNames.rename, {
@@ -2763,6 +3281,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
2763
3281
  description: toolDescriptions.rename,
2764
3282
  inputSchema: {
2765
3283
  workspaceId: z.string().describe("Workspace identifier returned by open_workspace."),
3284
+ member: z.string().optional().describe("Required for a Composite Workspace; explicit member name that owns this operation."),
2766
3285
  path: z.string().describe("Source file or directory path relative to the workspace root, or absolute inside the OS temp directory."),
2767
3286
  newPath: z.string().describe("Destination path relative to the workspace root, or absolute inside the OS temp directory. The destination must not already exist."),
2768
3287
  },
@@ -2773,21 +3292,21 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
2773
3292
  }),
2774
3293
  ...toolWidgetDescriptorMeta(config, "edit"),
2775
3294
  annotations: EDIT_TOOL_ANNOTATIONS,
2776
- }, async ({ workspaceId, path, newPath }, extra) => {
2777
- if (remoteWorkspaces.has(workspaceId)) {
2778
- return remoteWorkspaces.rename(workspaceId, { path, newPath }, hostScopeIdFor(extra._meta, extra.sessionId));
3295
+ }, async ({ workspaceId, member, path, newPath }, extra) => {
3296
+ const target = resolveExecutionTarget(workspaceId, member);
3297
+ const executionWorkspaceId = target.executionWorkspaceId;
3298
+ const executionContext = await prepareExecutionContext(target, extra._meta, extra.signal, extra.sessionId);
3299
+ if (remoteWorkspaces.has(executionWorkspaceId)) {
3300
+ return presentExecutionResult(await remoteWorkspaces.rename(executionWorkspaceId, { path, newPath }, hostScopeIdFor(extra._meta, extra.sessionId)), target);
2779
3301
  }
2780
- return coreOperations.rename({ workspaceId, path, newPath }, {
2781
- requestMeta: extra._meta,
2782
- signal: extra.signal,
2783
- sessionId: extra.sessionId,
2784
- });
3302
+ return presentExecutionResult(await coreOperations.rename({ workspaceId: executionWorkspaceId, path, newPath }, executionContext), target);
2785
3303
  });
2786
3304
  registerAppTool(server, toolNames.delete, {
2787
3305
  title: "Delete path",
2788
3306
  description: toolDescriptions.delete,
2789
3307
  inputSchema: {
2790
3308
  workspaceId: z.string().describe("Workspace identifier returned by open_workspace."),
3309
+ member: z.string().optional().describe("Required for a Composite Workspace; explicit member name that owns this operation."),
2791
3310
  path: z.string().optional().describe("One file or directory path to delete. Use exactly one of path or paths."),
2792
3311
  paths: z
2793
3312
  .array(z.string())
@@ -2813,25 +3332,20 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
2813
3332
  }),
2814
3333
  ...toolWidgetDescriptorMeta(config, "edit"),
2815
3334
  annotations: EDIT_TOOL_ANNOTATIONS,
2816
- }, async ({ workspaceId, path, paths, recursive }, extra) => {
3335
+ }, async ({ workspaceId, member, path, paths, recursive }, extra) => {
2817
3336
  if ((path === undefined) === (paths === undefined)) {
2818
3337
  throw new Error("delete requires exactly one of path or paths.");
2819
3338
  }
2820
- if (remoteWorkspaces.has(workspaceId)) {
2821
- return remoteWorkspaces.delete(workspaceId, { path, paths, recursive }, hostScopeIdFor(extra._meta, extra.sessionId));
3339
+ const target = resolveExecutionTarget(workspaceId, member);
3340
+ const executionWorkspaceId = target.executionWorkspaceId;
3341
+ const executionContext = await prepareExecutionContext(target, extra._meta, extra.signal, extra.sessionId);
3342
+ if (remoteWorkspaces.has(executionWorkspaceId)) {
3343
+ return presentExecutionResult(await remoteWorkspaces.delete(executionWorkspaceId, { path, paths, recursive }, hostScopeIdFor(extra._meta, extra.sessionId)), target);
2822
3344
  }
2823
3345
  if (path !== undefined) {
2824
- return coreOperations.delete({ workspaceId, path, recursive }, {
2825
- requestMeta: extra._meta,
2826
- signal: extra.signal,
2827
- sessionId: extra.sessionId,
2828
- });
3346
+ return presentExecutionResult(await coreOperations.delete({ workspaceId: executionWorkspaceId, path, recursive }, executionContext), target);
2829
3347
  }
2830
- return nativeBulkMutations.delete({ workspaceId, paths: paths, recursive }, {
2831
- requestMeta: extra._meta,
2832
- signal: extra.signal,
2833
- sessionId: extra.sessionId,
2834
- });
3348
+ return presentExecutionResult(await nativeBulkMutations.delete({ workspaceId: executionWorkspaceId, paths: paths, recursive }, executionContext), target);
2835
3349
  });
2836
3350
  if (config.toolMode === "codex") {
2837
3351
  registerAppTool(server, "apply_patch", {
@@ -2841,6 +3355,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
2841
3355
  workspaceId: z
2842
3356
  .string()
2843
3357
  .describe("Workspace identifier returned by open_workspace."),
3358
+ member: z.string().optional().describe("Required for a Composite Workspace; explicit member name that owns this patch."),
2844
3359
  patch: z
2845
3360
  .string()
2846
3361
  .describe("Patch text enclosed by *** Begin Patch and *** End Patch markers."),
@@ -2856,9 +3371,15 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
2856
3371
  }),
2857
3372
  ...toolWidgetDescriptorMeta(config, "edit"),
2858
3373
  annotations: EDIT_TOOL_ANNOTATIONS,
2859
- }, async ({ workspaceId, patch }, extra) => {
2860
- const workspace = workspaces.getWorkspace(workspaceId);
2861
- return runActivityToolWithHooks(activityLifecycle, hooks, workspace, hostScopeIdFor(extra._meta, extra.sessionId), { workspaceId, patch }, {
3374
+ }, async ({ workspaceId, member, patch }, extra) => {
3375
+ const target = resolveExecutionTarget(workspaceId, member);
3376
+ const executionWorkspaceId = target.executionWorkspaceId;
3377
+ const executionContext = await prepareExecutionContext(target, extra._meta, extra.signal, extra.sessionId);
3378
+ if (remoteWorkspaces.has(executionWorkspaceId)) {
3379
+ return presentExecutionResult(await remoteWorkspaces.applyPatch(executionWorkspaceId, { patch }, hostScopeIdFor(extra._meta, extra.sessionId)), target);
3380
+ }
3381
+ const workspace = workspaces.getWorkspace(executionWorkspaceId);
3382
+ return runActivityToolWithHooks(activityLifecycle, hooks, workspace, hostScopeIdFor(extra._meta, extra.sessionId), activityRequestFor({ workspaceId: executionWorkspaceId, patch }, executionContext), {
2862
3383
  signal: extra.signal,
2863
3384
  tool: "apply_patch",
2864
3385
  invocation: workspaceHookInvocation(workspace),
@@ -2886,7 +3407,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
2886
3407
  _meta: {
2887
3408
  tool: "apply_patch",
2888
3409
  card: {
2889
- workspaceId,
3410
+ workspaceId: executionWorkspaceId,
2890
3411
  path: displayPath,
2891
3412
  summary: {
2892
3413
  files: applied.files.length,
@@ -2905,7 +3426,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
2905
3426
  },
2906
3427
  };
2907
3428
  },
2908
- });
3429
+ }, activityRelationFor(executionContext)).then((result) => presentExecutionResult(result, target));
2909
3430
  });
2910
3431
  }
2911
3432
  if (config.toolMode !== "codex") {
@@ -2916,6 +3437,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
2916
3437
  workspaceId: z
2917
3438
  .string()
2918
3439
  .describe("Workspace identifier returned by open_workspace."),
3440
+ member: z.string().optional().describe("Required for a Composite Workspace; explicit member name that owns this process operation."),
2919
3441
  action: z
2920
3442
  .enum(["run", "process", "output"])
2921
3443
  .optional()
@@ -2989,9 +3511,12 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
2989
3511
  outputSchema: processOutputSchema(),
2990
3512
  ...toolWidgetDescriptorMeta(config, "shell"),
2991
3513
  annotations: SHELL_TOOL_ANNOTATIONS,
2992
- }, async ({ workspaceId, action = "run", command, processId, outputId, input, interrupt, tty, columns, rows, workingDirectory, yieldTimeMs, timeoutMs, maxOutputTokens, }, extra) => {
2993
- if (remoteWorkspaces.has(workspaceId)) {
2994
- return remoteWorkspaces.bash(workspaceId, {
3514
+ }, async ({ workspaceId, member, action = "run", command, processId, outputId, input, interrupt, tty, columns, rows, workingDirectory, yieldTimeMs, timeoutMs, maxOutputTokens, }, extra) => {
3515
+ const target = resolveExecutionTarget(workspaceId, member);
3516
+ const executionWorkspaceId = target.executionWorkspaceId;
3517
+ const executionContext = await prepareExecutionContext(target, extra._meta, extra.signal, extra.sessionId);
3518
+ if (remoteWorkspaces.has(executionWorkspaceId)) {
3519
+ return presentExecutionResult(await remoteWorkspaces.bash(executionWorkspaceId, {
2995
3520
  action,
2996
3521
  ...(command !== undefined ? { command } : {}),
2997
3522
  ...(processId !== undefined ? { processId } : {}),
@@ -3005,17 +3530,17 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
3005
3530
  ...(yieldTimeMs !== undefined ? { yieldTimeMs } : {}),
3006
3531
  ...(timeoutMs !== undefined ? { timeoutMs } : {}),
3007
3532
  ...(maxOutputTokens !== undefined ? { maxOutputTokens } : {}),
3008
- }, hostScopeIdFor(extra._meta, extra.sessionId));
3533
+ }, hostScopeIdFor(extra._meta, extra.sessionId)), target);
3009
3534
  }
3010
- const workspace = workspaces.getWorkspace(workspaceId);
3535
+ const workspace = workspaces.getWorkspace(executionWorkspaceId);
3011
3536
  if (action === "run") {
3012
3537
  if (!command)
3013
3538
  throw new Error("bash action=run requires command.");
3014
3539
  if (processId !== undefined || outputId !== undefined || input !== undefined || interrupt !== undefined) {
3015
3540
  throw new Error("bash action=run does not accept processId, outputId, input, or interrupt.");
3016
3541
  }
3017
- return coreOperations.shellRun({
3018
- workspaceId,
3542
+ return presentExecutionResult(await coreOperations.shellRun({
3543
+ workspaceId: executionWorkspaceId,
3019
3544
  command,
3020
3545
  surface: "bash",
3021
3546
  tty,
@@ -3025,11 +3550,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
3025
3550
  yieldTimeMs,
3026
3551
  timeoutMs,
3027
3552
  maxOutputTokens,
3028
- }, {
3029
- requestMeta: extra._meta,
3030
- signal: extra.signal,
3031
- sessionId: extra.sessionId,
3032
- });
3553
+ }, executionContext), target);
3033
3554
  }
3034
3555
  if (action === "output") {
3035
3556
  if (!outputId)
@@ -3044,8 +3565,8 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
3044
3565
  tool: toolNames.shell,
3045
3566
  invocation: workspaceHookInvocation(workspace),
3046
3567
  payload: { action, outputId },
3047
- operation: async () => durableOutputResponse(toolNames.shell, workspaceId, readWorkspaceBashOutput(bashOutputStore, workspaceId, outputId)),
3048
- });
3568
+ operation: async () => durableOutputResponse(toolNames.shell, executionWorkspaceId, readWorkspaceBashOutput(bashOutputStore, executionWorkspaceId, outputId)),
3569
+ }).then((result) => presentExecutionResult(result, target));
3049
3570
  }
3050
3571
  if (outputId !== undefined)
3051
3572
  throw new Error("bash action=process does not accept outputId.");
@@ -3073,7 +3594,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
3073
3594
  operation: async () => {
3074
3595
  const startedAt = performance.now();
3075
3596
  const snapshot = await processSessions.write({
3076
- workspaceId,
3597
+ workspaceId: executionWorkspaceId,
3077
3598
  processId,
3078
3599
  chars: interrupt ? "\u0003" : input,
3079
3600
  columns,
@@ -3091,7 +3612,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
3091
3612
  success: snapshot.running || snapshot.exitCode === 0,
3092
3613
  durationMs: Math.round(performance.now() - startedAt),
3093
3614
  });
3094
- const response = processToolResponse(toolNames.shell, workspaceId, snapshot, {
3615
+ const response = processToolResponse(toolNames.shell, executionWorkspaceId, snapshot, {
3095
3616
  action,
3096
3617
  processId,
3097
3618
  inputLength: input?.length ?? 0,
@@ -3103,12 +3624,20 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
3103
3624
  if (!snapshot.running) {
3104
3625
  recordBashCompletion(activityLifecycle, bashOutputStore, snapshot.outputId);
3105
3626
  }
3106
- return response;
3627
+ return presentExecutionResult(response, target);
3107
3628
  },
3108
3629
  });
3109
3630
  });
3110
3631
  }
3111
- registerProcessTools(server, config, workspaces, processSessions, hooks, activityLifecycle, bashOutputStore, (input, context) => coreOperations.shellRun(input, context));
3632
+ registerProcessTools(server, config, workspaces, processSessions, hooks, activityLifecycle, bashOutputStore, (input, context) => coreOperations.shellRun(input, context), {
3633
+ resolve: resolveExecutionTarget,
3634
+ prepare: prepareExecutionContext,
3635
+ present: presentExecutionResult,
3636
+ isRemote: (workspaceId) => remoteWorkspaces.has(workspaceId),
3637
+ execCommandRemote: (workspaceId, input, conversationScopeId) => remoteWorkspaces.execCommand(workspaceId, input, conversationScopeId),
3638
+ writeStdinRemote: (workspaceId, input, conversationScopeId) => remoteWorkspaces.writeStdin(workspaceId, input, conversationScopeId),
3639
+ hostScopeIdFor,
3640
+ });
3112
3641
  return server;
3113
3642
  }
3114
3643
  export function createServer(config = loadConfig(), options = {}) {