@akira-tl/forgerelay 0.6.1 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/README.md +38 -0
  3. package/capabilities/subagents/GUIDE.md +100 -40
  4. package/dist/activity/lifecycle.js +1 -1
  5. package/dist/activity/mcp-query-tools.js +1 -0
  6. package/dist/activity/query-service.js +1 -0
  7. package/dist/capabilities.js +1 -1
  8. package/dist/capability-registry.js +34 -0
  9. package/dist/cli.js +80 -165
  10. package/dist/composite-activity.js +155 -0
  11. package/dist/composite-workspaces.js +197 -0
  12. package/dist/db/migrations.js +14 -0
  13. package/dist/db/schema.js +6 -0
  14. package/dist/remote-workspace-relay.js +16 -0
  15. package/dist/server.js +698 -172
  16. package/dist/{local-agent-targets.js → subagents/cli-target.js} +6 -6
  17. package/dist/{local-agent-profiles.js → subagents/profiles.js} +13 -5
  18. package/dist/subagents/providers/adapters/acp.js +148 -0
  19. package/dist/subagents/providers/adapters/claude.js +75 -0
  20. package/dist/{local-agent-runtime.js → subagents/providers/adapters/codex.js} +10 -4
  21. package/dist/subagents/providers/adapters/opencode.js +137 -0
  22. package/dist/subagents/providers/adapters/pi.js +232 -0
  23. package/dist/{local-agent-availability.js → subagents/providers/availability.js} +24 -10
  24. package/dist/subagents/providers/continuation.js +11 -0
  25. package/dist/subagents/providers/contract.js +1 -0
  26. package/dist/subagents/providers/registry.js +26 -0
  27. package/dist/subagents/providers/shared.js +40 -0
  28. package/dist/subagents/sessions/capability.js +214 -0
  29. package/dist/subagents/sessions/delivery-mailbox.js +115 -0
  30. package/dist/subagents/sessions/execution.js +171 -0
  31. package/dist/subagents/sessions/manager.js +107 -0
  32. package/dist/subagents/sessions/mcp/audit.js +85 -0
  33. package/dist/subagents/sessions/mcp/runtime.js +19 -0
  34. package/dist/{local-agent-store.js → subagents/sessions/store.js} +75 -39
  35. package/dist/ui/.vite/manifest.json +33 -33
  36. package/dist/ui/activity-panel-app.html +3 -3
  37. package/dist/ui/assets/{activity-panel-app-CjZVvVNc.js → activity-panel-app-E1ju2dqI.js} +1 -1
  38. package/dist/ui/assets/{heavy-payload-vGgBRvNX.js → heavy-payload-CeW-n9w5.js} +1 -1
  39. package/dist/ui/assets/{review-payload-4erWKckt.js → review-payload-B9CO298v.js} +1 -1
  40. package/dist/ui/assets/{scrollbar-CaOPzUJd.js → scrollbar-C2twAENW.js} +1 -1
  41. package/dist/ui/assets/workspace-app-BztEvZIC.js +5 -0
  42. package/dist/ui/assets/{workspace-app-DkAiSl_0.js → workspace-app-CwbJnb_w.js} +1 -1
  43. package/dist/ui/assets/workspace-app-YnUST8IP.css +1 -0
  44. package/dist/ui/assets/workspace-app-rKuhdae8.js +1 -0
  45. package/dist/ui/assets/workspace-lifecycle-app-CEfMdudP.js +1 -0
  46. package/dist/ui/workspace-app.html +4 -4
  47. package/dist/ui/workspace-lifecycle-app.html +4 -4
  48. package/dist/workspaces.js +3 -3
  49. package/docs/chatgpt-coding-workflow.md +2 -9
  50. package/docs/configuration.md +23 -0
  51. package/docs/debugging.md +7 -0
  52. package/docs/roadmap.md +42 -7
  53. package/package.json +2 -2
  54. package/scripts/debug/runtime.mjs +23 -1
  55. package/scripts/debug/runtime.test.mjs +14 -2
  56. package/scripts/debug/serve.mjs +4 -4
  57. package/scripts/release/release-gate.test.mjs +2 -2
  58. package/dist/local-agent-adapters.js +0 -653
  59. package/dist/ui/assets/workspace-app-CcrHAUIn.css +0 -1
  60. package/dist/ui/assets/workspace-app-DJmkPYJC.js +0 -1
  61. package/dist/ui/assets/workspace-app-QyauBrJX.js +0 -5
  62. package/dist/ui/assets/workspace-lifecycle-app-BIXEo53I.js +0 -1
  63. /package/dist/{local-agent-path.js → subagents/providers/path.js} +0 -0
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";
@@ -52,8 +54,10 @@ import { shutdownHttpServer } from "./server-shutdown.js";
52
54
  import { formatPathForPrompt } from "./skills.js";
53
55
  import { createWorkspaceStore } from "./workspace-store.js";
54
56
  import { formatAgentsPath, WorkspaceRegistry } from "./workspaces.js";
55
- import { summarizeLocalAgentProfile } from "./local-agent-profiles.js";
56
- import { formatLocalAgentProviderAvailabilitySummary, getLocalAgentProviderAvailabilitySnapshot, } from "./local-agent-availability.js";
57
+ import { formatAvailableSubagentProfile, summarizeSubagentProfile } from "./subagents/profiles.js";
58
+ import { formatSubagentProviderAvailabilitySummary, formatUnavailableSubagentProvider, getSubagentProviderAvailabilitySnapshot, } from "./subagents/providers/availability.js";
59
+ import { capabilityActivityAuditRequest, capabilityActivityAuditResult } from "./subagents/sessions/mcp/audit.js";
60
+ import { createSubagentMcpRuntime } from "./subagents/sessions/mcp/runtime.js";
57
61
  // Legacy MCP Streamable HTTP clients can reconnect without closing the previous
58
62
  // transport. Bound stale transport-session retention so abandoned transports do
59
63
  // not accumulate for the life of the process.
@@ -129,17 +133,6 @@ async function assertWorkspaceInstructionsLoadedBeforeSideEffect(workspaces, wor
129
133
  "Apply these instructions, then retry this tool call. No mutation or command was executed.",
130
134
  ].join("\n"));
131
135
  }
