@akira-tl/forgerelay 0.5.4 → 0.5.6

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/dist/server.js CHANGED
@@ -36,6 +36,10 @@ import { createOpenAIIncomingArtifactAdapter, IncomingArtifactAdapterRegistry, }
36
36
  import { logEvent, requestIp, requestPath, commandPreview, transportSessionIdPrefix, workspaceLogLabel, } from "./logger.js";
37
37
  import { editFileTool, readFileTool, writeFileTool, } from "./pi-tools.js";
38
38
  import { SingleUserOAuthProvider } from "./oauth-provider.js";
39
+ import { BatchExecutor } from "./operations/batch/executor.js";
40
+ import { executeBulkRead } from "./operations/bulk-read.js";
41
+ import { NativeBulkMutationExecutor } from "./operations/native-bulk-mutations.js";
42
+ import { createCoreOperationExecutor, } from "./operations/core-operation-executor.js";
39
43
  import { McpTransportRegistry, } from "./mcp-sessions.js";
40
44
  import { ProcessManager, resolveProcessId, } from "./process-sessions.js";
41
45
  import { createReviewCheckpointManager } from "./review-checkpoints.js";
@@ -171,6 +175,7 @@ const capabilityCatalogOutputSchema = z.object({
171
175
  description: z.string(),
172
176
  available: z.boolean(),
173
177
  unavailableReason: z.string().optional(),
178
+ batchPolicy: z.enum(["parallel", "serial", "unsupported"]),
174
179
  guide: capabilityCatalogGuideOutputSchema,
175
180
  });
