@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.
- package/CHANGELOG.md +43 -0
- package/README.md +38 -0
- package/capabilities/subagents/GUIDE.md +100 -40
- package/dist/activity/lifecycle.js +1 -1
- package/dist/activity/mcp-query-tools.js +1 -0
- package/dist/activity/query-service.js +1 -0
- package/dist/capabilities.js +1 -1
- package/dist/capability-registry.js +34 -0
- package/dist/cli.js +80 -165
- package/dist/composite-activity.js +155 -0
- package/dist/composite-workspaces.js +197 -0
- package/dist/db/migrations.js +14 -0
- package/dist/db/schema.js +6 -0
- package/dist/remote-workspace-relay.js +16 -0
- package/dist/server.js +698 -172
- package/dist/{local-agent-targets.js → subagents/cli-target.js} +6 -6
- package/dist/{local-agent-profiles.js → subagents/profiles.js} +13 -5
- package/dist/subagents/providers/adapters/acp.js +148 -0
- package/dist/subagents/providers/adapters/claude.js +75 -0
- package/dist/{local-agent-runtime.js → subagents/providers/adapters/codex.js} +10 -4
- package/dist/subagents/providers/adapters/opencode.js +137 -0
- package/dist/subagents/providers/adapters/pi.js +232 -0
- package/dist/{local-agent-availability.js → subagents/providers/availability.js} +24 -10
- package/dist/subagents/providers/continuation.js +11 -0
- package/dist/subagents/providers/contract.js +1 -0
- package/dist/subagents/providers/registry.js +26 -0
- package/dist/subagents/providers/shared.js +40 -0
- package/dist/subagents/sessions/capability.js +214 -0
- package/dist/subagents/sessions/delivery-mailbox.js +115 -0
- package/dist/subagents/sessions/execution.js +171 -0
- package/dist/subagents/sessions/manager.js +107 -0
- package/dist/subagents/sessions/mcp/audit.js +85 -0
- package/dist/subagents/sessions/mcp/runtime.js +19 -0
- package/dist/{local-agent-store.js → subagents/sessions/store.js} +75 -39
- package/dist/ui/.vite/manifest.json +33 -33
- package/dist/ui/activity-panel-app.html +3 -3
- package/dist/ui/assets/{activity-panel-app-CjZVvVNc.js → activity-panel-app-E1ju2dqI.js} +1 -1
- package/dist/ui/assets/{heavy-payload-vGgBRvNX.js → heavy-payload-CeW-n9w5.js} +1 -1
- package/dist/ui/assets/{review-payload-4erWKckt.js → review-payload-B9CO298v.js} +1 -1
- package/dist/ui/assets/{scrollbar-CaOPzUJd.js → scrollbar-C2twAENW.js} +1 -1
- package/dist/ui/assets/workspace-app-BztEvZIC.js +5 -0
- package/dist/ui/assets/{workspace-app-DkAiSl_0.js → workspace-app-CwbJnb_w.js} +1 -1
- package/dist/ui/assets/workspace-app-YnUST8IP.css +1 -0
- package/dist/ui/assets/workspace-app-rKuhdae8.js +1 -0
- package/dist/ui/assets/workspace-lifecycle-app-CEfMdudP.js +1 -0
- package/dist/ui/workspace-app.html +4 -4
- package/dist/ui/workspace-lifecycle-app.html +4 -4
- package/dist/workspaces.js +3 -3
- package/docs/chatgpt-coding-workflow.md +2 -9
- package/docs/configuration.md +23 -0
- package/docs/debugging.md +7 -0
- package/docs/roadmap.md +42 -7
- package/package.json +2 -2
- package/scripts/debug/runtime.mjs +23 -1
- package/scripts/debug/runtime.test.mjs +14 -2
- package/scripts/debug/serve.mjs +4 -4
- package/scripts/release/release-gate.test.mjs +2 -2
- package/dist/local-agent-adapters.js +0 -653
- package/dist/ui/assets/workspace-app-CcrHAUIn.css +0 -1
- package/dist/ui/assets/workspace-app-DJmkPYJC.js +0 -1
- package/dist/ui/assets/workspace-app-QyauBrJX.js +0 -5
- package/dist/ui/assets/workspace-lifecycle-app-BIXEo53I.js +0 -1
- /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 {
|
|
56
|
-
import {
|
|
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
|
|
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
|
|
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) =>
|
|
1017
|
-
workspaceId,
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
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
|
|
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",
|
|
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",
|
|
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,
|
|
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
|
|
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
|
|
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("
|
|
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(
|
|
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
|
|
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 ?
|
|
2546
|
+
const cardAgentProviders = config.subagents ? subagentProviders : [];
|
|
2097
2547
|
const cardAgents = workspace.agentProfiles.map((profile) => {
|
|
2098
|
-
const summary =
|
|
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(
|
|
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(
|
|
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) =>
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
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
|
-
|
|
2309
|
-
|
|
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(
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
2610
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
2692
|
-
|
|
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
|
-
|
|
2745
|
-
|
|
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
|
-
|
|
2778
|
-
|
|
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
|
-
|
|
2821
|
-
|
|
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
|
|
2861
|
-
|
|
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
|
-
|
|
2994
|
-
|
|
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(
|
|
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,
|
|
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,
|
|
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
|
|
3148
|
-
?
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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: ${
|
|
3922
|
+
console.log(`subagent providers: ${formatSubagentProviderAvailabilitySummary(subagentProviders)}`);
|
|
3397
3923
|
}
|
|
3398
3924
|
});
|
|
3399
3925
|
let shuttingDown = false;
|