132
- function formatVisibleAgent(agent) {
133
- const model = agent.model ? `, model ${agent.model}` : "";
134
- const thinking = agent.thinking ? `, thinking ${agent.thinking}` : "";
135
- const availability = agent.providerAvailable === false
136
- ? `, unavailable: ${agent.providerUnavailableReason ?? "provider unavailable"}`
137
- : "";
138
- return `${agent.name} (${agent.provider}${model}${thinking}${availability})`;
139
- }
140
- function formatUnavailableAgentProvider(provider) {
141
- return `${provider.name} (${provider.reason ?? "unavailable"})`;
142
- }
143
136
  function resultOutputSchema(extra = {}) {
144
137
  return {
145
138
  result: z
@@ -226,9 +219,10 @@ const workspaceLocalAgentOutputSchema = z.object({
226
219
  providerAvailable: z.boolean().optional(),
227
220
  providerUnavailableReason: z.string().optional(),
228
221
  });
229
- const workspaceLocalAgentProviderOutputSchema = z.object({
222
+ const workspaceSubagentProviderOutputSchema = z.object({
230
223
  name: z.string(),
231
224
  available: z.boolean(),
225
+ continuationSupported: z.boolean(),
232
226
  reason: z.string().optional(),
233
227
  });
234
228
  const workspaceAvailableAgentsFileOutputSchema = z.object({
@@ -876,6 +870,39 @@ function toolResultContent(result) {
876
870
  const content = result.content;
877
871
  return Array.isArray(content) ? content : [];
878
872
  }
873
+ function remapCompositeToolResult(result, executionWorkspaceId, compositeWorkspaceId, member) {
874
+ if (typeof result !== "object" || result === null)
875
+ return result;
876
+ const record = result;
877
+ const remapped = replaceWorkspaceIdentity(record, executionWorkspaceId, compositeWorkspaceId);
878
+ const meta = typeof remapped._meta === "object" && remapped._meta !== null
879
+ ? { ...remapped._meta }
880
+ : undefined;
881
+ if (meta) {
882
+ const card = typeof meta.card === "object" && meta.card !== null
883
+ ? { ...meta.card, workspaceId: compositeWorkspaceId, member }
884
+ : undefined;
885
+ if (card)
886
+ meta.card = card;
887
+ remapped._meta = meta;
888
+ }
889
+ const structured = typeof remapped.structuredContent === "object" && remapped.structuredContent !== null
890
+ ? { ...remapped.structuredContent, member }
891
+ : undefined;
892
+ if (structured)
893
+ remapped.structuredContent = structured;
894
+ return remapped;
895
+ }
896
+ function replaceWorkspaceIdentity(value, from, to) {
897
+ if (typeof value === "string")
898
+ return value.split(from).join(to);
899
+ if (Array.isArray(value))
900
+ return value.map((entry) => replaceWorkspaceIdentity(entry, from, to));
901
+ if (!value || typeof value !== "object")
902
+ return value;
903
+ return Object.fromEntries(Object.entries(value)
904
+ .map(([key, entry]) => [key, replaceWorkspaceIdentity(entry, from, to)]));
905
+ }
879
906
  function toolResultAgentsFiles(result) {
880
907
  if (typeof result !== "object" || result === null)
881
908
  return [];
@@ -956,7 +983,15 @@ function activityRelationFor(context) {
956
983
  ...(context.turnId ? { turnId: context.turnId } : {}),
957
984
  };
958
985
  }
959
- function runActivityTool(lifecycle, workspace, conversationScopeId, tool, request, operation, outcome = standardActivityOutcome, relation = {}) {
986
+ function activityRequestFor(input, context) {
987
+ if (!context.activityMember || !input || typeof input !== "object" || Array.isArray(input))
988
+ return input;
989
+ return {
990
+ ...input,
991
+ member: context.activityMember,
992
+ };
993
+ }
994
+ function runActivityTool(lifecycle, workspace, conversationScopeId, tool, request, operation, outcome = standardActivityOutcome, relation = {}, auditResult) {
960
995
  return lifecycle.run({
961
996
  tool,
962
997
  workspace: workspaceActivitySnapshot(workspace),
@@ -964,19 +999,21 @@ function runActivityTool(lifecycle, workspace, conversationScopeId, tool, reques
964
999
  request,
965
1000
  operation,
966
1001
  outcome,
1002
+ ...(auditResult ? { auditResult } : {}),
967
1003
  ...relation,
968
1004
  });
969
1005
  }
970
1006
  function runActivityToolWithHooks(lifecycle, hooks, workspace, conversationScopeId, request, hookOptions, relation = {}) {
971
1007
  return runActivityTool(lifecycle, workspace, conversationScopeId, hookOptions.tool, request, () => runToolWithHooks(hooks, hookOptions), standardActivityOutcome, relation);
972
1008
  }
973
- function registerProcessTools(server, config, workspaces, processSessions, hooks, activityLifecycle, bashOutputStore, shellRun) {
1009
+ function registerProcessTools(server, config, workspaces, processSessions, hooks, activityLifecycle, bashOutputStore, shellRun, routing) {
974
1010
  if (config.toolMode === "codex") {
975
1011
  registerAppTool(server, "exec_command", {
976
1012
  title: "Execute command",
977
1013
  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
1014
  inputSchema: {
979
1015
  workspaceId: z.string().describe("Workspace identifier returned by open_workspace."),
1016
+ member: z.string().optional().describe("Required for a Composite Workspace; explicit member name that owns this process."),
980
1017
  cmd: z.string().min(1).describe("Shell command to execute."),
981
1018
  tty: z
982
1019
  .boolean()
@@ -1013,22 +1050,34 @@ function registerProcessTools(server, config, workspaces, processSessions, hooks
1013
1050
  outputSchema: processOutputSchema(),
1014
1051
  ...toolWidgetDescriptorMeta(config, "shell"),
1015
1052
  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
- }));
1053
+ }, async ({ workspaceId, member, cmd, tty, columns, rows, workingDirectory, yieldTimeMs, timeoutMs, maxOutputTokens }, extra) => {
1054
+ const target = routing.resolve(workspaceId, member);
1055
+ const context = await routing.prepare(target, extra._meta, extra.signal, extra.sessionId);
1056
+ if (routing.isRemote(target.executionWorkspaceId)) {
1057
+ return routing.present(await routing.execCommandRemote(target.executionWorkspaceId, {
1058
+ cmd,
1059
+ ...(tty !== undefined ? { tty } : {}),
1060
+ ...(columns !== undefined ? { columns } : {}),
1061
+ ...(rows !== undefined ? { rows } : {}),
1062
+ ...(workingDirectory !== undefined ? { workingDirectory } : {}),
1063
+ ...(yieldTimeMs !== undefined ? { yieldTimeMs } : {}),
1064
+ ...(timeoutMs !== undefined ? { timeoutMs } : {}),
1065
+ ...(maxOutputTokens !== undefined ? { maxOutputTokens } : {}),
1066
+ }, routing.hostScopeIdFor(extra._meta, extra.sessionId)), target);
1067
+ }
1068
+ return routing.present(await shellRun({
1069
+ workspaceId: target.executionWorkspaceId,
1070
+ command: cmd,
1071
+ surface: "exec_command",
1072
+ tty,
1073
+ columns,
1074
+ rows,
1075
+ workingDirectory,
1076
+ yieldTimeMs,
1077
+ timeoutMs,
1078
+ maxOutputTokens,
1079
+ }, context), target);
1080
+ });
1032
1081
  }
1033
1082
  if (config.toolMode !== "codex")
1034
1083
  return;
@@ -1037,6 +1086,7 @@ function registerProcessTools(server, config, workspaces, processSessions, hooks
1037
1086
  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
1087
  inputSchema: {
1039
1088
  workspaceId: z.string().describe("Workspace identifier used to start the process."),
1089
+ member: z.string().optional().describe("Required for a Composite Workspace; explicit member name that owns the process."),
1040
1090
  processId: z.number().int().positive().optional().describe("Canonical process identifier returned by bash or exec_command."),
1041
1091
  sessionId: z.number().int().positive().optional().describe("Deprecated alias for processId. Retained for compatibility."),
1042
1092
  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 +1111,23 @@ function registerProcessTools(server, config, workspaces, processSessions, hooks
1061
1111
  outputSchema: processOutputSchema(),
1062
1112
  ...toolWidgetDescriptorMeta(config, "shell"),
1063
1113
  annotations: SHELL_TOOL_ANNOTATIONS,
1064
- }, async ({ workspaceId, processId, sessionId, outputId, chars, columns, rows, yieldTimeMs, maxOutputTokens }, extra) => {
1065
- const workspace = workspaces.getWorkspace(workspaceId);
1114
+ }, async ({ workspaceId, member, processId, sessionId, outputId, chars, columns, rows, yieldTimeMs, maxOutputTokens }, extra) => {
1115
+ const target = routing.resolve(workspaceId, member);
1116
+ await routing.prepare(target, extra._meta, extra.signal, extra.sessionId);
1117
+ if (routing.isRemote(target.executionWorkspaceId)) {
1118
+ return routing.present(await routing.writeStdinRemote(target.executionWorkspaceId, {
1119
+ ...(processId !== undefined ? { processId } : {}),
1120
+ ...(sessionId !== undefined ? { sessionId } : {}),
1121
+ ...(outputId !== undefined ? { outputId } : {}),
1122
+ ...(chars !== undefined ? { chars } : {}),
1123
+ ...(columns !== undefined ? { columns } : {}),
1124
+ ...(rows !== undefined ? { rows } : {}),
1125
+ ...(yieldTimeMs !== undefined ? { yieldTimeMs } : {}),
1126
+ ...(maxOutputTokens !== undefined ? { maxOutputTokens } : {}),
1127
+ }, routing.hostScopeIdFor(extra._meta, extra.sessionId)), target);
1128
+ }
1129
+ const executionWorkspaceId = target.executionWorkspaceId;
1130
+ const workspace = workspaces.getWorkspace(executionWorkspaceId);
1066
1131
  if (outputId !== undefined) {
1067
1132
  if (processId !== undefined || sessionId !== undefined || chars !== undefined || columns !== undefined ||
1068
1133
  rows !== undefined || yieldTimeMs !== undefined || maxOutputTokens !== undefined) {
@@ -1073,7 +1138,7 @@ function registerProcessTools(server, config, workspaces, processSessions, hooks
1073
1138
  tool: "write_stdin",
1074
1139
  invocation: workspaceHookInvocation(workspace),
1075
1140
  payload: { outputId },
1076
- operation: async () => durableOutputResponse("write_stdin", workspaceId, readWorkspaceBashOutput(bashOutputStore, workspaceId, outputId)),
1141
+ operation: async () => durableOutputResponse("write_stdin", executionWorkspaceId, readWorkspaceBashOutput(bashOutputStore, executionWorkspaceId, outputId)),
1077
1142
  });
1078
1143
  }
1079
1144
  const resolvedProcessId = resolveProcessId(processId, sessionId);
@@ -1090,7 +1155,7 @@ function registerProcessTools(server, config, workspaces, processSessions, hooks
1090
1155
  operation: async () => {
1091
1156
  const startedAt = performance.now();
1092
1157
  const snapshot = await processSessions.write({
1093
- workspaceId,
1158
+ workspaceId: executionWorkspaceId,
1094
1159
  processId: resolvedProcessId,
1095
1160
  chars,
1096
1161
  columns,
@@ -1108,7 +1173,7 @@ function registerProcessTools(server, config, workspaces, processSessions, hooks
1108
1173
  success: snapshot.running || snapshot.exitCode === 0,
1109
1174
  durationMs: Math.round(performance.now() - startedAt),
1110
1175
  });
1111
- const response = processToolResponse("write_stdin", workspaceId, snapshot, {
1176
+ const response = processToolResponse("write_stdin", executionWorkspaceId, snapshot, {
1112
1177
  processId: resolvedProcessId,
1113
1178
  charactersWritten: chars?.length ?? 0,
1114
1179
  running: snapshot.running,
@@ -1120,13 +1185,59 @@ function registerProcessTools(server, config, workspaces, processSessions, hooks
1120
1185
  }
1121
1186
  return response;
1122
1187
  },
1123
- });
1188
+ }).then((result) => routing.present(result, target));
1124
1189
  });
1125
1190
  }
1126
- export function createMcpServer(config, workspaces, reviewCheckpoints, processSessions, localAgentProviders, incomingArtifactAdapters, codeIntelligence, activityLifecycle, bashOutputStore, activityQueries) {
1191
+ export function createMcpServer(config, workspaces, reviewCheckpoints, processSessions, subagentProviders, incomingArtifactAdapters, codeIntelligence, activityLifecycle, bashOutputStore, activityQueries, options = {}) {
1127
1192
  const connectionScopeId = `mcp-connection:${randomUUID()}`;
1128
1193
  const remoteWorkspaces = new RemoteWorkspaceRelay(config.configDir, config.stateDir);
1194
+ const compositeWorkspaces = new CompositeWorkspaceRegistry(config.stateDir);
1195
+ const compositeActivity = new CompositeActivityCoordinator(compositeWorkspaces, activityQueries, remoteWorkspaces);
1196
+ const resolveExecutionTarget = (workspaceId, memberName) => {
1197
+ if (!compositeWorkspaces.has(workspaceId)) {
1198
+ if (memberName !== undefined) {
1199
+ throw new Error(`Workspace ${workspaceId} is not composite and does not accept member.`);
1200
+ }
1201
+ return { executionWorkspaceId: workspaceId };
1202
+ }
1203
+ if (!memberName) {
1204
+ throw new Error(`Composite Workspace ${workspaceId} requires member for this operation.`);
1205
+ }
1206
+ const member = compositeWorkspaces.member(workspaceId, memberName);
1207
+ try {
1208
+ if (!remoteWorkspaces.has(member.workspaceId))
1209
+ workspaces.getWorkspace(member.workspaceId);
1210
+ }
1211
+ catch (error) {
1212
+ throw new Error(`Composite Workspace ${workspaceId} member ${member.name} is unavailable: ${error instanceof Error ? error.message : String(error)}`);
1213
+ }
1214
+ return {
1215
+ executionWorkspaceId: member.workspaceId,
1216
+ compositeWorkspaceId: workspaceId,
1217
+ memberName: member.name,
1218
+ };
1219
+ };
1220
+ const subagentMcp = createSubagentMcpRuntime(config, activityLifecycle, options);
1221
+ const presentExecutionResult = (result, target) => {
1222
+ const presented = !target.compositeWorkspaceId || !target.memberName
1223
+ ? result
1224
+ : remapCompositeToolResult(result, target.executionWorkspaceId, target.compositeWorkspaceId, target.memberName);
1225
+ return subagentMcp.decorateResult(target.executionWorkspaceId, presented);
1226
+ };
1129
1227
  const hostScopeIdFor = (requestMeta, transportSessionId) => hostConversationScopeId(requestMeta, transportSessionId, connectionScopeId);
1228
+ const prepareExecutionContext = async (target, requestMeta, signal, sessionId) => {
1229
+ const conversationScopeId = hostScopeIdFor(requestMeta, sessionId);
1230
+ const turnId = target.compositeWorkspaceId && target.memberName
1231
+ ? await compositeActivity.prepareMember(target.compositeWorkspaceId, target.memberName, target.executionWorkspaceId, conversationScopeId)
1232
+ : undefined;
1233
+ return {
1234
+ requestMeta,
1235
+ signal,
1236
+ sessionId,
1237
+ ...(turnId ? { turnId } : {}),
1238
+ ...(target.memberName ? { activityMember: target.memberName } : {}),
1239
+ };
1240
+ };
1130
1241
  const toolDescriptions = buildToolDescriptions(config);
1131
1242
  const hooks = new HookRunner(config.hooks, config.logging, process.env, (workspaceId, result) => attachCompletedProcessNotices(processSessions, workspaceId, result, (snapshot) => recordBashCompletion(activityLifecycle, bashOutputStore, snapshot.outputId)));
1132
1243
  const incomingArtifactRegistry = new IncomingArtifactAdapterRegistry(incomingArtifactAdapters);
@@ -1136,6 +1247,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
1136
1247
  const batchExecuteAvailable = config.toolMode !== "codex";
1137
1248
  const capabilityRegistry = createCapabilityRegistry({
1138
1249
  inspectHooks: (workspaceRoot) => checkHookConfiguration(workspaceRoot, config.hooks),
1250
+ ...subagentMcp.registryDependencies,
1139
1251
  batchExecute: {
1140
1252
  available: batchExecuteAvailable,
1141
1253
  unavailableReason: batchExecuteAvailable
@@ -1224,11 +1336,59 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
1224
1336
  },
1225
1337
  },
1226
1338
  });
1339
+ const loadCompositeMemberContext = async (compositeWorkspaceId, memberName, contextPolicy, conversationScopeId, protectedWorkspaceIds) => {
1340
+ const target = resolveExecutionTarget(compositeWorkspaceId, memberName);
1341
+ if (remoteWorkspaces.has(target.executionWorkspaceId)) {
1342
+ const resumed = await remoteWorkspaces.resumeWorkspace(target.executionWorkspaceId, contextPolicy, conversationScopeId);
1343
+ const presented = presentExecutionResult(resumed, target);
1344
+ return {
1345
+ member: memberName,
1346
+ ...(presented.structuredContent ?? {}),
1347
+ };
1348
+ }
1349
+ const opened = await workspaces.openWorkspace({ workspaceId: target.executionWorkspaceId, context: contextPolicy }, { conversationScopeId, protectedWorkspaceIds });
1350
+ const workspace = opened.workspace;
1351
+ const capabilityFingerprint = buildCapabilityFingerprint(config, FORGERELAY_VERSION, {
1352
+ artifactDownloadSupported: isArtifactDownloadSupportedPlatform(),
1353
+ });
1354
+ const capabilityCatalog = capabilityRegistry.catalog(capabilityContextFor(workspace));
1355
+ const agentsFiles = opened.agentsFiles.map((file) => ({
1356
+ path: formatAgentsPath(file.path, workspace.root),
1357
+ content: file.content,
1358
+ }));
1359
+ const availableAgentsFiles = opened.availableAgentsFiles.map((file) => ({
1360
+ path: formatAgentsPath(file.path, workspace.root),
1361
+ }));
1362
+ const skills = workspace.skills
1363
+ .filter((skill) => !skill.disableModelInvocation)
1364
+ .map((skill) => ({ name: skill.name, description: skill.description }));
1365
+ return {
1366
+ member: memberName,
1367
+ workspaceId: compositeWorkspaceId,
1368
+ root: workspace.root,
1369
+ mode: workspace.mode,
1370
+ contextFingerprint: opened.contextFingerprint,
1371
+ capabilityFingerprint,
1372
+ capabilityCatalog,
1373
+ includeBootstrapContext: opened.includeBootstrapContext,
1374
+ ...(opened.includeBootstrapContext
1375
+ ? {
1376
+ agentsFiles,
1377
+ availableAgentsFiles,
1378
+ skills,
1379
+ skillDiagnostics: redactSkillDiagnosticPaths(workspace.skillDiagnostics),
1380
+ }
1381
+ : {}),
1382
+ instruction: opened.includeBootstrapContext
1383
+ ? `Bootstrap context for Composite member ${memberName}. Keep using Composite workspaceId ${compositeWorkspaceId} and pass member=${memberName} for work operations.`
1384
+ : `Composite member ${memberName} context was already delivered for this Host context; keep using Composite workspaceId ${compositeWorkspaceId} with member=${memberName}.`,
1385
+ };
1386
+ };
1227
1387
  const coreOperations = createCoreOperationExecutor({
1228
1388
  read: async (input, context) => {
1229
1389
  const { workspaceId, ...readInput } = input;
1230
1390
  const workspace = workspaces.getWorkspace(workspaceId);
1231
- return runActivityToolWithHooks(activityLifecycle, hooks, workspace, hostScopeIdFor(context.requestMeta, context.sessionId), input, {
1391
+ return runActivityToolWithHooks(activityLifecycle, hooks, workspace, hostScopeIdFor(context.requestMeta, context.sessionId), activityRequestFor(input, context), {
1232
1392
  signal: context.signal,
1233
1393
  tool: toolNames.read,
1234
1394
  invocation: workspaceHookInvocation(workspace),
@@ -1300,7 +1460,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
1300
1460
  write: async (input, context) => {
1301
1461
  const { workspaceId, ...writeInput } = input;
1302
1462
  const workspace = workspaces.getWorkspace(workspaceId);
1303
- return runActivityToolWithHooks(activityLifecycle, hooks, workspace, hostScopeIdFor(context.requestMeta, context.sessionId), input, {
1463
+ return runActivityToolWithHooks(activityLifecycle, hooks, workspace, hostScopeIdFor(context.requestMeta, context.sessionId), activityRequestFor(input, context), {
1304
1464
  signal: context.signal,
1305
1465
  tool: toolNames.write,
1306
1466
  invocation: workspaceHookInvocation(workspace),
@@ -1361,7 +1521,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
1361
1521
  edit: async (input, context) => {
1362
1522
  const { workspaceId, ...editInput } = input;
1363
1523
  const workspace = workspaces.getWorkspace(workspaceId);
1364
- return runActivityToolWithHooks(activityLifecycle, hooks, workspace, hostScopeIdFor(context.requestMeta, context.sessionId), input, {
1524
+ return runActivityToolWithHooks(activityLifecycle, hooks, workspace, hostScopeIdFor(context.requestMeta, context.sessionId), activityRequestFor(input, context), {
1365
1525
  signal: context.signal,
1366
1526
  tool: toolNames.edit,
1367
1527
  invocation: workspaceHookInvocation(workspace),
@@ -1423,7 +1583,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
1423
1583
  rename: async (input, context) => {
1424
1584
  const { workspaceId, path, newPath } = input;
1425
1585
  const workspace = workspaces.getWorkspace(workspaceId);
1426
- return runActivityToolWithHooks(activityLifecycle, hooks, workspace, hostScopeIdFor(context.requestMeta, context.sessionId), input, {
1586
+ return runActivityToolWithHooks(activityLifecycle, hooks, workspace, hostScopeIdFor(context.requestMeta, context.sessionId), activityRequestFor(input, context), {
1427
1587
  signal: context.signal,
1428
1588
  tool: toolNames.rename,
1429
1589
  invocation: workspaceHookInvocation(workspace),
@@ -1482,7 +1642,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
1482
1642
  delete: async (input, context) => {
1483
1643
  const { workspaceId, path, recursive } = input;
1484
1644
  const workspace = workspaces.getWorkspace(workspaceId);
1485
- return runActivityToolWithHooks(activityLifecycle, hooks, workspace, hostScopeIdFor(context.requestMeta, context.sessionId), input, {
1645
+ return runActivityToolWithHooks(activityLifecycle, hooks, workspace, hostScopeIdFor(context.requestMeta, context.sessionId), activityRequestFor(input, context), {
1486
1646
  signal: context.signal,
1487
1647
  tool: toolNames.delete,
1488
1648
  invocation: workspaceHookInvocation(workspace),
@@ -1566,7 +1726,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
1566
1726
  maxOutputTokens,
1567
1727
  };
1568
1728
  let undeliveredProcessId;
1569
- const activityResult = await runActivityTool(activityLifecycle, workspace, hostScopeIdFor(context.requestMeta, context.sessionId), surface, activityRequest, async (activityContext) => {
1729
+ const activityResult = await runActivityTool(activityLifecycle, workspace, hostScopeIdFor(context.requestMeta, context.sessionId), surface, activityRequestFor(activityRequest, context), async (activityContext) => {
1570
1730
  try {
1571
1731
  const result = await runToolWithHooks(hooks, {
1572
1732
  signal: context.signal,
@@ -1640,7 +1800,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
1640
1800
  const { workspaceId, name, arguments: capabilityArguments, file } = input;
1641
1801
  const workspace = workspaces.getWorkspace(workspaceId);
1642
1802
  let changedPaths = [];
1643
- return runActivityToolWithHooks(activityLifecycle, hooks, workspace, hostScopeIdFor(context.requestMeta, context.sessionId), { workspaceId, name, action: "run", arguments: capabilityArguments, file }, {
1803
+ return runActivityTool(activityLifecycle, workspace, hostScopeIdFor(context.requestMeta, context.sessionId), toolNames.capability, activityRequestFor(capabilityActivityAuditRequest(input), context), (activityContext) => runToolWithHooks(hooks, {
1644
1804
  signal: context.signal,
1645
1805
  tool: toolNames.capability,
1646
1806
  invocation: workspaceHookInvocation(workspace),
@@ -1656,6 +1816,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
1656
1816
  requestMeta: context.requestMeta,
1657
1817
  sessionId: context.sessionId,
1658
1818
  batch: context.batch,
1819
+ activityId: activityContext.activityId,
1659
1820
  });
1660
1821
  changedPaths = execution.changedPaths ?? [];
1661
1822
  const result = {
@@ -1708,7 +1869,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
1708
1869
  return result;
1709
1870
  }
1710
1871
  },
1711
- }, activityRelationFor(context));
1872
+ }), standardActivityOutcome, activityRelationFor(context), (result) => capabilityActivityAuditResult(name, result));
1712
1873
  },
1713
1874
  });
1714
1875
  batchExecutor = new BatchExecutor({
@@ -1794,16 +1955,44 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
1794
1955
  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
1956
  registerAppTool(server, "open_workspace", {
1796
1957
  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.",
1958
+ 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
1959
  inputSchema: {
1799
1960
  action: z
1800
- .enum(["open", "list"])
1961
+ .enum(["open", "list", "member"])
1962
+ .optional()
1963
+ .describe("Defaults to open. Use list to inspect logical workspaces. Use member to add/remove a named execution member on an existing Composite Workspace."),
1964
+ memberAction: z
1965
+ .enum(["add", "update", "remove"])
1801
1966
  .optional()
1802
- .describe("Defaults to open. Use list only when you need to inspect or choose logical workspaces before resuming or cleaning them up."),
1967
+ .describe("Required with action=member."),
1968
+ member: z.object({
1969
+ name: z.string().describe("Stable member name such as code or compute. For update/remove this identifies the existing member."),
1970
+ newName: z.string().optional().describe("Optional replacement member name for memberAction=update."),
1971
+ purpose: z.string().optional().describe("Agent-facing purpose. Required when adding a member; optional replacement when updating."),
1972
+ workspaceId: z.string().optional().describe("Existing ordinary or relayed Workspace to mount. Mutually exclusive with path."),
1973
+ path: z.string().optional().describe("Workspace path to open internally and mount. Mutually exclusive with workspaceId."),
1974
+ relay: z.string().optional().describe("Optional registered remote ForgeRelay alias for a path-backed member."),
1975
+ mode: z.enum(["checkout", "worktree"]).optional(),
1976
+ baseRef: z.string().optional(),
1977
+ newWorktree: z.boolean().optional(),
1978
+ newWorkspace: z.boolean().optional(),
1979
+ }).optional().describe("Composite member definition used by action=member."),
1980
+ kind: z
1981
+ .enum(["workspace", "composite"])
1982
+ .optional()
1983
+ .describe("Workspace kind for action=open. Defaults to workspace. Use composite with name and no path to create or reopen a named Composite Workspace."),
1984
+ name: z
1985
+ .string()
1986
+ .optional()
1987
+ .describe("Composite Workspace name. Used only with kind=\"composite\" when creating/opening by name."),
1988
+ memberName: z
1989
+ .string()
1990
+ .optional()
1991
+ .describe("For action=open on a Composite Workspace, load bootstrap context for this named member without making it an implicit current member."),
1803
1992
  path: z
1804
1993
  .string()
1805
1994
  .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."),
1995
+ .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
1996
  relay: z
1808
1997
  .string()
1809
1998
  .optional()
@@ -1863,8 +2052,17 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
1863
2052
  .describe("For action=list, maximum records to return. Defaults to 50; maximum 100."),
1864
2053
  },
1865
2054
  outputSchema: {
1866
- action: z.enum(["open", "list"]),
2055
+ action: z.enum(["open", "list", "member"]),
1867
2056
  workspaceId: z.string().optional(),
2057
+ memberAction: z.enum(["add", "update", "remove"]).optional(),
2058
+ kind: z.enum(["workspace", "composite"]).optional(),
2059
+ name: z.string().optional(),
2060
+ members: z.array(z.object({
2061
+ name: z.string(),
2062
+ purpose: z.string(),
2063
+ workspaceId: z.string(),
2064
+ })).optional(),
2065
+ memberContext: z.unknown().optional(),
1868
2066
  root: z.string().optional(),
1869
2067
  mode: z.enum(["checkout", "worktree"]).optional(),
1870
2068
  sourceRoot: z.string().optional(),
@@ -1907,10 +2105,22 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
1907
2105
  agentsFiles: z.array(workspaceAgentsFileOutputSchema).optional(),
1908
2106
  availableAgentsFiles: z.array(workspaceAvailableAgentsFileOutputSchema).optional(),
1909
2107
  skills: z.array(workspaceSkillOutputSchema).optional(),
1910
- agentProviders: z.array(workspaceLocalAgentProviderOutputSchema).optional(),
2108
+ agentProviders: z.array(workspaceSubagentProviderOutputSchema).optional(),
1911
2109
  agents: z.array(workspaceLocalAgentOutputSchema).optional(),
1912
2110
  skillDiagnostics: z.array(workspaceSkillDiagnosticOutputSchema).optional(),
1913
2111
  workspaces: z.array(workspaceInventoryEntryOutputSchema).optional(),
2112
+ compositeWorkspaces: z.array(z.object({
2113
+ workspaceId: z.string(),
2114
+ kind: z.literal("composite"),
2115
+ name: z.string(),
2116
+ members: z.array(z.object({
2117
+ name: z.string(),
2118
+ purpose: z.string(),
2119
+ workspaceId: z.string(),
2120
+ })),
2121
+ createdAt: z.string(),
2122
+ lastUsedAt: z.string(),
2123
+ })).optional(),
1914
2124
  summary: workspaceInventorySummaryOutputSchema.optional(),
1915
2125
  page: workspaceInventoryPageOutputSchema.optional(),
1916
2126
  instruction: z.string(),
@@ -1922,26 +2132,198 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
1922
2132
  idempotentHint: false,
1923
2133
  openWorldHint: false,
1924
2134
  },
1925
- }, async ({ action = "open", path, relay, workspaceId, mode, baseRef, newWorktree, newWorkspace, context, root, status, state, staleOnly, offset, limit, }, { _meta, sessionId }) => {
2135
+ }, 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
2136
  const startedAt = performance.now();
1927
2137
  const conversationScopeId = openAiConversationScopeId(_meta);
1928
2138
  const protectedWorkspaceIds = processSessions.activeWorkspaceIds();
2139
+ if (action === "member") {
2140
+ if (!workspaceId || !compositeWorkspaces.has(workspaceId)) {
2141
+ throw new Error("open_workspace action=member requires an existing Composite Workspace workspaceId.");
2142
+ }
2143
+ if (!memberAction || !member) {
2144
+ throw new Error("open_workspace action=member requires memberAction and member.");
2145
+ }
2146
+ if (kind !== undefined || name !== undefined || memberName !== undefined || path !== undefined || relay !== undefined || mode !== undefined ||
2147
+ baseRef !== undefined || newWorktree !== undefined || newWorkspace !== undefined || context !== undefined ||
2148
+ root !== undefined || status !== undefined || state !== undefined || staleOnly !== undefined ||
2149
+ offset !== undefined || limit !== undefined) {
2150
+ throw new Error("open_workspace action=member accepts workspaceId, memberAction, and member only. Put any Workspace open definition inside member.");
2151
+ }
2152
+ const resolveMemberTargetWorkspaceId = async () => {
2153
+ const byWorkspaceId = typeof member.workspaceId === "string" && member.workspaceId.length > 0;
2154
+ const byPath = typeof member.path === "string" && member.path.length > 0;
2155
+ if (byWorkspaceId === byPath) {
2156
+ throw new Error("A Composite Workspace member target requires exactly one of member.workspaceId or member.path.");
2157
+ }
2158
+ if (byWorkspaceId) {
2159
+ const targetWorkspaceId = member.workspaceId;
2160
+ if (compositeWorkspaces.has(targetWorkspaceId)) {
2161
+ throw new Error("A Composite Workspace cannot be mounted as a Composite Workspace member.");
2162
+ }
2163
+ if (member.relay !== undefined || member.mode !== undefined || member.baseRef !== undefined ||
2164
+ member.newWorktree !== undefined || member.newWorkspace !== undefined) {
2165
+ throw new Error("member.workspaceId cannot be combined with relay/mode/baseRef/newWorktree/newWorkspace.");
2166
+ }
2167
+ if (!remoteWorkspaces.has(targetWorkspaceId))
2168
+ workspaces.getWorkspace(targetWorkspaceId);
2169
+ return targetWorkspaceId;
2170
+ }
2171
+ if (member.relay !== undefined) {
2172
+ const opened = await remoteWorkspaces.openWorkspace(member.relay, {
2173
+ path: member.path,
2174
+ ...(member.mode ? { mode: member.mode } : {}),
2175
+ ...(member.baseRef ? { baseRef: member.baseRef } : {}),
2176
+ ...(member.newWorktree !== undefined ? { newWorktree: member.newWorktree } : {}),
2177
+ ...(member.newWorkspace !== undefined ? { newWorkspace: member.newWorkspace } : {}),
2178
+ context: "none",
2179
+ });
2180
+ return opened.workspaceId;
2181
+ }
2182
+ const opened = await workspaces.openWorkspace({
2183
+ path: member.path,
2184
+ ...(member.mode ? { mode: member.mode } : {}),
2185
+ ...(member.baseRef ? { baseRef: member.baseRef } : {}),
2186
+ ...(member.newWorktree !== undefined ? { newWorktree: member.newWorktree } : {}),
2187
+ ...(member.newWorkspace !== undefined ? { newWorkspace: member.newWorkspace } : {}),
2188
+ context: "none",
2189
+ }, { protectedWorkspaceIds });
2190
+ return opened.workspace.id;
2191
+ };
2192
+ let composite;
2193
+ if (memberAction === "add") {
2194
+ if (member.newName !== undefined) {
2195
+ throw new Error("Adding a Composite Workspace member does not accept member.newName.");
2196
+ }
2197
+ const purpose = member.purpose?.trim();
2198
+ if (!purpose)
2199
+ throw new Error("Adding a Composite Workspace member requires member.purpose.");
2200
+ const targetWorkspaceId = await resolveMemberTargetWorkspaceId();
2201
+ composite = compositeWorkspaces.addMember(workspaceId, {
2202
+ name: member.name,
2203
+ purpose,
2204
+ workspaceId: targetWorkspaceId,
2205
+ });
2206
+ }
2207
+ else if (memberAction === "update") {
2208
+ const targetFieldsPresent = member.workspaceId !== undefined || member.path !== undefined || member.relay !== undefined ||
2209
+ member.mode !== undefined || member.baseRef !== undefined || member.newWorktree !== undefined ||
2210
+ member.newWorkspace !== undefined;
2211
+ if (member.newName === undefined && member.purpose === undefined && !targetFieldsPresent) {
2212
+ throw new Error("Updating a Composite Workspace member requires newName, purpose, or a replacement Workspace target.");
2213
+ }
2214
+ const targetWorkspaceId = targetFieldsPresent
2215
+ ? await resolveMemberTargetWorkspaceId()
2216
+ : undefined;
2217
+ composite = compositeWorkspaces.updateMember(workspaceId, member.name, {
2218
+ ...(member.newName !== undefined ? { name: member.newName } : {}),
2219
+ ...(member.purpose !== undefined ? { purpose: member.purpose } : {}),
2220
+ ...(targetWorkspaceId !== undefined ? { workspaceId: targetWorkspaceId } : {}),
2221
+ });
2222
+ }
2223
+ else {
2224
+ if (member.newName !== undefined || member.purpose !== undefined || member.workspaceId !== undefined || member.path !== undefined ||
2225
+ member.relay !== undefined || member.mode !== undefined || member.baseRef !== undefined ||
2226
+ member.newWorktree !== undefined || member.newWorkspace !== undefined) {
2227
+ throw new Error("Removing a Composite Workspace member accepts only member.name.");
2228
+ }
2229
+ composite = compositeWorkspaces.removeMember(workspaceId, member.name);
2230
+ }
2231
+ const memberActionVerb = memberAction === "add"
2232
+ ? "Added"
2233
+ : memberAction === "update"
2234
+ ? "Updated"
2235
+ : "Removed";
2236
+ const memberActionPreposition = memberAction === "remove" ? "from" : "in";
2237
+ const instruction = [
2238
+ `${memberActionVerb} member ${member.name} ${memberActionPreposition} Composite Workspace ${composite.name} (${composite.id}).`,
2239
+ composite.members.length > 0
2240
+ ? `Members: ${composite.members.map((entry) => `${entry.name} — ${entry.purpose}`).join("; ")}.`
2241
+ : "This Composite Workspace currently has no members.",
2242
+ "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.",
2243
+ ].join("\n");
2244
+ const response = {
2245
+ content: [textBlock(instruction)],
2246
+ _meta: {
2247
+ tool: "open_workspace",
2248
+ card: {
2249
+ workspaceId: composite.id,
2250
+ kind: "composite",
2251
+ name: composite.name,
2252
+ path: composite.name,
2253
+ members: composite.members,
2254
+ instruction,
2255
+ summary: { members: composite.members.length },
2256
+ },
2257
+ },
2258
+ structuredContent: {
2259
+ action: "member",
2260
+ workspaceId: composite.id,
2261
+ memberAction,
2262
+ kind: "composite",
2263
+ name: composite.name,
2264
+ members: composite.members,
2265
+ instruction,
2266
+ },
2267
+ };
2268
+ rememberWorkspacePanelState(composite.id, response);
2269
+ return response;
2270
+ }
1929
2271
  if (action === "list") {
1930
- if (path !== undefined || relay !== undefined || baseRef !== undefined || newWorktree !== undefined ||
2272
+ if (path !== undefined || relay !== undefined || name !== undefined || memberName !== undefined || baseRef !== undefined || newWorktree !== undefined ||
1931
2273
  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.");
2274
+ 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.");
2275
+ }
2276
+ if (kind === "composite") {
2277
+ if (root !== undefined || mode !== undefined || status !== undefined || state !== undefined ||
2278
+ staleOnly !== undefined || offset !== undefined || limit !== undefined) {
2279
+ throw new Error("Composite Workspace inventory does not accept root/mode/status/state/staleOnly/offset/limit filters; use workspaceId when selecting one Composite Workspace.");
2280
+ }
2281
+ const composites = compositeWorkspaces.list()
2282
+ .filter((entry) => workspaceId === undefined || entry.id === workspaceId)
2283
+ .map((entry) => ({
2284
+ workspaceId: entry.id,
2285
+ kind: entry.kind,
2286
+ name: entry.name,
2287
+ members: entry.members,
2288
+ createdAt: entry.createdAt,
2289
+ lastUsedAt: entry.lastUsedAt,
2290
+ }));
2291
+ const instruction = "Resume a Composite Workspace with open_workspace(action=\"open\", workspaceId=...). Use close_workspace only when the user chooses to dissolve it.";
2292
+ const result = [
2293
+ `Composite Workspace inventory: ${composites.length} matching record${composites.length === 1 ? "" : "s"}.`,
2294
+ ...composites.map((entry) => `${entry.name} [${entry.workspaceId}] members=${entry.members.length} last-used=${entry.lastUsedAt}`),
2295
+ instruction,
2296
+ ].join("\n");
2297
+ return {
2298
+ content: [textBlock(result)],
2299
+ structuredContent: {
2300
+ action: "list",
2301
+ compositeWorkspaces: composites,
2302
+ instruction,
2303
+ },
2304
+ };
1933
2305
  }
1934
2306
  const inventory = await workspaces.listWorkspaces({ workspaceId, mode, root, status, state, staleOnly, offset, limit }, { conversationScopeId, protectedWorkspaceIds });
2307
+ const composites = kind === "workspace"
2308
+ ? []
2309
+ : compositeWorkspaces.list().map((entry) => ({
2310
+ workspaceId: entry.id,
2311
+ kind: entry.kind,
2312
+ name: entry.name,
2313
+ members: entry.members,
2314
+ createdAt: entry.createdAt,
2315
+ lastUsedAt: entry.lastUsedAt,
2316
+ }));
1935
2317
  const nextOffset = inventory.page.offset + inventory.page.limit;
1936
2318
  const instruction = [
1937
2319
  "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.",
2320
+ "Use close_workspace only after the user chooses cleanup or Composite dissolution; never close inventory entries automatically.",
1939
2321
  inventory.page.hasMore
1940
2322
  ? `More matching workspaces are available; continue with offset=${nextOffset}.`
1941
2323
  : undefined,
1942
2324
  ].filter(Boolean).join(" ");
1943
2325
  const result = [
1944
- `Logical workspace inventory: ${inventory.summary.matching} matching of ${inventory.summary.total} stored records.`,
2326
+ `Logical workspace inventory: ${inventory.summary.matching} matching ordinary records; ${composites.length} Composite Workspace record${composites.length === 1 ? "" : "s"}.`,
1945
2327
  `States: active=${inventory.summary.active}, stale=${inventory.summary.stale}, invalid=${inventory.summary.invalid}, closed=${inventory.summary.closed}.`,
1946
2328
  ...inventory.workspaces.map((entry) => [
1947
2329
  entry.label,
@@ -1953,6 +2335,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
1953
2335
  `root=${entry.root}`,
1954
2336
  `last-used=${entry.lastUsedAt}`,
1955
2337
  ].filter(Boolean).join(" ")),
2338
+ ...composites.map((entry) => `${entry.name} [${entry.workspaceId}] kind=composite members=${entry.members.length}`),
1956
2339
  instruction,
1957
2340
  ].join("\n");
1958
2341
  logToolCall(config, {
@@ -1967,6 +2350,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
1967
2350
  structuredContent: {
1968
2351
  action: "list",
1969
2352
  ...inventory,
2353
+ ...(composites.length > 0 ? { compositeWorkspaces: composites } : {}),
1970
2354
  instruction,
1971
2355
  },
1972
2356
  };
@@ -1975,6 +2359,70 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
1975
2359
  staleOnly !== undefined || offset !== undefined || limit !== undefined) {
1976
2360
  throw new Error("open_workspace inventory filters root, status, state, staleOnly, offset, and limit are only valid with action=list.");
1977
2361
  }
2362
+ const openingComposite = kind === "composite" ||
2363
+ (workspaceId !== undefined && compositeWorkspaces.has(workspaceId));
2364
+ if (openingComposite) {
2365
+ if (relay !== undefined || path !== undefined || mode !== undefined || baseRef !== undefined ||
2366
+ newWorktree !== undefined || newWorkspace !== undefined) {
2367
+ throw new Error("Composite Workspace open accepts name/workspaceId/context only; members are attached separately and keep their own Workspace definitions.");
2368
+ }
2369
+ if (workspaceId !== undefined && kind === "workspace") {
2370
+ throw new Error(`${workspaceId} is a Composite Workspace, not an ordinary Workspace.`);
2371
+ }
2372
+ const composite = workspaceId !== undefined
2373
+ ? compositeWorkspaces.open(workspaceId)
2374
+ : compositeWorkspaces.create(name ?? "");
2375
+ const memberContext = memberName
2376
+ ? await loadCompositeMemberContext(composite.id, memberName, context ?? "auto", conversationScopeId, protectedWorkspaceIds)
2377
+ : undefined;
2378
+ const instruction = [
2379
+ `This is Composite Workspace ${composite.name} (${composite.id}).`,
2380
+ "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.",
2381
+ composite.members.length > 0
2382
+ ? `Members: ${composite.members.map((member) => `${member.name} — ${member.purpose}`).join("; ")}.`
2383
+ : "This Composite Workspace currently has no members.",
2384
+ "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.",
2385
+ composite.members.length > 0
2386
+ ? "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."
2387
+ : undefined,
2388
+ "Use close_workspace only when the user chooses to dissolve this Composite Workspace; dissolution does not close or clean up member Workspaces.",
2389
+ ].join("\n\n");
2390
+ const response = {
2391
+ content: [textBlock(instruction)],
2392
+ _meta: {
2393
+ tool: "open_workspace",
2394
+ card: {
2395
+ workspaceId: composite.id,
2396
+ kind: "composite",
2397
+ name: composite.name,
2398
+ path: composite.name,
2399
+ members: composite.members,
2400
+ instruction,
2401
+ summary: { members: composite.members.length },
2402
+ },
2403
+ },
2404
+ structuredContent: {
2405
+ action: "open",
2406
+ workspaceId: composite.id,
2407
+ kind: "composite",
2408
+ name: composite.name,
2409
+ members: composite.members,
2410
+ ...(memberContext ? { memberContext } : {}),
2411
+ instruction,
2412
+ },
2413
+ };
2414
+ logToolCall(config, {
2415
+ tool: "open_workspace",
2416
+ action: "composite",
2417
+ success: true,
2418
+ durationMs: Math.round(performance.now() - startedAt),
2419
+ });
2420
+ rememberWorkspacePanelState(composite.id, response);
2421
+ return response;
2422
+ }
2423
+ if (name !== undefined || memberName !== undefined) {
2424
+ throw new Error("open_workspace name/memberName are only valid for a Composite Workspace.");
2425
+ }
1978
2426
  if (relay !== undefined) {
1979
2427
  if (workspaceId !== undefined) {
1980
2428
  throw new Error("Relayed open_workspace requires a path; resuming a relayed workspace is not available in this tracer bullet.");
@@ -2014,6 +2462,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
2014
2462
  tool: "open_workspace",
2015
2463
  card: {
2016
2464
  workspaceId: opened.workspaceId,
2465
+ kind: "workspace",
2017
2466
  root: opened.root,
2018
2467
  path: opened.root,
2019
2468
  mode: opened.mode,
@@ -2025,6 +2474,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
2025
2474
  structuredContent: {
2026
2475
  action: "open",
2027
2476
  workspaceId: opened.workspaceId,
2477
+ kind: "workspace",
2028
2478
  root: opened.root,
2029
2479
  mode: opened.mode,
2030
2480
  ...(opened.sourceRoot ? { sourceRoot: opened.sourceRoot } : {}),
@@ -2093,9 +2543,9 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
2093
2543
  path: formatPathForPrompt(guide.filePath),
2094
2544
  }));
2095
2545
  const capabilityCatalog = capabilityRegistry.catalog(capabilityContextFor(workspace));
2096
- const cardAgentProviders = config.subagents ? localAgentProviders : [];
2546
+ const cardAgentProviders = config.subagents ? subagentProviders : [];
2097
2547
  const cardAgents = workspace.agentProfiles.map((profile) => {
2098
- const summary = summarizeLocalAgentProfile(profile);
2548
+ const summary = summarizeSubagentProfile(profile);
2099
2549
  const availability = cardAgentProviders.find((provider) => provider.name === summary.provider);
2100
2550
  return {
2101
2551
  ...summary,
@@ -2173,10 +2623,10 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
2173
2623
  ? `Available subagent providers: ${visibleAgentProviders.filter((provider) => provider.available).map((provider) => provider.name).join(", ")}`
2174
2624
  : undefined,
2175
2625
  visibleAgentProviders.some((provider) => !provider.available)
2176
- ? `Unavailable subagent providers: ${visibleAgentProviders.filter((provider) => !provider.available).map(formatUnavailableAgentProvider).join(", ")}`
2626
+ ? `Unavailable subagent providers: ${visibleAgentProviders.filter((provider) => !provider.available).map(formatUnavailableSubagentProvider).join(", ")}`
2177
2627
  : undefined,
2178
2628
  visibleAgents.length > 0
2179
- ? `Available subagent profiles: ${visibleAgents.map(formatVisibleAgent).join(", ")}`
2629
+ ? `Available subagent profiles: ${visibleAgents.map(formatAvailableSubagentProfile).join(", ")}`
2180
2630
  : undefined,
2181
2631
  knownWorktrees.length > 0
2182
2632
  ? `Known worktrees: ${knownWorktrees.map((worktree) => `${worktree.path} [${worktree.workspaceId}]${worktree.branch ? ` branch=${worktree.branch}` : ""}${worktree.targetBranch ? ` target=${worktree.targetBranch}` : ""}${worktree.current ? " (current)" : ""}`).join(", ")}`
@@ -2202,6 +2652,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
2202
2652
  tool: "open_workspace",
2203
2653
  card: {
2204
2654
  workspaceId: workspace.id,
2655
+ kind: "workspace",
2205
2656
  root: workspace.root,
2206
2657
  path: workspace.root,
2207
2658
  mode: workspace.mode,
@@ -2234,6 +2685,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
2234
2685
  structuredContent: {
2235
2686
  action: "open",
2236
2687
  workspaceId: workspace.id,
2688
+ kind: "workspace",
2237
2689
  root: workspace.root,
2238
2690
  mode: workspace.mode,
2239
2691
  sourceRoot: workspace.sourceRoot,
@@ -2261,18 +2713,40 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
2261
2713
  return response;
2262
2714
  });
2263
2715
  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),
2716
+ panel: async (workspaceId, conversationScopeId) => {
2717
+ if (compositeWorkspaces.has(workspaceId)) {
2718
+ return compositeActivity.beginPanel(workspaceId, conversationScopeId);
2719
+ }
2720
+ return remoteWorkspaces.has(workspaceId)
2721
+ ? remoteWorkspaces.activityPanel(workspaceId, conversationScopeId)
2722
+ : undefined;
2723
+ },
2724
+ snapshot: async (input, conversationScopeId) => {
2725
+ const compositeTurnId = input.turnId ?? (input.workspaceId && compositeWorkspaces.has(input.workspaceId)
2726
+ ? compositeActivity.currentTurnId(conversationScopeId, input.workspaceId)
2727
+ : undefined);
2728
+ if (compositeTurnId) {
2729
+ const composite = await compositeActivity.snapshot(compositeTurnId, input.knownRevision);
2730
+ if (composite)
2731
+ return composite;
2732
+ }
2733
+ return remoteWorkspaces.activitySnapshot(input, conversationScopeId);
2734
+ },
2735
+ detail: async (turnId, activityId, conversationScopeId) => {
2736
+ const composite = await compositeActivity.detail(turnId, activityId);
2737
+ return composite ?? remoteWorkspaces.activityDetail(turnId, activityId, conversationScopeId);
2738
+ },
2739
+ output: async (turnId, outputId, conversationScopeId) => {
2740
+ const composite = await compositeActivity.output(turnId, outputId);
2741
+ return composite ?? remoteWorkspaces.activityOutput(turnId, outputId, conversationScopeId);
2742
+ },
2270
2743
  });
2271
2744
  registerAppTool(server, toolNames.capability, {
2272
2745
  title: "Use optional capability",
2273
2746
  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
2747
  inputSchema: {
2275
2748
  workspaceId: z.string().describe("Workspace identifier returned by open_workspace."),
2749
+ member: z.string().optional().describe("Required for a Composite Workspace; explicit member name whose capability surface is used."),
2276
2750
  name: z
2277
2751
  .string()
2278
2752
  .regex(/^[a-z][a-z0-9-]*(?:\.[a-z][a-z0-9-]*)+$/)
@@ -2304,17 +2778,20 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
2304
2778
  idempotentHint: false,
2305
2779
  openWorldHint: true,
2306
2780
  },
2307
- }, async ({ workspaceId, name, action, arguments: capabilityArguments, file }, extra) => {
2308
- if (remoteWorkspaces.has(workspaceId)) {
2309
- return remoteWorkspaces.capability(workspaceId, {
2781
+ }, async ({ workspaceId, member, name, action, arguments: capabilityArguments, file }, extra) => {
2782
+ const target = resolveExecutionTarget(workspaceId, member);
2783
+ const executionWorkspaceId = target.executionWorkspaceId;
2784
+ const executionContext = await prepareExecutionContext(target, extra._meta, extra.signal, extra.sessionId);
2785
+ if (remoteWorkspaces.has(executionWorkspaceId)) {
2786
+ return presentExecutionResult(await remoteWorkspaces.capability(executionWorkspaceId, {
2310
2787
  name,
2311
2788
  action,
2312
2789
  ...(capabilityArguments !== undefined ? { arguments: capabilityArguments } : {}),
2313
2790
  ...(file !== undefined ? { file } : {}),
2314
- }, hostScopeIdFor(extra._meta, extra.sessionId));
2791
+ }, hostScopeIdFor(extra._meta, extra.sessionId)), target);
2315
2792
  }
2316
2793
  if (action === "run" && name === "batch.execute") {
2317
- const workspace = workspaces.getWorkspace(workspaceId);
2794
+ const workspace = workspaces.getWorkspace(executionWorkspaceId);
2318
2795
  const startedAt = performance.now();
2319
2796
  try {
2320
2797
  const execution = await capabilityRegistry.run(name, capabilityArguments ?? {}, capabilityContextFor(workspace), {
@@ -2335,7 +2812,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
2335
2812
  success: true,
2336
2813
  durationMs: Math.round(performance.now() - startedAt),
2337
2814
  });
2338
- return result;
2815
+ return presentExecutionResult(result, target);
2339
2816
  }
2340
2817
  catch (error) {
2341
2818
  if (extra.signal.aborted)
@@ -2358,18 +2835,14 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
2358
2835
  capability: name,
2359
2836
  action,
2360
2837
  }, result.content, startedAt);
2361
- return result;
2838
+ return presentExecutionResult(result, target);
2362
2839
  }
2363
2840
  }
2364
2841
  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
- });
2842
+ return presentExecutionResult(await coreOperations.capabilityRun({ workspaceId: executionWorkspaceId, name, arguments: capabilityArguments, file }, executionContext), target);
2370
2843
  }
2371
- const workspace = workspaces.getWorkspace(workspaceId);
2372
- return runActivityToolWithHooks(activityLifecycle, hooks, workspace, hostScopeIdFor(extra._meta, extra.sessionId), { workspaceId, name, action, arguments: capabilityArguments, file }, {
2844
+ const workspace = workspaces.getWorkspace(executionWorkspaceId);
2845
+ return runActivityToolWithHooks(activityLifecycle, hooks, workspace, hostScopeIdFor(extra._meta, extra.sessionId), activityRequestFor({ workspaceId: executionWorkspaceId, name, action, arguments: capabilityArguments, file }, executionContext), {
2373
2846
  signal: extra.signal,
2374
2847
  tool: toolNames.capability,
2375
2848
  invocation: workspaceHookInvocation(workspace),
@@ -2422,11 +2895,11 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
2422
2895
  return result;
2423
2896
  }
2424
2897
  },
2425
- });
2898
+ }, activityRelationFor(executionContext)).then((result) => presentExecutionResult(result, target));
2426
2899
  });
2427
2900
  registerAppTool(server, toolNames.closeWorkspace, {
2428
2901
  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.",
2902
+ 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
2903
  inputSchema: {
2431
2904
  workspaceId: z.string().describe("Workspace identifier to close."),
2432
2905
  commitMessage: z
@@ -2437,7 +2910,15 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
2437
2910
  },
2438
2911
  outputSchema: resultOutputSchema({
2439
2912
  workspaceId: z.string(),
2440
- mode: z.enum(["checkout", "worktree"]),
2913
+ kind: z.enum(["workspace", "composite"]).optional(),
2914
+ mode: z.enum(["checkout", "worktree"]).optional(),
2915
+ name: z.string().optional(),
2916
+ members: z.array(z.object({
2917
+ name: z.string(),
2918
+ purpose: z.string(),
2919
+ workspaceId: z.string(),
2920
+ })).optional(),
2921
+ dissolved: z.boolean().optional(),
2441
2922
  sourceRoot: z.string().optional(),
2442
2923
  branch: z.string().optional(),
2443
2924
  targetBranch: z.string().optional(),
@@ -2449,6 +2930,43 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
2449
2930
  _meta: {},
2450
2931
  annotations: WRITE_TOOL_ANNOTATIONS,
2451
2932
  }, async ({ workspaceId, commitMessage }, extra) => {
2933
+ if (compositeWorkspaces.has(workspaceId)) {
2934
+ if (commitMessage !== undefined) {
2935
+ throw new Error("close_workspace commitMessage is not valid when dissolving a Composite Workspace.");
2936
+ }
2937
+ const composite = compositeWorkspaces.dissolve(workspaceId);
2938
+ compositeActivity.forgetComposite(workspaceId);
2939
+ workspacePanelStates.delete(workspaceId);
2940
+ const result = [
2941
+ `Dissolved Composite Workspace ${composite.name} (${workspaceId}).`,
2942
+ composite.members.length > 0
2943
+ ? `Preserved member Workspaces: ${composite.members.map((member) => `${member.name} [${member.workspaceId}]`).join(", ")}.`
2944
+ : "The Composite Workspace had no members.",
2945
+ "Member Workspace handles, managed worktrees, processes, files, and Workspace Relay routes were not closed or cleaned up.",
2946
+ ].join("\n");
2947
+ return {
2948
+ content: [textBlock(result)],
2949
+ _meta: {
2950
+ tool: toolNames.closeWorkspace,
2951
+ card: {
2952
+ workspaceId,
2953
+ kind: "composite",
2954
+ name: composite.name,
2955
+ members: composite.members,
2956
+ dissolved: true,
2957
+ payload: { content: [textBlock(result)] },
2958
+ },
2959
+ },
2960
+ structuredContent: {
2961
+ result,
2962
+ workspaceId,
2963
+ kind: "composite",
2964
+ name: composite.name,
2965
+ members: composite.members,
2966
+ dissolved: true,
2967
+ },
2968
+ };
2969
+ }
2452
2970
  if (remoteWorkspaces.has(workspaceId)) {
2453
2971
  const response = await remoteWorkspaces.closeWorkspace(workspaceId, commitMessage, hostScopeIdFor(extra._meta, extra.sessionId));
2454
2972
  workspacePanelStates.delete(workspaceId);
@@ -2565,6 +3083,10 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
2565
3083
  workspaceId: z
2566
3084
  .string()
2567
3085
  .describe("Workspace identifier returned by open_workspace."),
3086
+ member: z
3087
+ .string()
3088
+ .optional()
3089
+ .describe("Required for a Composite Workspace; explicit member name that owns this operation."),
2568
3090
  path: z
2569
3091
  .string()
2570
3092
  .optional()
@@ -2602,30 +3124,27 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
2602
3124
  }),
2603
3125
  ...toolWidgetDescriptorMeta(config, "read"),
2604
3126
  annotations: { readOnlyHint: true },
2605
- }, async ({ workspaceId, path, paths, offset, limit }, extra) => {
3127
+ }, async ({ workspaceId, member, path, paths, offset, limit }, extra) => {
2606
3128
  if ((path === undefined) === (paths === undefined)) {
2607
3129
  throw new Error("read requires exactly one of path or paths.");
2608
3130
  }
2609
- if (remoteWorkspaces.has(workspaceId)) {
2610
- return remoteWorkspaces.read(workspaceId, { path, paths, offset, limit }, hostScopeIdFor(extra._meta, extra.sessionId));
3131
+ const target = resolveExecutionTarget(workspaceId, member);
3132
+ const executionWorkspaceId = target.executionWorkspaceId;
3133
+ const executionContext = await prepareExecutionContext(target, extra._meta, extra.signal, extra.sessionId);
3134
+ if (remoteWorkspaces.has(executionWorkspaceId)) {
3135
+ return presentExecutionResult(await remoteWorkspaces.read(executionWorkspaceId, { path, paths, offset, limit }, hostScopeIdFor(extra._meta, extra.sessionId)), target);
2611
3136
  }
2612
3137
  if (path !== undefined) {
2613
- return coreOperations.read({ workspaceId, path, offset, limit }, {
2614
- requestMeta: extra._meta,
2615
- signal: extra.signal,
2616
- sessionId: extra.sessionId,
2617
- });
3138
+ return presentExecutionResult(await coreOperations.read({ workspaceId: executionWorkspaceId, path, offset, limit }, executionContext), target);
2618
3139
  }
2619
- const workspace = workspaces.getWorkspace(workspaceId);
3140
+ const workspace = workspaces.getWorkspace(executionWorkspaceId);
2620
3141
  let response;
2621
- await runActivityTool(activityLifecycle, workspace, hostScopeIdFor(extra._meta, extra.sessionId), toolNames.read, { workspaceId, paths, offset, limit }, async (parentContext) => {
3142
+ await runActivityTool(activityLifecycle, workspace, hostScopeIdFor(extra._meta, extra.sessionId), toolNames.read, activityRequestFor({ workspaceId: executionWorkspaceId, paths, offset, limit }, executionContext), async (parentContext) => {
2622
3143
  const execution = await executeBulkRead({
2623
3144
  paths: paths,
2624
3145
  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,
3146
+ run: (childPath) => coreOperations.read({ workspaceId: executionWorkspaceId, path: childPath, offset, limit }, {
3147
+ ...executionContext,
2629
3148
  parentActivityId: parentContext.activityId,
2630
3149
  turnId: parentContext.turnId,
2631
3150
  }),
@@ -2666,10 +3185,10 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
2666
3185
  };
2667
3186
  }, (summary) => summary.failed > 0
2668
3187
  ? { type: "failed", error: `${summary.failed} of ${summary.childCount} child Reads failed.` }
2669
- : { type: "succeeded" });
3188
+ : { type: "succeeded" }, activityRelationFor(executionContext));
2670
3189
  if (!response)
2671
3190
  throw new Error("Bulk Read completed without a response.");
2672
- return response;
3191
+ return presentExecutionResult(response, target);
2673
3192
  });
2674
3193
  if (config.toolMode !== "codex") {
2675
3194
  registerAppTool(server, toolNames.write, {
@@ -2679,6 +3198,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
2679
3198
  workspaceId: z
2680
3199
  .string()
2681
3200
  .describe("Workspace identifier returned by open_workspace."),
3201
+ member: z.string().optional().describe("Required for a Composite Workspace; explicit member name that owns this operation."),
2682
3202
  path: z
2683
3203
  .string()
2684
3204
  .describe("File path to write, relative to the workspace root or absolute inside the OS temp directory."),
@@ -2687,15 +3207,14 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
2687
3207
  outputSchema: resultOutputSchema(),
2688
3208
  ...toolWidgetDescriptorMeta(config, "write"),
2689
3209
  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));
3210
+ }, async ({ workspaceId, member, ...input }, extra) => {
3211
+ const target = resolveExecutionTarget(workspaceId, member);
3212
+ const executionWorkspaceId = target.executionWorkspaceId;
3213
+ const executionContext = await prepareExecutionContext(target, extra._meta, extra.signal, extra.sessionId);
3214
+ if (remoteWorkspaces.has(executionWorkspaceId)) {
3215
+ return presentExecutionResult(await remoteWorkspaces.write(executionWorkspaceId, input, hostScopeIdFor(extra._meta, extra.sessionId)), target);
2693
3216
  }
2694
- return coreOperations.write({ workspaceId, ...input }, {
2695
- requestMeta: extra._meta,
2696
- signal: extra.signal,
2697
- sessionId: extra.sessionId,
2698
- });
3217
+ return presentExecutionResult(await coreOperations.write({ workspaceId: executionWorkspaceId, ...input }, executionContext), target);
2699
3218
  });
2700
3219
  registerAppTool(server, toolNames.edit, {
2701
3220
  title: "Edit file",
@@ -2704,6 +3223,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
2704
3223
  workspaceId: z
2705
3224
  .string()
2706
3225
  .describe("Workspace identifier returned by open_workspace."),
3226
+ member: z.string().optional().describe("Required for a Composite Workspace; explicit member name that owns this operation."),
2707
3227
  path: z
2708
3228
  .string()
2709
3229
  .optional()
@@ -2737,25 +3257,20 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
2737
3257
  }),
2738
3258
  ...toolWidgetDescriptorMeta(config, "edit"),
2739
3259
  annotations: EDIT_TOOL_ANNOTATIONS,
2740
- }, async ({ workspaceId, path, paths, edits }, extra) => {
3260
+ }, async ({ workspaceId, member, path, paths, edits }, extra) => {
2741
3261
  if ((path === undefined) === (paths === undefined)) {
2742
3262
  throw new Error("edit requires exactly one of path or paths.");
2743
3263
  }
2744
- if (remoteWorkspaces.has(workspaceId)) {
2745
- return remoteWorkspaces.edit(workspaceId, { path, paths, edits }, hostScopeIdFor(extra._meta, extra.sessionId));
3264
+ const target = resolveExecutionTarget(workspaceId, member);
3265
+ const executionWorkspaceId = target.executionWorkspaceId;
3266
+ const executionContext = await prepareExecutionContext(target, extra._meta, extra.signal, extra.sessionId);
3267
+ if (remoteWorkspaces.has(executionWorkspaceId)) {
3268
+ return presentExecutionResult(await remoteWorkspaces.edit(executionWorkspaceId, { path, paths, edits }, hostScopeIdFor(extra._meta, extra.sessionId)), target);
2746
3269
  }
2747
3270
  if (path !== undefined) {
2748
- return coreOperations.edit({ workspaceId, path, edits }, {
2749
- requestMeta: extra._meta,
2750
- signal: extra.signal,
2751
- sessionId: extra.sessionId,
2752
- });
3271
+ return presentExecutionResult(await coreOperations.edit({ workspaceId: executionWorkspaceId, path, edits }, executionContext), target);
2753
3272
  }
2754
- return nativeBulkMutations.edit({ workspaceId, paths: paths, edits }, {
2755
- requestMeta: extra._meta,
2756
- signal: extra.signal,
2757
- sessionId: extra.sessionId,
2758
- });
3273
+ return presentExecutionResult(await nativeBulkMutations.edit({ workspaceId: executionWorkspaceId, paths: paths, edits }, executionContext), target);
2759
3274
  });
2760
3275
  }
2761
3276
  registerAppTool(server, toolNames.rename, {
@@ -2763,6 +3278,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
2763
3278
  description: toolDescriptions.rename,
2764
3279
  inputSchema: {
2765
3280
  workspaceId: z.string().describe("Workspace identifier returned by open_workspace."),
3281
+ member: z.string().optional().describe("Required for a Composite Workspace; explicit member name that owns this operation."),
2766
3282
  path: z.string().describe("Source file or directory path relative to the workspace root, or absolute inside the OS temp directory."),
2767
3283
  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
3284
  },
@@ -2773,21 +3289,21 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
2773
3289
  }),
2774
3290
  ...toolWidgetDescriptorMeta(config, "edit"),
2775
3291
  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));
3292
+ }, async ({ workspaceId, member, path, newPath }, extra) => {
3293
+ const target = resolveExecutionTarget(workspaceId, member);
3294
+ const executionWorkspaceId = target.executionWorkspaceId;
3295
+ const executionContext = await prepareExecutionContext(target, extra._meta, extra.signal, extra.sessionId);
3296
+ if (remoteWorkspaces.has(executionWorkspaceId)) {
3297
+ return presentExecutionResult(await remoteWorkspaces.rename(executionWorkspaceId, { path, newPath }, hostScopeIdFor(extra._meta, extra.sessionId)), target);
2779
3298
  }
2780
- return coreOperations.rename({ workspaceId, path, newPath }, {
2781
- requestMeta: extra._meta,
2782
- signal: extra.signal,
2783
- sessionId: extra.sessionId,
2784
- });
3299
+ return presentExecutionResult(await coreOperations.rename({ workspaceId: executionWorkspaceId, path, newPath }, executionContext), target);
2785
3300
  });
2786
3301
  registerAppTool(server, toolNames.delete, {
2787
3302
  title: "Delete path",
2788
3303
  description: toolDescriptions.delete,
2789
3304
  inputSchema: {
2790
3305
  workspaceId: z.string().describe("Workspace identifier returned by open_workspace."),
3306
+ member: z.string().optional().describe("Required for a Composite Workspace; explicit member name that owns this operation."),
2791
3307
  path: z.string().optional().describe("One file or directory path to delete. Use exactly one of path or paths."),
2792
3308
  paths: z
2793
3309
  .array(z.string())
@@ -2813,25 +3329,20 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
2813
3329
  }),
2814
3330
  ...toolWidgetDescriptorMeta(config, "edit"),
2815
3331
  annotations: EDIT_TOOL_ANNOTATIONS,
2816
- }, async ({ workspaceId, path, paths, recursive }, extra) => {
3332
+ }, async ({ workspaceId, member, path, paths, recursive }, extra) => {
2817
3333
  if ((path === undefined) === (paths === undefined)) {
2818
3334
  throw new Error("delete requires exactly one of path or paths.");
2819
3335
  }
2820
- if (remoteWorkspaces.has(workspaceId)) {
2821
- return remoteWorkspaces.delete(workspaceId, { path, paths, recursive }, hostScopeIdFor(extra._meta, extra.sessionId));
3336
+ const target = resolveExecutionTarget(workspaceId, member);
3337
+ const executionWorkspaceId = target.executionWorkspaceId;
3338
+ const executionContext = await prepareExecutionContext(target, extra._meta, extra.signal, extra.sessionId);
3339
+ if (remoteWorkspaces.has(executionWorkspaceId)) {
3340
+ return presentExecutionResult(await remoteWorkspaces.delete(executionWorkspaceId, { path, paths, recursive }, hostScopeIdFor(extra._meta, extra.sessionId)), target);
2822
3341
  }
2823
3342
  if (path !== undefined) {
2824
- return coreOperations.delete({ workspaceId, path, recursive }, {
2825
- requestMeta: extra._meta,
2826
- signal: extra.signal,
2827
- sessionId: extra.sessionId,
2828
- });
3343
+ return presentExecutionResult(await coreOperations.delete({ workspaceId: executionWorkspaceId, path, recursive }, executionContext), target);
2829
3344
  }
2830
- return nativeBulkMutations.delete({ workspaceId, paths: paths, recursive }, {
2831
- requestMeta: extra._meta,
2832
- signal: extra.signal,
2833
- sessionId: extra.sessionId,
2834
- });
3345
+ return presentExecutionResult(await nativeBulkMutations.delete({ workspaceId: executionWorkspaceId, paths: paths, recursive }, executionContext), target);
2835
3346
  });
2836
3347
  if (config.toolMode === "codex") {
2837
3348
  registerAppTool(server, "apply_patch", {
@@ -2841,6 +3352,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
2841
3352
  workspaceId: z
2842
3353
  .string()
2843
3354
  .describe("Workspace identifier returned by open_workspace."),
3355
+ member: z.string().optional().describe("Required for a Composite Workspace; explicit member name that owns this patch."),
2844
3356
  patch: z
2845
3357
  .string()
2846
3358
  .describe("Patch text enclosed by *** Begin Patch and *** End Patch markers."),
@@ -2856,9 +3368,15 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
2856
3368
  }),
2857
3369
  ...toolWidgetDescriptorMeta(config, "edit"),
2858
3370
  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 }, {
3371
+ }, async ({ workspaceId, member, patch }, extra) => {
3372
+ const target = resolveExecutionTarget(workspaceId, member);
3373
+ const executionWorkspaceId = target.executionWorkspaceId;
3374
+ const executionContext = await prepareExecutionContext(target, extra._meta, extra.signal, extra.sessionId);
3375
+ if (remoteWorkspaces.has(executionWorkspaceId)) {
3376
+ return presentExecutionResult(await remoteWorkspaces.applyPatch(executionWorkspaceId, { patch }, hostScopeIdFor(extra._meta, extra.sessionId)), target);
3377
+ }
3378
+ const workspace = workspaces.getWorkspace(executionWorkspaceId);
3379
+ return runActivityToolWithHooks(activityLifecycle, hooks, workspace, hostScopeIdFor(extra._meta, extra.sessionId), activityRequestFor({ workspaceId: executionWorkspaceId, patch }, executionContext), {
2862
3380
  signal: extra.signal,
2863
3381
  tool: "apply_patch",
2864
3382
  invocation: workspaceHookInvocation(workspace),
@@ -2886,7 +3404,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
2886
3404
  _meta: {
2887
3405
  tool: "apply_patch",
2888
3406
  card: {
2889
- workspaceId,
3407
+ workspaceId: executionWorkspaceId,
2890
3408
  path: displayPath,
2891
3409
  summary: {
2892
3410
  files: applied.files.length,
@@ -2905,7 +3423,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
2905
3423
  },
2906
3424
  };
2907
3425
  },
2908
- });
3426
+ }, activityRelationFor(executionContext)).then((result) => presentExecutionResult(result, target));
2909
3427
  });
2910
3428
  }
2911
3429
  if (config.toolMode !== "codex") {
@@ -2916,6 +3434,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
2916
3434
  workspaceId: z
2917
3435
  .string()
2918
3436
  .describe("Workspace identifier returned by open_workspace."),
3437
+ member: z.string().optional().describe("Required for a Composite Workspace; explicit member name that owns this process operation."),
2919
3438
  action: z
2920
3439
  .enum(["run", "process", "output"])
2921
3440
  .optional()
@@ -2989,9 +3508,12 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
2989
3508
  outputSchema: processOutputSchema(),
2990
3509
  ...toolWidgetDescriptorMeta(config, "shell"),
2991
3510
  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, {
3511
+ }, async ({ workspaceId, member, action = "run", command, processId, outputId, input, interrupt, tty, columns, rows, workingDirectory, yieldTimeMs, timeoutMs, maxOutputTokens, }, extra) => {
3512
+ const target = resolveExecutionTarget(workspaceId, member);
3513
+ const executionWorkspaceId = target.executionWorkspaceId;
3514
+ const executionContext = await prepareExecutionContext(target, extra._meta, extra.signal, extra.sessionId);
3515
+ if (remoteWorkspaces.has(executionWorkspaceId)) {
3516
+ return presentExecutionResult(await remoteWorkspaces.bash(executionWorkspaceId, {
2995
3517
  action,
2996
3518
  ...(command !== undefined ? { command } : {}),
2997
3519
  ...(processId !== undefined ? { processId } : {}),
@@ -3005,17 +3527,17 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
3005
3527
  ...(yieldTimeMs !== undefined ? { yieldTimeMs } : {}),
3006
3528
  ...(timeoutMs !== undefined ? { timeoutMs } : {}),
3007
3529
  ...(maxOutputTokens !== undefined ? { maxOutputTokens } : {}),
3008
- }, hostScopeIdFor(extra._meta, extra.sessionId));
3530
+ }, hostScopeIdFor(extra._meta, extra.sessionId)), target);
3009
3531
  }
3010
- const workspace = workspaces.getWorkspace(workspaceId);
3532
+ const workspace = workspaces.getWorkspace(executionWorkspaceId);
3011
3533
  if (action === "run") {
3012
3534
  if (!command)
3013
3535
  throw new Error("bash action=run requires command.");
3014
3536
  if (processId !== undefined || outputId !== undefined || input !== undefined || interrupt !== undefined) {
3015
3537
  throw new Error("bash action=run does not accept processId, outputId, input, or interrupt.");
3016
3538
  }
3017
- return coreOperations.shellRun({
3018
- workspaceId,
3539
+ return presentExecutionResult(await coreOperations.shellRun({
3540
+ workspaceId: executionWorkspaceId,
3019
3541
  command,
3020
3542
  surface: "bash",
3021
3543
  tty,
@@ -3025,11 +3547,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
3025
3547
  yieldTimeMs,
3026
3548
  timeoutMs,
3027
3549
  maxOutputTokens,
3028
- }, {
3029
- requestMeta: extra._meta,
3030
- signal: extra.signal,
3031
- sessionId: extra.sessionId,
3032
- });
3550
+ }, executionContext), target);
3033
3551
  }
3034
3552
  if (action === "output") {
3035
3553
  if (!outputId)
@@ -3044,8 +3562,8 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
3044
3562
  tool: toolNames.shell,
3045
3563
  invocation: workspaceHookInvocation(workspace),
3046
3564
  payload: { action, outputId },
3047
- operation: async () => durableOutputResponse(toolNames.shell, workspaceId, readWorkspaceBashOutput(bashOutputStore, workspaceId, outputId)),
3048
- });
3565
+ operation: async () => durableOutputResponse(toolNames.shell, executionWorkspaceId, readWorkspaceBashOutput(bashOutputStore, executionWorkspaceId, outputId)),
3566
+ }).then((result) => presentExecutionResult(result, target));
3049
3567
  }
3050
3568
  if (outputId !== undefined)
3051
3569
  throw new Error("bash action=process does not accept outputId.");
@@ -3073,7 +3591,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
3073
3591
  operation: async () => {
3074
3592
  const startedAt = performance.now();
3075
3593
  const snapshot = await processSessions.write({
3076
- workspaceId,
3594
+ workspaceId: executionWorkspaceId,
3077
3595
  processId,
3078
3596
  chars: interrupt ? "\u0003" : input,
3079
3597
  columns,
@@ -3091,7 +3609,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
3091
3609
  success: snapshot.running || snapshot.exitCode === 0,
3092
3610
  durationMs: Math.round(performance.now() - startedAt),
3093
3611
  });
3094
- const response = processToolResponse(toolNames.shell, workspaceId, snapshot, {
3612
+ const response = processToolResponse(toolNames.shell, executionWorkspaceId, snapshot, {
3095
3613
  action,
3096
3614
  processId,
3097
3615
  inputLength: input?.length ?? 0,
@@ -3103,12 +3621,20 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
3103
3621
  if (!snapshot.running) {
3104
3622
  recordBashCompletion(activityLifecycle, bashOutputStore, snapshot.outputId);
3105
3623
  }
3106
- return response;
3624
+ return presentExecutionResult(response, target);
3107
3625
  },
3108
3626
  });
3109
3627
  });
3110
3628
  }
3111
- registerProcessTools(server, config, workspaces, processSessions, hooks, activityLifecycle, bashOutputStore, (input, context) => coreOperations.shellRun(input, context));
3629
+ registerProcessTools(server, config, workspaces, processSessions, hooks, activityLifecycle, bashOutputStore, (input, context) => coreOperations.shellRun(input, context), {
3630
+ resolve: resolveExecutionTarget,
3631
+ prepare: prepareExecutionContext,
3632
+ present: presentExecutionResult,
3633
+ isRemote: (workspaceId) => remoteWorkspaces.has(workspaceId),
3634
+ execCommandRemote: (workspaceId, input, conversationScopeId) => remoteWorkspaces.execCommand(workspaceId, input, conversationScopeId),
3635
+ writeStdinRemote: (workspaceId, input, conversationScopeId) => remoteWorkspaces.writeStdin(workspaceId, input, conversationScopeId),
3636
+ hostScopeIdFor,
3637
+ });
3112
3638
  return server;
3113
3639
  }
3114
3640
  export function createServer(config = loadConfig(), options = {}) {
@@ -3144,8 +3670,8 @@ export function createServer(config = loadConfig(), options = {}) {
3144
3670
  const reviewCheckpoints = createReviewCheckpointManager();
3145
3671
  const processSessions = new ProcessManager({ outputAudit: bashOutputStore });
3146
3672
  const codeIntelligence = new CodeIntelligenceManager(config);
3147
- const localAgentProviders = config.subagents
3148
- ? getLocalAgentProviderAvailabilitySnapshot()
3673
+ const subagentProviders = config.subagents
3674
+ ? getSubagentProviderAvailabilitySnapshot()
3149
3675
  : [];
3150
3676
  const logTransportCloseResults = (reason, results) => {
3151
3677
  let closedCount = 0;
@@ -3328,7 +3854,7 @@ export function createServer(config = loadConfig(), options = {}) {
3328
3854
  });
3329
3855
  }
3330
3856
  };
3331
- const server = createMcpServer(config, workspaces, reviewCheckpoints, processSessions, localAgentProviders, incomingArtifactAdapters, codeIntelligence, activityLifecycle, bashOutputStore, activityQueries);
3857
+ const server = createMcpServer(config, workspaces, reviewCheckpoints, processSessions, subagentProviders, incomingArtifactAdapters, codeIntelligence, activityLifecycle, bashOutputStore, activityQueries);
3332
3858
  await server.connect(transport);
3333
3859
  }
3334
3860
  else {
@@ -3351,7 +3877,7 @@ export function createServer(config = loadConfig(), options = {}) {
3351
3877
  return {
3352
3878
  app,
3353
3879
  config,
3354
- localAgentProviders,
3880
+ subagentProviders,
3355
3881
  close: () => {
3356
3882
  closePromise ??= (async () => {
3357
3883
  clearInterval(transportCleanupTimer);
@@ -3377,7 +3903,7 @@ async function isMainModule() {
3377
3903
  return modulePath === entrypointPath;
3378
3904
  }
3379
3905
  if (await isMainModule()) {
3380
- const { app, config, close, localAgentProviders } = createServer();
3906
+ const { app, config, close, subagentProviders } = createServer();
3381
3907
  const httpServer = app.listen(config.port, config.host, () => {
3382
3908
  console.log(`forgerelay listening on http://${config.host}:${config.port}/mcp`);
3383
3909
  console.log(`allowed roots: ${config.allowedRoots.join(", ")}`);
@@ -3393,7 +3919,7 @@ if (await isMainModule()) {
3393
3919
  : `unsupported on ${process.platform}`;
3394
3920
  console.log(`native artifact download: ${artifactDownloadStatus}`);
3395
3921
  if (config.subagents) {
3396
- console.log(`subagent providers: ${formatLocalAgentProviderAvailabilitySummary(localAgentProviders)}`);
3922
+ console.log(`subagent providers: ${formatSubagentProviderAvailabilitySummary(subagentProviders)}`);
3397
3923
  }
3398
3924
  });
3399
3925
  let shuttingDown = false;