176
181
  const capabilityErrorOutputSchema = z.object({
@@ -724,6 +729,53 @@ async function reviewWorkspaceChanges(reviewCheckpoints, workspace) {
724
729
  function toolResultIsError(result) {
725
730
  return typeof result === "object" && result !== null && result.isError === true;
726
731
  }
732
+ function toolResultText(result) {
733
+ if (typeof result !== "object" || result === null)
734
+ return String(result ?? "");
735
+ const record = result;
736
+ if (Array.isArray(record.content)) {
737
+ const text = record.content
738
+ .map((entry) => {
739
+ if (typeof entry !== "object" || entry === null)
740
+ return "";
741
+ const value = entry.text;
742
+ return typeof value === "string" ? value : "";
743
+ })
744
+ .filter(Boolean)
745
+ .join("\n");
746
+ if (text)
747
+ return text;
748
+ }
749
+ if (typeof record.structuredContent === "object" && record.structuredContent !== null) {
750
+ const value = record.structuredContent.result;
751
+ if (typeof value === "string")
752
+ return value;
753
+ }
754
+ return "";
755
+ }
756
+ function toolResultContent(result) {
757
+ if (typeof result !== "object" || result === null)
758
+ return [];
759
+ const content = result.content;
760
+ return Array.isArray(content) ? content : [];
761
+ }
762
+ function toolResultAgentsFiles(result) {
763
+ if (typeof result !== "object" || result === null)
764
+ return [];
765
+ const structured = result.structuredContent;
766
+ if (typeof structured !== "object" || structured === null)
767
+ return [];
768
+ const agentsFiles = structured.agentsFiles;
769
+ if (!Array.isArray(agentsFiles))
770
+ return [];
771
+ return agentsFiles.flatMap((entry) => {
772
+ if (typeof entry !== "object" || entry === null)
773
+ return [];
774
+ const path = entry.path;
775
+ const content = entry.content;
776
+ return typeof path === "string" && typeof content === "string" ? [{ path, content }] : [];
777
+ });
778
+ }
727
779
  function workspaceActivitySnapshot(workspace) {
728
780
  return {
729
781
  id: workspace.id,
@@ -781,7 +833,13 @@ function processActivityOutcome(result) {
781
833
  }
782
834
  return { type: "succeeded" };
783
835
  }
784
- function runActivityTool(lifecycle, workspace, requestMeta, tool, request, operation, outcome = standardActivityOutcome) {
836
+ function activityRelationFor(context) {
837
+ return {
838
+ ...(context.parentActivityId ? { parentActivityId: context.parentActivityId } : {}),
839
+ ...(context.turnId ? { turnId: context.turnId } : {}),
840
+ };
841
+ }
842
+ function runActivityTool(lifecycle, workspace, requestMeta, tool, request, operation, outcome = standardActivityOutcome, relation = {}) {
785
843
  return lifecycle.run({
786
844
  tool,
787
845
  workspace: workspaceActivitySnapshot(workspace),
@@ -789,12 +847,13 @@ function runActivityTool(lifecycle, workspace, requestMeta, tool, request, opera
789
847
  request,
790
848
  operation,
791
849
  outcome,
850
+ ...relation,
792
851
  });
793
852
  }
794
- function runActivityToolWithHooks(lifecycle, hooks, workspace, requestMeta, request, hookOptions) {
795
- return runActivityTool(lifecycle, workspace, requestMeta, hookOptions.tool, request, () => runToolWithHooks(hooks, hookOptions));
853
+ function runActivityToolWithHooks(lifecycle, hooks, workspace, requestMeta, request, hookOptions, relation = {}) {
854
+ return runActivityTool(lifecycle, workspace, requestMeta, hookOptions.tool, request, () => runToolWithHooks(hooks, hookOptions), standardActivityOutcome, relation);
796
855
  }
797
- function registerProcessTools(server, config, workspaces, processSessions, hooks, activityLifecycle, bashOutputStore) {
856
+ function registerProcessTools(server, config, workspaces, processSessions, hooks, activityLifecycle, bashOutputStore, shellRun) {
798
857
  if (config.toolMode === "codex") {
799
858
  registerAppTool(server, "exec_command", {
800
859
  title: "Execute command",
@@ -837,70 +896,22 @@ function registerProcessTools(server, config, workspaces, processSessions, hooks
837
896
  outputSchema: processOutputSchema(),
838
897
  ...toolWidgetDescriptorMeta(config, "shell"),
839
898
  annotations: SHELL_TOOL_ANNOTATIONS,
840
- }, async ({ workspaceId, cmd, tty, columns, rows, workingDirectory, yieldTimeMs, timeoutMs, maxOutputTokens }, extra) => {
841
- const workspace = workspaces.getWorkspace(workspaceId);
842
- let undeliveredProcessId;
843
- const activityResult = await runActivityTool(activityLifecycle, workspace, extra._meta, "exec_command", { workspaceId, cmd, tty, columns, rows, workingDirectory, yieldTimeMs, timeoutMs, maxOutputTokens }, async (activityContext) => {
844
- try {
845
- const result = await runToolWithHooks(hooks, {
846
- signal: extra.signal,
847
- tool: "exec_command",
848
- invocation: workspaceHookInvocation(workspace),
849
- payload: { command: cmd, workingDirectory: workingDirectory ?? "." },
850
- operation: async () => {
851
- const startedAt = performance.now();
852
- const cwd = workspaces.resolveWorkingDirectory(workspace, workingDirectory);
853
- await assertWorkspaceInstructionsLoadedBeforeSideEffect(workspaces, workspace, [cwd]);
854
- const snapshot = await processSessions.start({
855
- workspaceId,
856
- command: cmd,
857
- cwd,
858
- workspaceRoot: workspace.root,
859
- tty,
860
- columns,
861
- rows,
862
- yieldTimeMs,
863
- timeoutMs,
864
- maxOutputTokens,
865
- codexCi: true,
866
- signal: extra.signal,
867
- audit: activityContext,
868
- });
869
- undeliveredProcessId = snapshot.running ? snapshot.processId : undefined;
870
- logToolCall(config, {
871
- tool: "exec_command",
872
- ...workspaceLogContext(workspace, extra.sessionId),
873
- workingDirectory: workingDirectory ?? ".",
874
- command: cmd,
875
- commandLength: cmd.length,
876
- exitCode: snapshot.exitCode,
877
- running: snapshot.running,
878
- processId: snapshot.processId,
879
- success: snapshot.running || snapshot.exitCode === 0,
880
- durationMs: Math.round(performance.now() - startedAt),
881
- });
882
- return processToolResponse("exec_command", workspaceId, snapshot, {
883
- command: cmd,
884
- workingDirectory: workingDirectory ?? ".",
885
- running: snapshot.running,
886
- exitCode: snapshot.exitCode,
887
- wallTimeMs: snapshot.wallTimeMs,
888
- });
889
- },
890
- });
891
- extra.signal.throwIfAborted();
892
- return result;
893
- }
894
- catch (error) {
895
- if (undeliveredProcessId !== undefined) {
896
- processSessions.discardUndelivered(workspaceId, undeliveredProcessId);
897
- }
898
- throw error;
899
- }
900
- }, processActivityOutcome);
901
- markReturnedOutput(bashOutputStore, activityResult);
902
- return activityResult;
903
- });
899
+ }, async ({ workspaceId, cmd, tty, columns, rows, workingDirectory, yieldTimeMs, timeoutMs, maxOutputTokens }, extra) => shellRun({
900
+ workspaceId,
901
+ command: cmd,
902
+ surface: "exec_command",
903
+ tty,
904
+ columns,
905
+ rows,
906
+ workingDirectory,
907
+ yieldTimeMs,
908
+ timeoutMs,
909
+ maxOutputTokens,
910
+ }, {
911
+ requestMeta: extra._meta,
912
+ signal: extra.signal,
913
+ sessionId: extra.sessionId,
914
+ }));
904
915
  }
905
916
  if (config.toolMode !== "codex")
906
917
  return;
@@ -1001,8 +1012,27 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
1001
1012
  const incomingArtifactRegistry = new IncomingArtifactAdapterRegistry(incomingArtifactAdapters);
1002
1013
  const artifactDownloadAvailable = config.artifactsEnabled && isArtifactDownloadSupportedPlatform();
1003
1014
  const reviewChangesAvailable = config.widgets === "changes";
1015
+ let batchExecutor;
1016
+ const batchExecuteAvailable = config.toolMode !== "codex";
1004
1017
  const capabilityRegistry = createCapabilityRegistry({
1005
1018
  inspectHooks: (workspaceRoot) => checkHookConfiguration(workspaceRoot, config.hooks),
1019
+ batchExecute: {
1020
+ available: batchExecuteAvailable,
1021
+ unavailableReason: batchExecuteAvailable
1022
+ ? undefined
1023
+ : "batch.execute is unavailable in Codex tool mode because v0.5.5 core batch tasks use the regular Read/Write/Edit/Bash operation surface.",
1024
+ run: async (input, context, options) => {
1025
+ if (!batchExecutor)
1026
+ throw new Error("Batch executor is not initialized.");
1027
+ return {
1028
+ value: await batchExecutor.run(context.workspaceId, input, {
1029
+ requestMeta: options.requestMeta,
1030
+ signal: options.signal,
1031
+ sessionId: options.sessionId,
1032
+ }),
1033
+ };
1034
+ },
1035
+ },
1006
1036
  codeIntelligence: {
1007
1037
  available: true,
1008
1038
  run: async (input, context, options) => {
@@ -1074,6 +1104,510 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
1074
1104
  },
1075
1105
  },
1076
1106
  });
1107
+ const coreOperations = createCoreOperationExecutor({
1108
+ read: async (input, context) => {
1109
+ const { workspaceId, ...readInput } = input;
1110
+ const workspace = workspaces.getWorkspace(workspaceId);
1111
+ return runActivityToolWithHooks(activityLifecycle, hooks, workspace, context.requestMeta, input, {
1112
+ signal: context.signal,
1113
+ tool: toolNames.read,
1114
+ invocation: workspaceHookInvocation(workspace),
1115
+ payload: { path: readInput.path, offset: readInput.offset, limit: readInput.limit },
1116
+ isFailure: toolResultIsError,
1117
+ operation: async () => {
1118
+ const startedAt = performance.now();
1119
+ const readPath = workspaces.resolveReadPath(workspace, readInput.path);
1120
+ const discoveredInstructions = (await workspaces.discoverPathInstructions(workspace, readPath.absolutePath)).filter((file) => file.path !== readPath.absolutePath);
1121
+ const response = await readFileTool({ ...readInput, path: readPath.absolutePath }, {
1122
+ cwd: workspace.root,
1123
+ root: workspace.root,
1124
+ readRoots: readPath.readRoots,
1125
+ });
1126
+ if (response.isError) {
1127
+ logFailedToolResponse(config, {
1128
+ tool: toolNames.read,
1129
+ ...workspaceLogContext(workspace, context.sessionId),
1130
+ path: readInput.path,
1131
+ }, response.content, startedAt);
1132
+ return response;
1133
+ }
1134
+ workspaces.markReadPathLoaded(workspace, readPath);
1135
+ const discoveredInstructionContent = discoveredInstructions.length > 0
1136
+ ? textBlock(formatDiscoveredWorkspaceInstructions(discoveredInstructions, workspace.root))
1137
+ : undefined;
1138
+ const content = discoveredInstructionContent
1139
+ ? [discoveredInstructionContent, ...response.content]
1140
+ : response.content;
1141
+ const summary = {
1142
+ ...textSummary(response.content),
1143
+ offset: readInput.offset ?? 1,
1144
+ limited: readInput.limit !== undefined,
1145
+ };
1146
+ logToolCall(config, {
1147
+ tool: toolNames.read,
1148
+ ...workspaceLogContext(workspace, context.sessionId),
1149
+ path: readInput.path,
1150
+ success: true,
1151
+ durationMs: Math.round(performance.now() - startedAt),
1152
+ });
1153
+ return {
1154
+ ...response,
1155
+ content,
1156
+ _meta: {
1157
+ tool: toolNames.read,
1158
+ card: {
1159
+ workspaceId,
1160
+ path: readInput.path,
1161
+ summary,
1162
+ payload: { content: response.content },
1163
+ },
1164
+ },
1165
+ structuredContent: {
1166
+ result: contentText(content),
1167
+ ...(discoveredInstructions.length > 0
1168
+ ? {
1169
+ agentsFiles: discoveredInstructions.map((file) => ({
1170
+ path: formatAgentsPath(file.path, workspace.root),
1171
+ content: file.content,
1172
+ })),
1173
+ }
1174
+ : {}),
1175
+ },
1176
+ };
1177
+ },
1178
+ }, activityRelationFor(context));
1179
+ },
1180
+ write: async (input, context) => {
1181
+ const { workspaceId, ...writeInput } = input;
1182
+ const workspace = workspaces.getWorkspace(workspaceId);
1183
+ return runActivityToolWithHooks(activityLifecycle, hooks, workspace, context.requestMeta, input, {
1184
+ signal: context.signal,
1185
+ tool: toolNames.write,
1186
+ invocation: workspaceHookInvocation(workspace),
1187
+ payload: { path: writeInput.path },
1188
+ isFailure: toolResultIsError,
1189
+ changedPaths: (result) => toolResultIsError(result) ? [] : [writeInput.path],
1190
+ operation: async () => {
1191
+ const startedAt = performance.now();
1192
+ await assertWorkspaceInstructionsLoadedBeforeSideEffect(workspaces, workspace, [writeInput.path]);
1193
+ const response = await writeFileTool(writeInput, {
1194
+ cwd: workspace.root,
1195
+ root: workspace.root,
1196
+ fileRoots: workspaces.fileToolRoots(workspace),
1197
+ });
1198
+ if (response.isError) {
1199
+ logFailedToolResponse(config, {
1200
+ tool: toolNames.write,
1201
+ ...workspaceLogContext(workspace, context.sessionId),
1202
+ path: writeInput.path,
1203
+ }, response.content, startedAt);
1204
+ return response;
1205
+ }
1206
+ const patch = newFilePatch(writeInput.path, writeInput.content);
1207
+ const stats = countDiffStats(patch);
1208
+ const summary = {
1209
+ ...stats,
1210
+ lines: contentLineCount(writeInput.content),
1211
+ characters: writeInput.content.length,
1212
+ };
1213
+ logToolCall(config, {
1214
+ tool: toolNames.write,
1215
+ ...workspaceLogContext(workspace, context.sessionId),
1216
+ path: writeInput.path,
1217
+ success: true,
1218
+ durationMs: Math.round(performance.now() - startedAt),
1219
+ });
1220
+ return {
1221
+ ...response,
1222
+ _meta: {
1223
+ tool: toolNames.write,
1224
+ card: {
1225
+ workspaceId,
1226
+ path: writeInput.path,
1227
+ summary,
1228
+ payload: {
1229
+ content: response.content,
1230
+ patch,
1231
+ },
1232
+ },
1233
+ },
1234
+ structuredContent: {
1235
+ result: contentText(response.content),
1236
+ },
1237
+ };
1238
+ },
1239
+ }, activityRelationFor(context));
1240
+ },
1241
+ edit: async (input, context) => {
1242
+ const { workspaceId, ...editInput } = input;
1243
+ const workspace = workspaces.getWorkspace(workspaceId);
1244
+ return runActivityToolWithHooks(activityLifecycle, hooks, workspace, context.requestMeta, input, {
1245
+ signal: context.signal,
1246
+ tool: toolNames.edit,
1247
+ invocation: workspaceHookInvocation(workspace),
1248
+ payload: { path: editInput.path, editCount: editInput.edits.length },
1249
+ isFailure: toolResultIsError,
1250
+ changedPaths: (result) => toolResultIsError(result) ? [] : [editInput.path],
1251
+ operation: async () => {
1252
+ const startedAt = performance.now();
1253
+ await assertWorkspaceInstructionsLoadedBeforeSideEffect(workspaces, workspace, [editInput.path]);
1254
+ const response = await editFileTool(editInput, {
1255
+ cwd: workspace.root,
1256
+ root: workspace.root,
1257
+ fileRoots: workspaces.fileToolRoots(workspace),
1258
+ });
1259
+ if (response.isError) {
1260
+ logFailedToolResponse(config, {
1261
+ tool: toolNames.edit,
1262
+ ...workspaceLogContext(workspace, context.sessionId),
1263
+ path: editInput.path,
1264
+ }, response.content, startedAt);
1265
+ return response;
1266
+ }
1267
+ const stats = countDiffStats(response.details?.patch ?? response.details?.diff);
1268
+ const summary = {
1269
+ ...stats,
1270
+ editCount: editInput.edits.length,
1271
+ };
1272
+ const editResultText = `Edited ${editInput.path} (+${stats.additions} -${stats.removals}).`;
1273
+ const editContent = [textBlock(editResultText)];
1274
+ logToolCall(config, {
1275
+ tool: toolNames.edit,
1276
+ ...workspaceLogContext(workspace, context.sessionId),
1277
+ path: editInput.path,
1278
+ success: true,
1279
+ durationMs: Math.round(performance.now() - startedAt),
1280
+ });
1281
+ return {
1282
+ content: editContent,
1283
+ _meta: {
1284
+ tool: toolNames.edit,
1285
+ card: {
1286
+ workspaceId,
1287
+ path: editInput.path,
1288
+ summary,
1289
+ payload: {
1290
+ diff: response.details?.diff,
1291
+ patch: response.details?.patch,
1292
+ },
1293
+ },
1294
+ },
1295
+ structuredContent: {
1296
+ status: "applied",
1297
+ result: contentText(editContent),
1298
+ },
1299
+ };
1300
+ },
1301
+ }, activityRelationFor(context));
1302
+ },
1303
+ rename: async (input, context) => {
1304
+ const { workspaceId, path, newPath } = input;
1305
+ const workspace = workspaces.getWorkspace(workspaceId);
1306
+ return runActivityToolWithHooks(activityLifecycle, hooks, workspace, context.requestMeta, input, {
1307
+ signal: context.signal,
1308
+ tool: toolNames.rename,
1309
+ invocation: workspaceHookInvocation(workspace),
1310
+ payload: { path, newPath, paths: [path, newPath] },
1311
+ changedPaths: () => [path, newPath],
1312
+ operation: async () => {
1313
+ const startedAt = performance.now();
1314
+ try {
1315
+ await assertWorkspaceInstructionsLoadedBeforeSideEffect(workspaces, workspace, [path, newPath]);
1316
+ await renamePath({ path, newPath }, {
1317
+ cwd: workspace.root,
1318
+ allowedRoots: workspaces.fileToolRoots(workspace),
1319
+ });
1320
+ const result = `Renamed ${path} to ${newPath}.`;
1321
+ const content = [textBlock(result)];
1322
+ logToolCall(config, {
1323
+ tool: toolNames.rename,
1324
+ ...workspaceLogContext(workspace, context.sessionId),
1325
+ path: `${path} -> ${newPath}`,
1326
+ success: true,
1327
+ durationMs: Math.round(performance.now() - startedAt),
1328
+ });
1329
+ return {
1330
+ content,
1331
+ _meta: {
1332
+ tool: toolNames.rename,
1333
+ card: {
1334
+ workspaceId,
1335
+ path: newPath,
1336
+ summary: { previousPath: path },
1337
+ payload: { content },
1338
+ },
1339
+ },
1340
+ structuredContent: {
1341
+ result,
1342
+ status: "renamed",
1343
+ path,
1344
+ newPath,
1345
+ },
1346
+ };
1347
+ }
1348
+ catch (error) {
1349
+ logToolCall(config, {
1350
+ tool: toolNames.rename,
1351
+ ...workspaceLogContext(workspace, context.sessionId),
1352
+ path: `${path} -> ${newPath}`,
1353
+ success: false,
1354
+ durationMs: Math.round(performance.now() - startedAt),
1355
+ error: error instanceof Error ? error.message : String(error),
1356
+ });
1357
+ throw error;
1358
+ }
1359
+ },
1360
+ }, activityRelationFor(context));
1361
+ },
1362
+ delete: async (input, context) => {
1363
+ const { workspaceId, path, recursive } = input;
1364
+ const workspace = workspaces.getWorkspace(workspaceId);
1365
+ return runActivityToolWithHooks(activityLifecycle, hooks, workspace, context.requestMeta, input, {
1366
+ signal: context.signal,
1367
+ tool: toolNames.delete,
1368
+ invocation: workspaceHookInvocation(workspace),
1369
+ payload: { path, recursive: recursive ?? false },
1370
+ changedPaths: () => [path],
1371
+ operation: async () => {
1372
+ const startedAt = performance.now();
1373
+ try {
1374
+ await assertWorkspaceInstructionsLoadedBeforeSideEffect(workspaces, workspace, [path]);
1375
+ const deleted = await deletePath({ path, recursive }, {
1376
+ cwd: workspace.root,
1377
+ allowedRoots: workspaces.fileToolRoots(workspace),
1378
+ });
1379
+ const result = `Deleted ${path}${deleted.recursive ? " recursively" : ""}.`;
1380
+ const content = [textBlock(result)];
1381
+ logToolCall(config, {
1382
+ tool: toolNames.delete,
1383
+ ...workspaceLogContext(workspace, context.sessionId),
1384
+ path,
1385
+ success: true,
1386
+ durationMs: Math.round(performance.now() - startedAt),
1387
+ });
1388
+ return {
1389
+ content,
1390
+ _meta: {
1391
+ tool: toolNames.delete,
1392
+ card: {
1393
+ workspaceId,
1394
+ path,
1395
+ summary: { recursive: deleted.recursive },
1396
+ payload: { content },
1397
+ },
1398
+ },
1399
+ structuredContent: {
1400
+ result,
1401
+ status: "deleted",
1402
+ path,
1403
+ recursive: deleted.recursive,
1404
+ },
1405
+ };
1406
+ }
1407
+ catch (error) {
1408
+ logToolCall(config, {
1409
+ tool: toolNames.delete,
1410
+ ...workspaceLogContext(workspace, context.sessionId),
1411
+ path,
1412
+ success: false,
1413
+ durationMs: Math.round(performance.now() - startedAt),
1414
+ error: error instanceof Error ? error.message : String(error),
1415
+ });
1416
+ throw error;
1417
+ }
1418
+ },
1419
+ }, activityRelationFor(context));
1420
+ },
1421
+ shellRun: async (input, context) => {
1422
+ const { workspaceId, command, surface, tty, columns, rows, workingDirectory, yieldTimeMs, timeoutMs, maxOutputTokens, } = input;
1423
+ const workspace = workspaces.getWorkspace(workspaceId);
1424
+ const activityRequest = surface === "exec_command"
1425
+ ? {
1426
+ workspaceId,
1427
+ cmd: command,
1428
+ tty,
1429
+ columns,
1430
+ rows,
1431
+ workingDirectory,
1432
+ yieldTimeMs,
1433
+ timeoutMs,
1434
+ maxOutputTokens,
1435
+ }
1436
+ : {
1437
+ workspaceId,
1438
+ action: "run",
1439
+ command,
1440
+ tty,
1441
+ columns,
1442
+ rows,
1443
+ workingDirectory,
1444
+ yieldTimeMs,
1445
+ timeoutMs,
1446
+ maxOutputTokens,
1447
+ };
1448
+ let undeliveredProcessId;
1449
+ const activityResult = await runActivityTool(activityLifecycle, workspace, context.requestMeta, surface, activityRequest, async (activityContext) => {
1450
+ try {
1451
+ const result = await runToolWithHooks(hooks, {
1452
+ signal: context.signal,
1453
+ tool: surface,
1454
+ invocation: workspaceHookInvocation(workspace),
1455
+ payload: surface === "exec_command"
1456
+ ? { command, workingDirectory: workingDirectory ?? "." }
1457
+ : { action: "run", command, workingDirectory: workingDirectory ?? "." },
1458
+ ...(surface === "bash" ? { isFailure: toolResultIsError } : {}),
1459
+ operation: async () => {
1460
+ const startedAt = performance.now();
1461
+ const cwd = workspaces.resolveWorkingDirectory(workspace, workingDirectory);
1462
+ await assertWorkspaceInstructionsLoadedBeforeSideEffect(workspaces, workspace, [cwd]);
1463
+ const snapshot = await processSessions.start({
1464
+ workspaceId,
1465
+ command,
1466
+ cwd,
1467
+ workspaceRoot: workspace.root,
1468
+ tty,
1469
+ columns,
1470
+ rows,
1471
+ yieldTimeMs,
1472
+ timeoutMs,
1473
+ maxOutputTokens,
1474
+ ...(surface === "exec_command" ? { codexCi: true } : {}),
1475
+ signal: context.signal,
1476
+ audit: activityContext,
1477
+ });
1478
+ undeliveredProcessId = snapshot.running ? snapshot.processId : undefined;
1479
+ logToolCall(config, {
1480
+ tool: surface,
1481
+ ...workspaceLogContext(workspace, context.sessionId),
1482
+ workingDirectory: workingDirectory ?? ".",
1483
+ command,
1484
+ commandLength: command.length,
1485
+ exitCode: snapshot.exitCode,
1486
+ running: snapshot.running,
1487
+ processId: snapshot.processId,
1488
+ success: surface === "exec_command"
1489
+ ? snapshot.running || snapshot.exitCode === 0
1490
+ : snapshot.running || (snapshot.exitCode === 0 && !snapshot.signal),
1491
+ durationMs: Math.round(performance.now() - startedAt),
1492
+ });
1493
+ const response = processToolResponse(surface, workspaceId, snapshot, {
1494
+ ...(surface === "bash" ? { action: "run" } : {}),
1495
+ command,
1496
+ workingDirectory: workingDirectory ?? ".",
1497
+ running: snapshot.running,
1498
+ exitCode: snapshot.exitCode,
1499
+ wallTimeMs: snapshot.wallTimeMs,
1500
+ });
1501
+ return surface === "bash" && !snapshot.running && (snapshot.signal || snapshot.exitCode !== 0)
1502
+ ? { ...response, isError: true }
1503
+ : response;
1504
+ },
1505
+ });
1506
+ context.signal?.throwIfAborted();
1507
+ return result;
1508
+ }
1509
+ catch (error) {
1510
+ if (undeliveredProcessId !== undefined) {
1511
+ processSessions.discardUndelivered(workspaceId, undeliveredProcessId);
1512
+ }
1513
+ throw error;
1514
+ }
1515
+ }, processActivityOutcome, activityRelationFor(context));
1516
+ markReturnedOutput(bashOutputStore, activityResult);
1517
+ return activityResult;
1518
+ },
1519
+ capabilityRun: async (input, context) => {
1520
+ const { workspaceId, name, arguments: capabilityArguments, file } = input;
1521
+ const workspace = workspaces.getWorkspace(workspaceId);
1522
+ let changedPaths = [];
1523
+ return runActivityToolWithHooks(activityLifecycle, hooks, workspace, context.requestMeta, { workspaceId, name, action: "run", arguments: capabilityArguments, file }, {
1524
+ signal: context.signal,
1525
+ tool: toolNames.capability,
1526
+ invocation: workspaceHookInvocation(workspace),
1527
+ payload: { name, action: "run" },
1528
+ isFailure: toolResultIsError,
1529
+ changedPaths: () => changedPaths,
1530
+ operation: async () => {
1531
+ const startedAt = performance.now();
1532
+ try {
1533
+ const execution = await capabilityRegistry.run(name, capabilityArguments ?? {}, capabilityContextFor(workspace), {
1534
+ nativeFile: file,
1535
+ signal: context.signal,
1536
+ requestMeta: context.requestMeta,
1537
+ sessionId: context.sessionId,
1538
+ batch: context.batch,
1539
+ });
1540
+ changedPaths = execution.changedPaths ?? [];
1541
+ const result = {
1542
+ content: [textBlock(`Capability ${name} completed.\n${JSON.stringify(execution.value, null, 2)}`)],
1543
+ ...(execution.card
1544
+ ? {
1545
+ _meta: {
1546
+ tool: toolNames.capability,
1547
+ card: {
1548
+ workspaceId,
1549
+ capabilityName: name,
1550
+ summary: execution.card.summary ?? {},
1551
+ files: execution.card.files,
1552
+ payload: execution.card.payload ?? {},
1553
+ },
1554
+ },
1555
+ }
1556
+ : {}),
1557
+ structuredContent: { name, action: "run", result: execution.value },
1558
+ };
1559
+ logToolCall(config, {
1560
+ tool: toolNames.capability,
1561
+ ...workspaceLogContext(workspace, context.sessionId),
1562
+ capability: name,
1563
+ action: "run",
1564
+ success: true,
1565
+ durationMs: Math.round(performance.now() - startedAt),
1566
+ });
1567
+ return result;
1568
+ }
1569
+ catch (error) {
1570
+ const capabilityError = error instanceof CapabilityError
1571
+ ? error
1572
+ : new CapabilityError("execution_failed", error instanceof Error ? error.message : String(error));
1573
+ const result = {
1574
+ content: [textBlock(`${capabilityError.code}: ${capabilityError.message}`)],
1575
+ structuredContent: {
1576
+ name,
1577
+ action: "run",
1578
+ error: { code: capabilityError.code, message: capabilityError.message },
1579
+ },
1580
+ isError: true,
1581
+ };
1582
+ logFailedToolResponse(config, {
1583
+ tool: toolNames.capability,
1584
+ ...workspaceLogContext(workspace, context.sessionId),
1585
+ capability: name,
1586
+ action: "run",
1587
+ }, result.content, startedAt);
1588
+ return result;
1589
+ }
1590
+ },
1591
+ }, activityRelationFor(context));
1592
+ },
1593
+ });
1594
+ batchExecutor = new BatchExecutor({
1595
+ lifecycle: activityLifecycle,
1596
+ workspaces,
1597
+ coreOperations,
1598
+ resultIsError: toolResultIsError,
1599
+ capabilityBatchPolicy: (name) => capabilityRegistry.batchPolicy(name),
1600
+ shellSurface: "bash",
1601
+ });
1602
+ const nativeBulkMutations = new NativeBulkMutationExecutor({
1603
+ lifecycle: activityLifecycle,
1604
+ workspaces,
1605
+ coreOperations,
1606
+ preflightInstructions: (workspace, paths) => assertWorkspaceInstructionsLoadedBeforeSideEffect(workspaces, workspace, paths),
1607
+ resultIsError: toolResultIsError,
1608
+ resultText: toolResultText,
1609
+ resultContent: toolResultContent,
1610
+ });
1077
1611
  const server = new McpServer({
1078
1612
  name: "forgerelay",
1079
1613
  title: "ForgeRelay",
@@ -1506,60 +2040,82 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
1506
2040
  openWorldHint: true,
1507
2041
  },
1508
2042
  }, async ({ workspaceId, name, action, arguments: capabilityArguments, file }, extra) => {
2043
+ if (action === "run" && name === "batch.execute") {
2044
+ const workspace = workspaces.getWorkspace(workspaceId);
2045
+ const startedAt = performance.now();
2046
+ try {
2047
+ const execution = await capabilityRegistry.run(name, capabilityArguments ?? {}, capabilityContextFor(workspace), {
2048
+ nativeFile: file,
2049
+ signal: extra.signal,
2050
+ requestMeta: extra._meta,
2051
+ sessionId: extra.sessionId,
2052
+ });
2053
+ const result = {
2054
+ content: [textBlock(`Capability ${name} completed.\n${JSON.stringify(execution.value, null, 2)}`)],
2055
+ structuredContent: { name, action, result: execution.value },
2056
+ };
2057
+ logToolCall(config, {
2058
+ tool: toolNames.capability,
2059
+ ...workspaceLogContext(workspace, extra.sessionId),
2060
+ capability: name,
2061
+ action,
2062
+ success: true,
2063
+ durationMs: Math.round(performance.now() - startedAt),
2064
+ });
2065
+ return result;
2066
+ }
2067
+ catch (error) {
2068
+ if (extra.signal.aborted)
2069
+ throw error;
2070
+ const capabilityError = error instanceof CapabilityError
2071
+ ? error
2072
+ : new CapabilityError("execution_failed", error instanceof Error ? error.message : String(error));
2073
+ const result = {
2074
+ content: [textBlock(`${capabilityError.code}: ${capabilityError.message}`)],
2075
+ structuredContent: {
2076
+ name,
2077
+ action,
2078
+ error: { code: capabilityError.code, message: capabilityError.message },
2079
+ },
2080
+ isError: true,
2081
+ };
2082
+ logFailedToolResponse(config, {
2083
+ tool: toolNames.capability,
2084
+ ...workspaceLogContext(workspace, extra.sessionId),
2085
+ capability: name,
2086
+ action,
2087
+ }, result.content, startedAt);
2088
+ return result;
2089
+ }
2090
+ }
2091
+ if (action === "run") {
2092
+ return coreOperations.capabilityRun({ workspaceId, name, arguments: capabilityArguments, file }, {
2093
+ requestMeta: extra._meta,
2094
+ signal: extra.signal,
2095
+ sessionId: extra.sessionId,
2096
+ });
2097
+ }
1509
2098
  const workspace = workspaces.getWorkspace(workspaceId);
1510
- let changedPaths = [];
1511
2099
  return runActivityToolWithHooks(activityLifecycle, hooks, workspace, extra._meta, { workspaceId, name, action, arguments: capabilityArguments, file }, {
1512
2100
  signal: extra.signal,
1513
2101
  tool: toolNames.capability,
1514
2102
  invocation: workspaceHookInvocation(workspace),
1515
2103
  payload: { name, action },
1516
2104
  isFailure: toolResultIsError,
1517
- changedPaths: () => changedPaths,
1518
2105
  operation: async () => {
1519
2106
  const startedAt = performance.now();
1520
2107
  try {
1521
- if (action === "describe") {
1522
- const capability = capabilityRegistry.describe(name, capabilityContextFor(workspace));
1523
- const result = {
1524
- content: [textBlock([
1525
- `${capability.name}: ${capability.description}`,
1526
- `Available: ${capability.available}`,
1527
- `Guide: ${capability.guide.path}`,
1528
- capability.guide.readBeforeFirstUse
1529
- ? "Read the guide before first use when this contract is unfamiliar."
1530
- : undefined,
1531
- ].filter(Boolean).join("\n"))],
1532
- structuredContent: { name, action, capability },
1533
- };
1534
- logToolCall(config, {
1535
- tool: toolNames.capability,
1536
- ...workspaceLogContext(workspace),
1537
- capability: name,
1538
- action,
1539
- success: true,
1540
- durationMs: Math.round(performance.now() - startedAt),
1541
- });
1542
- return result;
1543
- }
1544
- const execution = await capabilityRegistry.run(name, capabilityArguments ?? {}, capabilityContextFor(workspace), { nativeFile: file, signal: extra.signal });
1545
- changedPaths = execution.changedPaths ?? [];
1546
- const result = {
1547
- content: [textBlock(`Capability ${name} completed.\n${JSON.stringify(execution.value, null, 2)}`)],
1548
- ...(execution.card
1549
- ? {
1550
- _meta: {
1551
- tool: toolNames.capability,
1552
- card: {
1553
- workspaceId,
1554
- capabilityName: name,
1555
- summary: execution.card.summary ?? {},
1556
- files: execution.card.files,
1557
- payload: execution.card.payload ?? {},
1558
- },
1559
- },
1560
- }
1561
- : {}),
1562
- structuredContent: { name, action, result: execution.value },
2108
+ const capability = capabilityRegistry.describe(name, capabilityContextFor(workspace));
2109
+ const result = {
2110
+ content: [textBlock([
2111
+ `${capability.name}: ${capability.description}`,
2112
+ `Available: ${capability.available}`,
2113
+ `Guide: ${capability.guide.path}`,
2114
+ capability.guide.readBeforeFirstUse
2115
+ ? "Read the guide before first use when this contract is unfamiliar."
2116
+ : undefined,
2117
+ ].filter(Boolean).join("\n"))],
2118
+ structuredContent: { name, action, capability },
1563
2119
  };
1564
2120
  logToolCall(config, {
1565
2121
  tool: toolNames.capability,
@@ -1708,9 +2264,16 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
1708
2264
  .describe("Workspace identifier returned by open_workspace."),
1709
2265
  path: z
1710
2266
  .string()
2267
+ .optional()
1711
2268
  .describe(config.skillsEnabled
1712
- ? "File path to read, relative to the workspace root or absolute inside the OS temp directory. May also be an advertised skill or capability-guide path from open_workspace, including a ~/... home-relative path."
1713
- : "File path to read, relative to the workspace root or absolute inside the OS temp directory. May also be an advertised capability-guide path from open_workspace."),
2269
+ ? "One file path to read, relative to the workspace root or absolute inside the OS temp directory. May also be an advertised skill or capability-guide path from open_workspace, including a ~/... home-relative path. Use exactly one of path or paths."
2270
+ : "One file path to read, relative to the workspace root or absolute inside the OS temp directory. May also be an advertised capability-guide path from open_workspace. Use exactly one of path or paths."),
2271
+ paths: z
2272
+ .array(z.string())
2273
+ .min(1)
2274
+ .max(100)
2275
+ .optional()
2276
+ .describe("Multiple file paths to read in one call. Uses the same offset/limit for every file. Use exactly one of path or paths."),
1714
2277
  offset: z
1715
2278
  .number()
1716
2279
  .int()
@@ -1726,79 +2289,81 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
1726
2289
  },
1727
2290
  outputSchema: resultOutputSchema({
1728
2291
  agentsFiles: z.array(workspaceAgentsFileOutputSchema).optional(),
2292
+ results: z.array(z.object({
2293
+ path: z.string(),
2294
+ status: z.enum(["done", "error"]),
2295
+ result: z.string(),
2296
+ })).optional(),
2297
+ files: z.number().int().nonnegative().optional(),
2298
+ failed: z.number().int().nonnegative().optional(),
1729
2299
  }),
1730
2300
  ...toolWidgetDescriptorMeta(config, "read"),
1731
2301
  annotations: { readOnlyHint: true },
1732
- }, async ({ workspaceId, ...input }, extra) => {
2302
+ }, async ({ workspaceId, path, paths, offset, limit }, extra) => {
2303
+ if ((path === undefined) === (paths === undefined)) {
2304
+ throw new Error("read requires exactly one of path or paths.");
2305
+ }
2306
+ if (path !== undefined) {
2307
+ return coreOperations.read({ workspaceId, path, offset, limit }, {
2308
+ requestMeta: extra._meta,
2309
+ signal: extra.signal,
2310
+ sessionId: extra.sessionId,
2311
+ });
2312
+ }
1733
2313
  const workspace = workspaces.getWorkspace(workspaceId);
1734
- return runActivityToolWithHooks(activityLifecycle, hooks, workspace, extra._meta, { workspaceId, ...input }, {
1735
- signal: extra.signal,
1736
- tool: toolNames.read,
1737
- invocation: workspaceHookInvocation(workspace),
1738
- payload: { path: input.path, offset: input.offset, limit: input.limit },
1739
- isFailure: toolResultIsError,
1740
- operation: async () => {
1741
- const startedAt = performance.now();
1742
- const readPath = workspaces.resolveReadPath(workspace, input.path);
1743
- const discoveredInstructions = (await workspaces.discoverPathInstructions(workspace, readPath.absolutePath)).filter((file) => file.path !== readPath.absolutePath);
1744
- const response = await readFileTool({ ...input, path: readPath.absolutePath }, {
1745
- cwd: workspace.root,
1746
- root: workspace.root,
1747
- readRoots: readPath.readRoots,
1748
- });
1749
- if (response.isError) {
1750
- logFailedToolResponse(config, {
1751
- tool: toolNames.read,
1752
- ...workspaceLogContext(workspace, extra.sessionId),
1753
- path: input.path,
1754
- }, response.content, startedAt);
1755
- return response;
1756
- }
1757
- workspaces.markReadPathLoaded(workspace, readPath);
1758
- const discoveredInstructionContent = discoveredInstructions.length > 0
1759
- ? textBlock(formatDiscoveredWorkspaceInstructions(discoveredInstructions, workspace.root))
1760
- : undefined;
1761
- const content = discoveredInstructionContent
1762
- ? [discoveredInstructionContent, ...response.content]
1763
- : response.content;
1764
- const summary = {
1765
- ...textSummary(response.content),
1766
- offset: input.offset ?? 1,
1767
- limited: input.limit !== undefined,
1768
- };
1769
- logToolCall(config, {
1770
- tool: toolNames.read,
1771
- ...workspaceLogContext(workspace, extra.sessionId),
1772
- path: input.path,
1773
- success: true,
1774
- durationMs: Math.round(performance.now() - startedAt),
1775
- });
1776
- return {
1777
- ...response,
1778
- content,
1779
- _meta: {
1780
- tool: toolNames.read,
1781
- card: {
1782
- workspaceId,
1783
- path: input.path,
1784
- summary,
1785
- payload: { content: response.content },
1786
- },
1787
- },
1788
- structuredContent: {
1789
- result: contentText(content),
1790
- ...(discoveredInstructions.length > 0
1791
- ? {
1792
- agentsFiles: discoveredInstructions.map((file) => ({
1793
- path: formatAgentsPath(file.path, workspace.root),
1794
- content: file.content,
1795
- })),
1796
- }
1797
- : {}),
1798
- },
1799
- };
1800
- },
1801
- });
2314
+ let response;
2315
+ await runActivityTool(activityLifecycle, workspace, extra._meta, toolNames.read, { workspaceId, paths, offset, limit }, async (parentContext) => {
2316
+ const execution = await executeBulkRead({
2317
+ paths: paths,
2318
+ signal: extra.signal,
2319
+ run: (childPath) => coreOperations.read({ workspaceId, path: childPath, offset, limit }, {
2320
+ requestMeta: extra._meta,
2321
+ signal: extra.signal,
2322
+ sessionId: extra.sessionId,
2323
+ parentActivityId: parentContext.activityId,
2324
+ turnId: parentContext.turnId,
2325
+ }),
2326
+ isError: toolResultIsError,
2327
+ resultText: toolResultText,
2328
+ });
2329
+ const content = execution.children.flatMap((child) => [
2330
+ textBlock(`--- ${child.path} · ${child.status} ---`),
2331
+ ...(child.response
2332
+ ? toolResultContent(child.response)
2333
+ : [textBlock(child.result)]),
2334
+ ]);
2335
+ const seenAgentPaths = new Set();
2336
+ const agentsFiles = execution.children.flatMap((child) => child.response ? toolResultAgentsFiles(child.response) : []).filter((file) => {
2337
+ if (seenAgentPaths.has(file.path))
2338
+ return false;
2339
+ seenAgentPaths.add(file.path);
2340
+ return true;
2341
+ });
2342
+ response = {
2343
+ content,
2344
+ structuredContent: {
2345
+ result: contentText(content),
2346
+ results: execution.children.map(({ path: childPath, status, result }) => ({
2347
+ path: childPath,
2348
+ status,
2349
+ result,
2350
+ })),
2351
+ files: execution.children.length,
2352
+ failed: execution.failed,
2353
+ ...(agentsFiles.length > 0 ? { agentsFiles } : {}),
2354
+ },
2355
+ };
2356
+ return {
2357
+ childCount: execution.children.length,
2358
+ succeeded: execution.succeeded,
2359
+ failed: execution.failed,
2360
+ };
2361
+ }, (summary) => summary.failed > 0
2362
+ ? { type: "failed", error: `${summary.failed} of ${summary.childCount} child Reads failed.` }
2363
+ : { type: "succeeded" });
2364
+ if (!response)
2365
+ throw new Error("Bulk Read completed without a response.");
2366
+ return response;
1802
2367
  });
1803
2368
  if (config.toolMode !== "codex") {
1804
2369
  registerAppTool(server, toolNames.write, {
@@ -1816,66 +2381,11 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
1816
2381
  outputSchema: resultOutputSchema(),
1817
2382
  ...toolWidgetDescriptorMeta(config, "write"),
1818
2383
  annotations: WRITE_TOOL_ANNOTATIONS,
1819
- }, async ({ workspaceId, ...input }, extra) => {
1820
- const workspace = workspaces.getWorkspace(workspaceId);
1821
- return runActivityToolWithHooks(activityLifecycle, hooks, workspace, extra._meta, { workspaceId, ...input }, {
1822
- signal: extra.signal,
1823
- tool: toolNames.write,
1824
- invocation: workspaceHookInvocation(workspace),
1825
- payload: { path: input.path },
1826
- isFailure: toolResultIsError,
1827
- changedPaths: (result) => toolResultIsError(result) ? [] : [input.path],
1828
- operation: async () => {
1829
- const startedAt = performance.now();
1830
- await assertWorkspaceInstructionsLoadedBeforeSideEffect(workspaces, workspace, [input.path]);
1831
- const response = await writeFileTool(input, {
1832
- cwd: workspace.root,
1833
- root: workspace.root,
1834
- fileRoots: workspaces.fileToolRoots(workspace),
1835
- });
1836
- if (response.isError) {
1837
- logFailedToolResponse(config, {
1838
- tool: toolNames.write,
1839
- ...workspaceLogContext(workspace, extra.sessionId),
1840
- path: input.path,
1841
- }, response.content, startedAt);
1842
- return response;
1843
- }
1844
- const patch = newFilePatch(input.path, input.content);
1845
- const stats = countDiffStats(patch);
1846
- const summary = {
1847
- ...stats,
1848
- lines: contentLineCount(input.content),
1849
- characters: input.content.length,
1850
- };
1851
- logToolCall(config, {
1852
- tool: toolNames.write,
1853
- ...workspaceLogContext(workspace, extra.sessionId),
1854
- path: input.path,
1855
- success: true,
1856
- durationMs: Math.round(performance.now() - startedAt),
1857
- });
1858
- return {
1859
- ...response,
1860
- _meta: {
1861
- tool: toolNames.write,
1862
- card: {
1863
- workspaceId,
1864
- path: input.path,
1865
- summary,
1866
- payload: {
1867
- content: response.content,
1868
- patch,
1869
- },
1870
- },
1871
- },
1872
- structuredContent: {
1873
- result: contentText(response.content),
1874
- },
1875
- };
1876
- },
1877
- });
1878
- });
2384
+ }, async ({ workspaceId, ...input }, extra) => coreOperations.write({ workspaceId, ...input }, {
2385
+ requestMeta: extra._meta,
2386
+ signal: extra.signal,
2387
+ sessionId: extra.sessionId,
2388
+ }));
1879
2389
  registerAppTool(server, toolNames.edit, {
1880
2390
  title: "Edit file",
1881
2391
  description: toolDescriptions.edit,
@@ -1885,7 +2395,14 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
1885
2395
  .describe("Workspace identifier returned by open_workspace."),
1886
2396
  path: z
1887
2397
  .string()
1888
- .describe("File path to edit, relative to the workspace root or absolute inside the OS temp directory."),
2398
+ .optional()
2399
+ .describe("One file path to edit. Use exactly one of path or paths."),
2400
+ paths: z
2401
+ .array(z.string())
2402
+ .min(1)
2403
+ .max(100)
2404
+ .optional()
2405
+ .describe("Multiple file paths to edit with the same edits. Use exactly one of path or paths."),
1889
2406
  edits: z
1890
2407
  .array(z.object({
1891
2408
  oldText: z
@@ -1896,69 +2413,34 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
1896
2413
  .min(1),
1897
2414
  },
1898
2415
  outputSchema: resultOutputSchema({
1899
- status: z.literal("applied"),
2416
+ status: z.enum(["applied", "partial"]),
2417
+ results: z.array(z.object({
2418
+ path: z.string(),
2419
+ status: z.enum(["done", "error", "unexecuted"]),
2420
+ result: z.string().optional(),
2421
+ })).optional(),
2422
+ files: z.number().int().nonnegative().optional(),
2423
+ completed: z.number().int().nonnegative().optional(),
2424
+ failed: z.number().int().nonnegative().optional(),
2425
+ unexecuted: z.number().int().nonnegative().optional(),
1900
2426
  }),
1901
2427
  ...toolWidgetDescriptorMeta(config, "edit"),
1902
2428
  annotations: EDIT_TOOL_ANNOTATIONS,
1903
- }, async ({ workspaceId, ...input }, extra) => {
1904
- const workspace = workspaces.getWorkspace(workspaceId);
1905
- return runActivityToolWithHooks(activityLifecycle, hooks, workspace, extra._meta, { workspaceId, ...input }, {
2429
+ }, async ({ workspaceId, path, paths, edits }, extra) => {
2430
+ if ((path === undefined) === (paths === undefined)) {
2431
+ throw new Error("edit requires exactly one of path or paths.");
2432
+ }
2433
+ if (path !== undefined) {
2434
+ return coreOperations.edit({ workspaceId, path, edits }, {
2435
+ requestMeta: extra._meta,
2436
+ signal: extra.signal,
2437
+ sessionId: extra.sessionId,
2438
+ });
2439
+ }
2440
+ return nativeBulkMutations.edit({ workspaceId, paths: paths, edits }, {
2441
+ requestMeta: extra._meta,
1906
2442
  signal: extra.signal,
1907
- tool: toolNames.edit,
1908
- invocation: workspaceHookInvocation(workspace),
1909
- payload: { path: input.path, editCount: input.edits.length },
1910
- isFailure: toolResultIsError,
1911
- changedPaths: (result) => toolResultIsError(result) ? [] : [input.path],
1912
- operation: async () => {
1913
- const startedAt = performance.now();
1914
- await assertWorkspaceInstructionsLoadedBeforeSideEffect(workspaces, workspace, [input.path]);
1915
- const response = await editFileTool(input, {
1916
- cwd: workspace.root,
1917
- root: workspace.root,
1918
- fileRoots: workspaces.fileToolRoots(workspace),
1919
- });
1920
- if (response.isError) {
1921
- logFailedToolResponse(config, {
1922
- tool: toolNames.edit,
1923
- ...workspaceLogContext(workspace, extra.sessionId),
1924
- path: input.path,
1925
- }, response.content, startedAt);
1926
- return response;
1927
- }
1928
- const stats = countDiffStats(response.details?.patch ?? response.details?.diff);
1929
- const summary = {
1930
- ...stats,
1931
- editCount: input.edits.length,
1932
- };
1933
- const editResultText = `Edited ${input.path} (+${stats.additions} -${stats.removals}).`;
1934
- const editContent = [textBlock(editResultText)];
1935
- logToolCall(config, {
1936
- tool: toolNames.edit,
1937
- ...workspaceLogContext(workspace, extra.sessionId),
1938
- path: input.path,
1939
- success: true,
1940
- durationMs: Math.round(performance.now() - startedAt),
1941
- });
1942
- return {
1943
- content: editContent,
1944
- _meta: {
1945
- tool: toolNames.edit,
1946
- card: {
1947
- workspaceId,
1948
- path: input.path,
1949
- summary,
1950
- payload: {
1951
- diff: response.details?.diff,
1952
- patch: response.details?.patch,
1953
- },
1954
- },
1955
- },
1956
- structuredContent: {
1957
- status: "applied",
1958
- result: contentText(editContent),
1959
- },
1960
- };
1961
- },
2443
+ sessionId: extra.sessionId,
1962
2444
  });
1963
2445
  });
1964
2446
  }
@@ -1977,135 +2459,56 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
1977
2459
  }),
1978
2460
  ...toolWidgetDescriptorMeta(config, "edit"),
1979
2461
  annotations: EDIT_TOOL_ANNOTATIONS,
1980
- }, async ({ workspaceId, path, newPath }, extra) => {
1981
- const workspace = workspaces.getWorkspace(workspaceId);
1982
- return runActivityToolWithHooks(activityLifecycle, hooks, workspace, extra._meta, { workspaceId, path, newPath }, {
1983
- signal: extra.signal,
1984
- tool: toolNames.rename,
1985
- invocation: workspaceHookInvocation(workspace),
1986
- payload: { path, newPath, paths: [path, newPath] },
1987
- changedPaths: () => [path, newPath],
1988
- operation: async () => {
1989
- const startedAt = performance.now();
1990
- try {
1991
- await assertWorkspaceInstructionsLoadedBeforeSideEffect(workspaces, workspace, [path, newPath]);
1992
- await renamePath({ path, newPath }, {
1993
- cwd: workspace.root,
1994
- allowedRoots: workspaces.fileToolRoots(workspace),
1995
- });
1996
- const result = `Renamed ${path} to ${newPath}.`;
1997
- const content = [textBlock(result)];
1998
- logToolCall(config, {
1999
- tool: toolNames.rename,
2000
- ...workspaceLogContext(workspace, extra.sessionId),
2001
- path: `${path} -> ${newPath}`,
2002
- success: true,
2003
- durationMs: Math.round(performance.now() - startedAt),
2004
- });
2005
- return {
2006
- content,
2007
- _meta: {
2008
- tool: toolNames.rename,
2009
- card: {
2010
- workspaceId,
2011
- path: newPath,
2012
- summary: { previousPath: path },
2013
- payload: { content },
2014
- },
2015
- },
2016
- structuredContent: {
2017
- result,
2018
- status: "renamed",
2019
- path,
2020
- newPath,
2021
- },
2022
- };
2023
- }
2024
- catch (error) {
2025
- logToolCall(config, {
2026
- tool: toolNames.rename,
2027
- ...workspaceLogContext(workspace, extra.sessionId),
2028
- path: `${path} -> ${newPath}`,
2029
- success: false,
2030
- durationMs: Math.round(performance.now() - startedAt),
2031
- error: error instanceof Error ? error.message : String(error),
2032
- });
2033
- throw error;
2034
- }
2035
- },
2036
- });
2037
- });
2462
+ }, async ({ workspaceId, path, newPath }, extra) => coreOperations.rename({ workspaceId, path, newPath }, {
2463
+ requestMeta: extra._meta,
2464
+ signal: extra.signal,
2465
+ sessionId: extra.sessionId,
2466
+ }));
2038
2467
  registerAppTool(server, toolNames.delete, {
2039
2468
  title: "Delete path",
2040
2469
  description: toolDescriptions.delete,
2041
2470
  inputSchema: {
2042
2471
  workspaceId: z.string().describe("Workspace identifier returned by open_workspace."),
2043
- path: z.string().describe("File or directory path relative to the workspace root, or absolute inside the OS temp directory."),
2044
- recursive: z.boolean().optional().describe("Delete a non-empty directory tree. Defaults to false."),
2472
+ path: z.string().optional().describe("One file or directory path to delete. Use exactly one of path or paths."),
2473
+ paths: z
2474
+ .array(z.string())
2475
+ .min(1)
2476
+ .max(100)
2477
+ .optional()
2478
+ .describe("Multiple paths to delete in one call. Use exactly one of path or paths."),
2479
+ recursive: z.boolean().optional().describe("Delete non-empty directory trees. Defaults to false and applies to every bulk target."),
2045
2480
  },
2046
2481
  outputSchema: resultOutputSchema({
2047
- status: z.literal("deleted"),
2048
- path: z.string(),
2049
- recursive: z.boolean(),
2482
+ status: z.enum(["deleted", "partial"]),
2483
+ path: z.string().optional(),
2484
+ recursive: z.boolean().optional(),
2485
+ results: z.array(z.object({
2486
+ path: z.string(),
2487
+ status: z.enum(["done", "error", "unexecuted"]),
2488
+ result: z.string().optional(),
2489
+ })).optional(),
2490
+ paths: z.number().int().nonnegative().optional(),
2491
+ completed: z.number().int().nonnegative().optional(),
2492
+ failed: z.number().int().nonnegative().optional(),
2493
+ unexecuted: z.number().int().nonnegative().optional(),
2050
2494
  }),
2051
2495
  ...toolWidgetDescriptorMeta(config, "edit"),
2052
2496
  annotations: EDIT_TOOL_ANNOTATIONS,
2053
- }, async ({ workspaceId, path, recursive }, extra) => {
2054
- const workspace = workspaces.getWorkspace(workspaceId);
2055
- return runActivityToolWithHooks(activityLifecycle, hooks, workspace, extra._meta, { workspaceId, path, recursive }, {
2497
+ }, async ({ workspaceId, path, paths, recursive }, extra) => {
2498
+ if ((path === undefined) === (paths === undefined)) {
2499
+ throw new Error("delete requires exactly one of path or paths.");
2500
+ }
2501
+ if (path !== undefined) {
2502
+ return coreOperations.delete({ workspaceId, path, recursive }, {
2503
+ requestMeta: extra._meta,
2504
+ signal: extra.signal,
2505
+ sessionId: extra.sessionId,
2506
+ });
2507
+ }
2508
+ return nativeBulkMutations.delete({ workspaceId, paths: paths, recursive }, {
2509
+ requestMeta: extra._meta,
2056
2510
  signal: extra.signal,
2057
- tool: toolNames.delete,
2058
- invocation: workspaceHookInvocation(workspace),
2059
- payload: { path, recursive: recursive ?? false },
2060
- changedPaths: () => [path],
2061
- operation: async () => {
2062
- const startedAt = performance.now();
2063
- try {
2064
- await assertWorkspaceInstructionsLoadedBeforeSideEffect(workspaces, workspace, [path]);
2065
- const deleted = await deletePath({ path, recursive }, {
2066
- cwd: workspace.root,
2067
- allowedRoots: workspaces.fileToolRoots(workspace),
2068
- });
2069
- const result = `Deleted ${path}${deleted.recursive ? " recursively" : ""}.`;
2070
- const content = [textBlock(result)];
2071
- logToolCall(config, {
2072
- tool: toolNames.delete,
2073
- ...workspaceLogContext(workspace, extra.sessionId),
2074
- path,
2075
- success: true,
2076
- durationMs: Math.round(performance.now() - startedAt),
2077
- });
2078
- return {
2079
- content,
2080
- _meta: {
2081
- tool: toolNames.delete,
2082
- card: {
2083
- workspaceId,
2084
- path,
2085
- summary: { recursive: deleted.recursive },
2086
- payload: { content },
2087
- },
2088
- },
2089
- structuredContent: {
2090
- result,
2091
- status: "deleted",
2092
- path,
2093
- recursive: deleted.recursive,
2094
- },
2095
- };
2096
- }
2097
- catch (error) {
2098
- logToolCall(config, {
2099
- tool: toolNames.delete,
2100
- ...workspaceLogContext(workspace, extra.sessionId),
2101
- path,
2102
- success: false,
2103
- durationMs: Math.round(performance.now() - startedAt),
2104
- error: error instanceof Error ? error.message : String(error),
2105
- });
2106
- throw error;
2107
- }
2108
- },
2511
+ sessionId: extra.sessionId,
2109
2512
  });
2110
2513
  });
2111
2514
  if (config.toolMode === "codex") {
@@ -2272,11 +2675,10 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
2272
2675
  if (processId !== undefined || outputId !== undefined || input !== undefined || interrupt !== undefined) {
2273
2676
  throw new Error("bash action=run does not accept processId, outputId, input, or interrupt.");
2274
2677
  }
2275
- let undeliveredProcessId;
2276
- const activityResult = await runActivityTool(activityLifecycle, workspace, extra._meta, toolNames.shell, {
2678
+ return coreOperations.shellRun({
2277
2679
  workspaceId,
2278
- action,
2279
2680
  command,
2681
+ surface: "bash",
2280
2682
  tty,
2281
2683
  columns,
2282
2684
  rows,
@@ -2284,74 +2686,11 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
2284
2686
  yieldTimeMs,
2285
2687
  timeoutMs,
2286
2688
  maxOutputTokens,
2287
- }, async (activityContext) => {
2288
- try {
2289
- const result = await runToolWithHooks(hooks, {
2290
- signal: extra.signal,
2291
- tool: toolNames.shell,
2292
- invocation: workspaceHookInvocation(workspace),
2293
- payload: {
2294
- action,
2295
- command,
2296
- workingDirectory: workingDirectory ?? ".",
2297
- },
2298
- isFailure: toolResultIsError,
2299
- operation: async () => {
2300
- const startedAt = performance.now();
2301
- const cwd = workspaces.resolveWorkingDirectory(workspace, workingDirectory);
2302
- await assertWorkspaceInstructionsLoadedBeforeSideEffect(workspaces, workspace, [cwd]);
2303
- const snapshot = await processSessions.start({
2304
- workspaceId,
2305
- command,
2306
- cwd,
2307
- workspaceRoot: workspace.root,
2308
- tty,
2309
- columns,
2310
- rows,
2311
- yieldTimeMs,
2312
- timeoutMs,
2313
- maxOutputTokens,
2314
- signal: extra.signal,
2315
- audit: activityContext,
2316
- });
2317
- undeliveredProcessId = snapshot.running ? snapshot.processId : undefined;
2318
- logToolCall(config, {
2319
- tool: toolNames.shell,
2320
- ...workspaceLogContext(workspace, extra.sessionId),
2321
- workingDirectory: workingDirectory ?? ".",
2322
- command,
2323
- commandLength: command.length,
2324
- exitCode: snapshot.exitCode,
2325
- running: snapshot.running,
2326
- processId: snapshot.processId,
2327
- success: snapshot.running || (snapshot.exitCode === 0 && !snapshot.signal),
2328
- durationMs: Math.round(performance.now() - startedAt),
2329
- });
2330
- const response = processToolResponse(toolNames.shell, workspaceId, snapshot, {
2331
- action,
2332
- command,
2333
- workingDirectory: workingDirectory ?? ".",
2334
- running: snapshot.running,
2335
- exitCode: snapshot.exitCode,
2336
- wallTimeMs: snapshot.wallTimeMs,
2337
- });
2338
- return !snapshot.running && (snapshot.signal || snapshot.exitCode !== 0)
2339
- ? { ...response, isError: true }
2340
- : response;
2341
- },
2342
- });
2343
- extra.signal.throwIfAborted();
2344
- return result;
2345
- }
2346
- catch (error) {
2347
- if (undeliveredProcessId !== undefined) {
2348
- processSessions.discardUndelivered(workspaceId, undeliveredProcessId);
2349
- }
2350
- throw error;
2351
- }
2352
- }, processActivityOutcome);
2353
- markReturnedOutput(bashOutputStore, activityResult);
2354
- return activityResult;
2689
+ }, {
2690
+ requestMeta: extra._meta,
2691
+ signal: extra.signal,
2692
+ sessionId: extra.sessionId,
2693
+ });
2355
2694
  }
2356
2695
  if (action === "output") {
2357
2696
  if (!outputId)
@@ -2430,7 +2769,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
2430
2769
  });
2431
2770
  });
2432
2771
  }
2433
- registerProcessTools(server, config, workspaces, processSessions, hooks, activityLifecycle, bashOutputStore);
2772
+ registerProcessTools(server, config, workspaces, processSessions, hooks, activityLifecycle, bashOutputStore, (input, context) => coreOperations.shellRun(input, context));
2434
2773
  return server;
2435
2774
  }
2436
2775
  export function createServer(config = loadConfig(), options = {}) {