@akira-tl/forgerelay 0.8.2 → 0.8.4

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
@@ -21,7 +21,7 @@ import { HostTurnStore } from "./activity/host-turn-store.js";
21
21
  import { registerActivityQueryTools } from "./activity/mcp-query-tools.js";
22
22
  import { ActivityLifecycle, } from "./activity/lifecycle.js";
23
23
  import { ActivityQueryService } from "./activity/query-service.js";
24
- import { buildCapabilityFingerprint } from "./capabilities.js";
24
+ import { buildCapabilityFingerprint, loadCapabilityGuides } from "./capabilities.js";
25
25
  import { CapabilityError, createCapabilityRegistry, } from "./capability-registry.js";
26
26
  import { deletePath, renamePath } from "./file-mutations.js";
27
27
  import { downloadIncomingArtifact, isArtifactDownloadSupportedPlatform, } from "./artifact-tools.js";
@@ -53,6 +53,8 @@ import { ACTIVITY_PANEL_APP_LEGACY_URI, ACTIVITY_PANEL_APP_URI_TEMPLATE, MCP_APP
53
53
  import { shutdownHttpServer } from "./server-shutdown.js";
54
54
  import { formatPathForPrompt } from "./skills.js";
55
55
  import { createWorkspaceStore } from "./workspace-store.js";
56
+ import { WorkspaceTaskReminderTracker } from "./workspace-task-reminders.js";
57
+ import { WorkspaceTaskStore } from "./workspace-tasks.js";
56
58
  import { formatAgentsPath, WorkspaceRegistry } from "./workspaces.js";
57
59
  import { formatAvailableSubagentProfile, summarizeSubagentProfile } from "./subagents/profiles.js";
58
60
  import { formatSubagentProviderAvailabilitySummary, formatUnavailableSubagentProvider, getSubagentProviderAvailabilitySnapshot, } from "./subagents/providers/availability.js";
@@ -258,6 +260,74 @@ const workspaceInventoryPageOutputSchema = z.object({
258
260
  limit: z.number().int().positive(),
259
261
  hasMore: z.boolean(),
260
262
  });
263
+ const workspaceTaskInspectionSummaryOutputSchema = z.object({
264
+ level: z.literal("summary"),
265
+ version: z.literal(1),
266
+ revision: z.number().int().nonnegative(),
267
+ lists: z.array(z.object({
268
+ id: z.string(),
269
+ name: z.string(),
270
+ state: z.enum(["active", "archived"]),
271
+ revision: z.number().int().positive(),
272
+ taskCount: z.number().int().nonnegative(),
273
+ unfinishedTaskCount: z.number().int().nonnegative(),
274
+ })),
275
+ });
276
+ const workspaceInspectionMemberOutputSchema = z.object({
277
+ name: z.string(),
278
+ purpose: z.string(),
279
+ workspaceId: z.string(),
280
+ known: z.boolean(),
281
+ location: z.enum(["local", "relay"]).optional(),
282
+ state: z.enum(["active", "stale", "invalid", "closed"]).optional(),
283
+ status: z.string().optional(),
284
+ routeState: z.literal("known").optional(),
285
+ mode: z.enum(["checkout", "worktree"]).optional(),
286
+ rootValid: z.boolean().optional(),
287
+ });
288
+ const workspaceInspectionOutputSchema = z.union([
289
+ z.object({
290
+ workspaceId: z.string(),
291
+ kind: z.literal("workspace"),
292
+ location: z.literal("local"),
293
+ label: z.string(),
294
+ root: z.string(),
295
+ status: z.string(),
296
+ state: z.enum(["active", "stale", "invalid", "closed"]),
297
+ mode: z.enum(["checkout", "worktree"]),
298
+ sourceRoot: z.string().optional(),
299
+ branch: z.string().optional(),
300
+ targetBranch: z.string().optional(),
301
+ managed: z.boolean(),
302
+ createdAt: z.string(),
303
+ lastUsedAt: z.string(),
304
+ idleMs: z.number().nonnegative(),
305
+ rootValid: z.boolean(),
306
+ taskSummary: workspaceTaskInspectionSummaryOutputSchema.optional(),
307
+ }),
308
+ z.object({
309
+ workspaceId: z.string(),
310
+ kind: z.literal("workspace"),
311
+ location: z.literal("relay"),
312
+ root: z.string(),
313
+ routeState: z.literal("known"),
314
+ mode: z.enum(["checkout", "worktree"]),
315
+ sourceRoot: z.string().optional(),
316
+ relay: z.string(),
317
+ executionLocation: z.string(),
318
+ }),
319
+ z.object({
320
+ workspaceId: z.string(),
321
+ kind: z.literal("composite"),
322
+ name: z.string(),
323
+ status: z.enum(["active", "closed"]),
324
+ state: z.enum(["active", "closed"]),
325
+ createdAt: z.string(),
326
+ lastUsedAt: z.string(),
327
+ members: z.array(workspaceInspectionMemberOutputSchema),
328
+ taskSummary: workspaceTaskInspectionSummaryOutputSchema.optional(),
329
+ }),
330
+ ]);
261
331
  const reviewFileOutputSchema = z.object({
262
332
  path: z.string(),
263
333
  previousPath: z.string().optional(),
@@ -344,6 +414,17 @@ function logFailedToolResponse(config, fields, content, startedAt) {
344
414
  function textBlock(text) {
345
415
  return { type: "text", text };
346
416
  }
417
+ function attachWorkspaceTaskReminder(result, reminder) {
418
+ if (!reminder || toolResultIsError(result) || typeof result !== "object" || result === null)
419
+ return result;
420
+ const content = result.content;
421
+ if (!Array.isArray(content))
422
+ return result;
423
+ return {
424
+ ...result,
425
+ content: [...content, textBlock(reminder)],
426
+ };
427
+ }
347
428
  function textSummary(content) {
348
429
  const text = contentText(content);
349
430
  return {
@@ -821,6 +902,7 @@ function workspaceHookInvocation(workspace) {
821
902
  function capabilityContextFor(workspace) {
822
903
  return {
823
904
  workspaceId: workspace.id,
905
+ workspaceKind: "workspace",
824
906
  workspaceRoot: workspace.root,
825
907
  guides: workspace.capabilityGuides.map((guide) => ({
826
908
  name: guide.name,
@@ -830,6 +912,66 @@ function capabilityContextFor(workspace) {
830
912
  })),
831
913
  };
832
914
  }
915
+ function compositeCapabilityContext(workspaceId, guides) {
916
+ return {
917
+ workspaceId,
918
+ workspaceKind: "composite",
919
+ guides: guides.map((guide) => ({
920
+ name: guide.name,
921
+ description: guide.description,
922
+ whenToRead: guide.whenToRead,
923
+ path: formatPathForPrompt(guide.filePath),
924
+ })),
925
+ };
926
+ }
927
+ function requireCapabilityWorkspaceRoot(context) {
928
+ if (!context.workspaceRoot) {
929
+ throw new CapabilityError("capability_unavailable", `Capability execution requires a filesystem-backed Workspace; ${context.workspaceId} is ${context.workspaceKind}.`);
930
+ }
931
+ return context.workspaceRoot;
932
+ }
933
+ function runWorkspaceTasksCapability(store, workspaceId, input) {
934
+ switch (input.operation) {
935
+ case "get":
936
+ if (input.level === "headers")
937
+ return store.readHeaders(workspaceId, input.listId);
938
+ if (input.level === "detail")
939
+ return store.readTaskDetail(workspaceId, input.listId, input.taskId);
940
+ return store.readSummary(workspaceId);
941
+ case "list.create":
942
+ store.createList(workspaceId, { name: input.name, position: input.position });
943
+ return store.readSummary(workspaceId);
944
+ case "list.update":
945
+ store.updateList(workspaceId, input.listId, {
946
+ name: input.name,
947
+ state: input.state,
948
+ position: input.position,
949
+ });
950
+ return store.readSummary(workspaceId);
951
+ case "list.delete":
952
+ store.deleteList(workspaceId, input.listId);
953
+ return store.readSummary(workspaceId);
954
+ case "task.create":
955
+ store.createTask(workspaceId, input.listId, {
956
+ subject: input.subject,
957
+ content: input.content,
958
+ status: input.status,
959
+ position: input.position,
960
+ });
961
+ return store.readHeaders(workspaceId, input.listId);
962
+ case "task.update":
963
+ store.updateTask(workspaceId, input.listId, input.taskId, {
964
+ subject: input.subject,
965
+ content: input.content,
966
+ status: input.status,
967
+ position: input.position,
968
+ });
969
+ return store.readHeaders(workspaceId, input.listId);
970
+ case "task.delete":
971
+ store.deleteTask(workspaceId, input.listId, input.taskId);
972
+ return store.readHeaders(workspaceId, input.listId);
973
+ }
974
+ }
833
975
  async function reviewWorkspaceChanges(reviewCheckpoints, workspace) {
834
976
  return reviewCheckpoints.reviewChanges({
835
977
  workspaceId: workspace.id,
@@ -1054,7 +1196,7 @@ function registerProcessTools(server, config, workspaces, processSessions, hooks
1054
1196
  const target = routing.resolve(workspaceId, member);
1055
1197
  const context = await routing.prepare(target, extra._meta, extra.signal, extra.sessionId);
1056
1198
  if (routing.isRemote(target.executionWorkspaceId)) {
1057
- return routing.present(await routing.execCommandRemote(target.executionWorkspaceId, {
1199
+ return routing.presentSemantic(await routing.execCommandRemote(target.executionWorkspaceId, {
1058
1200
  cmd,
1059
1201
  ...(tty !== undefined ? { tty } : {}),
1060
1202
  ...(columns !== undefined ? { columns } : {}),
@@ -1065,7 +1207,7 @@ function registerProcessTools(server, config, workspaces, processSessions, hooks
1065
1207
  ...(maxOutputTokens !== undefined ? { maxOutputTokens } : {}),
1066
1208
  }, routing.hostScopeIdFor(extra._meta, extra.sessionId)), target);
1067
1209
  }
1068
- return routing.present(await shellRun({
1210
+ return routing.presentSemantic(await shellRun({
1069
1211
  workspaceId: target.executionWorkspaceId,
1070
1212
  command: cmd,
1071
1213
  surface: "exec_command",
@@ -1192,6 +1334,9 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
1192
1334
  const connectionScopeId = `mcp-connection:${randomUUID()}`;
1193
1335
  const remoteWorkspaces = new RemoteWorkspaceRelay(config.configDir, config.stateDir);
1194
1336
  const compositeWorkspaces = new CompositeWorkspaceRegistry(config.stateDir);
1337
+ const workspaceTasks = new WorkspaceTaskStore(config.stateDir);
1338
+ const taskReminders = new WorkspaceTaskReminderTracker(config.taskReminderInterval, workspaceTasks);
1339
+ const compositeTaskGuides = loadCapabilityGuides(config).filter((guide) => guide.name === "workspace-tasks");
1195
1340
  const compositeActivity = new CompositeActivityCoordinator(compositeWorkspaces, activityQueries, remoteWorkspaces);
1196
1341
  const resolveExecutionTarget = (workspaceId, memberName) => {
1197
1342
  if (!compositeWorkspaces.has(workspaceId)) {
@@ -1224,6 +1369,25 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
1224
1369
  : remapCompositeToolResult(result, target.executionWorkspaceId, target.compositeWorkspaceId, target.memberName);
1225
1370
  return subagentMcp.decorateResult(target.executionWorkspaceId, presented);
1226
1371
  };
1372
+ const taskReminderWorkspaceIdFor = (target) => {
1373
+ if (target.compositeWorkspaceId)
1374
+ return target.compositeWorkspaceId;
1375
+ if (remoteWorkspaces.has(target.executionWorkspaceId))
1376
+ return undefined;
1377
+ try {
1378
+ return workspaces.getWorkspace(target.executionWorkspaceId).id;
1379
+ }
1380
+ catch {
1381
+ return undefined;
1382
+ }
1383
+ };
1384
+ const presentSemanticWorkResult = (result, target) => {
1385
+ const presented = presentExecutionResult(result, target);
1386
+ if (toolResultIsError(presented))
1387
+ return presented;
1388
+ const reminderWorkspaceId = taskReminderWorkspaceIdFor(target);
1389
+ return attachWorkspaceTaskReminder(presented, reminderWorkspaceId ? taskReminders.recordWork(reminderWorkspaceId) : undefined);
1390
+ };
1227
1391
  const hostScopeIdFor = (requestMeta, transportSessionId) => hostConversationScopeId(requestMeta, transportSessionId, connectionScopeId);
1228
1392
  const prepareExecutionContext = async (target, requestMeta, signal, sessionId) => {
1229
1393
  const conversationScopeId = hostScopeIdFor(requestMeta, sessionId);
@@ -1248,6 +1412,15 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
1248
1412
  const capabilityRegistry = createCapabilityRegistry({
1249
1413
  inspectHooks: (workspaceRoot) => checkHookConfiguration(workspaceRoot, config.hooks),
1250
1414
  ...subagentMcp.registryDependencies,
1415
+ workspaceTasks: {
1416
+ available: true,
1417
+ run: async (input, context) => {
1418
+ const value = runWorkspaceTasksCapability(workspaceTasks, context.workspaceId, input);
1419
+ if (input.operation !== "get")
1420
+ taskReminders.reset(context.workspaceId);
1421
+ return { value };
1422
+ },
1423
+ },
1251
1424
  batchExecute: {
1252
1425
  available: batchExecuteAvailable,
1253
1426
  unavailableReason: batchExecuteAvailable
@@ -1270,7 +1443,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
1270
1443
  run: async (input, context, options) => {
1271
1444
  try {
1272
1445
  return {
1273
- value: await codeIntelligence.run(context.workspaceRoot, input, { signal: options.signal }),
1446
+ value: await codeIntelligence.run(requireCapabilityWorkspaceRoot(context), input, { signal: options.signal }),
1274
1447
  };
1275
1448
  }
1276
1449
  catch (error) {
@@ -1289,7 +1462,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
1289
1462
  run: async (context) => {
1290
1463
  const review = await reviewWorkspaceChanges(reviewCheckpoints, {
1291
1464
  id: context.workspaceId,
1292
- root: context.workspaceRoot,
1465
+ root: requireCapabilityWorkspaceRoot(context),
1293
1466
  });
1294
1467
  return {
1295
1468
  value: {
@@ -1317,7 +1490,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
1317
1490
  const downloaded = await downloadIncomingArtifact({
1318
1491
  registry: incomingArtifactRegistry,
1319
1492
  workspaceId: context.workspaceId,
1320
- workspaceRoot: context.workspaceRoot,
1493
+ workspaceRoot: requireCapabilityWorkspaceRoot(context),
1321
1494
  maxFileBytes: config.artifactMaxFileBytes,
1322
1495
  file: input.file,
1323
1496
  path: input.path,
@@ -1979,9 +2152,9 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
1979
2152
  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.",
1980
2153
  inputSchema: {
1981
2154
  action: z
1982
- .enum(["open", "list", "member"])
2155
+ .enum(["open", "list", "inspect", "member"])
1983
2156
  .optional()
1984
- .describe("Defaults to open. Use list to inspect known Workspaces. Use member to add/remove a named execution member on an existing Composite Workspace."),
2157
+ .describe("Defaults to open. Use list for lightweight inventory, inspect for bounded read-only metadata about one known Workspace without opening/resuming it, or member to change Composite membership."),
1985
2158
  memberAction: z
1986
2159
  .enum(["add", "update", "remove"])
1987
2160
  .optional()
@@ -2021,7 +2194,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
2021
2194
  workspaceId: z
2022
2195
  .string()
2023
2196
  .optional()
2024
- .describe("For action=open, an existing Workspace ID to resume or reuse. Historical duplicate IDs from earlier ForgeRelay versions may resolve to the canonical Workspace ID. For action=list, filters inventory to one Workspace ID."),
2197
+ .describe("For action=open, an existing Workspace ID to resume or reuse. Historical duplicate IDs from earlier ForgeRelay versions may resolve to the canonical Workspace ID. For action=list, filters inventory. For action=inspect, identifies the single Workspace to inspect without opening or binding it."),
2025
2198
  mode: z
2026
2199
  .enum(["checkout", "worktree"])
2027
2200
  .optional()
@@ -2073,7 +2246,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
2073
2246
  .describe("For action=list, maximum records to return. Defaults to 50; maximum 100."),
2074
2247
  },
2075
2248
  outputSchema: {
2076
- action: z.enum(["open", "list", "member"]),
2249
+ action: z.enum(["open", "list", "inspect", "member"]),
2077
2250
  workspaceId: z.string().optional(),
2078
2251
  memberAction: z.enum(["add", "update", "remove"]).optional(),
2079
2252
  kind: z.enum(["workspace", "composite"]).optional(),
@@ -2148,6 +2321,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
2148
2321
  })).optional(),
2149
2322
  summary: workspaceInventorySummaryOutputSchema.optional(),
2150
2323
  page: workspaceInventoryPageOutputSchema.optional(),
2324
+ inspection: workspaceInspectionOutputSchema.optional(),
2151
2325
  instruction: z.string(),
2152
2326
  },
2153
2327
  _meta: {},
@@ -2161,6 +2335,121 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
2161
2335
  const startedAt = performance.now();
2162
2336
  const conversationScopeId = openAiConversationScopeId(_meta);
2163
2337
  const protectedWorkspaceIds = processSessions.activeWorkspaceIds();
2338
+ const inspectTaskSummary = (targetWorkspaceId) => {
2339
+ try {
2340
+ const summary = workspaceTasks.inspectSummary(targetWorkspaceId);
2341
+ if (!summary)
2342
+ return undefined;
2343
+ const { fingerprint: _fingerprint, ...inspectionSummary } = summary;
2344
+ return inspectionSummary;
2345
+ }
2346
+ catch {
2347
+ return undefined;
2348
+ }
2349
+ };
2350
+ const inspectCompositeMember = async (entry) => {
2351
+ if (remoteWorkspaces.has(entry.workspaceId)) {
2352
+ const inspected = remoteWorkspaces.inspectWorkspace(entry.workspaceId);
2353
+ return {
2354
+ name: entry.name,
2355
+ purpose: entry.purpose,
2356
+ workspaceId: entry.workspaceId,
2357
+ known: true,
2358
+ location: inspected.location,
2359
+ routeState: inspected.routeState,
2360
+ mode: inspected.mode,
2361
+ };
2362
+ }
2363
+ try {
2364
+ const inspected = await workspaces.inspectWorkspace(entry.workspaceId);
2365
+ return {
2366
+ name: entry.name,
2367
+ purpose: entry.purpose,
2368
+ workspaceId: entry.workspaceId,
2369
+ known: true,
2370
+ location: inspected.location,
2371
+ state: inspected.state,
2372
+ status: inspected.status,
2373
+ mode: inspected.mode,
2374
+ rootValid: inspected.rootValid,
2375
+ };
2376
+ }
2377
+ catch {
2378
+ return {
2379
+ name: entry.name,
2380
+ purpose: entry.purpose,
2381
+ workspaceId: entry.workspaceId,
2382
+ known: false,
2383
+ };
2384
+ }
2385
+ };
2386
+ if (action === "inspect") {
2387
+ if (!workspaceId) {
2388
+ throw new Error("open_workspace action=inspect requires workspaceId.");
2389
+ }
2390
+ if (memberAction !== undefined || member !== undefined || kind !== undefined || name !== undefined ||
2391
+ memberName !== undefined || path !== undefined || relay !== undefined || mode !== undefined ||
2392
+ baseRef !== undefined || newWorktree !== undefined || newWorkspace !== undefined || context !== undefined ||
2393
+ root !== undefined || status !== undefined || state !== undefined || staleOnly !== undefined ||
2394
+ offset !== undefined || limit !== undefined) {
2395
+ throw new Error("open_workspace action=inspect accepts only workspaceId. It never opens, resumes, binds, or mutates the inspected Workspace.");
2396
+ }
2397
+ let inspection;
2398
+ if (compositeWorkspaces.has(workspaceId)) {
2399
+ const composite = compositeWorkspaces.get(workspaceId);
2400
+ const members = await Promise.all(composite.members.map(inspectCompositeMember));
2401
+ const taskSummary = inspectTaskSummary(composite.id);
2402
+ inspection = {
2403
+ workspaceId: composite.id,
2404
+ kind: "composite",
2405
+ name: composite.name,
2406
+ status: composite.status,
2407
+ state: composite.status,
2408
+ createdAt: composite.createdAt,
2409
+ lastUsedAt: composite.lastUsedAt,
2410
+ members,
2411
+ ...(taskSummary ? { taskSummary } : {}),
2412
+ };
2413
+ }
2414
+ else if (remoteWorkspaces.has(workspaceId)) {
2415
+ inspection = remoteWorkspaces.inspectWorkspace(workspaceId);
2416
+ }
2417
+ else {
2418
+ const inspected = await workspaces.inspectWorkspace(workspaceId);
2419
+ const taskSummary = inspectTaskSummary(inspected.workspaceId);
2420
+ inspection = {
2421
+ ...inspected,
2422
+ ...(taskSummary ? { taskSummary } : {}),
2423
+ };
2424
+ }
2425
+ const instruction = "This is a bounded read-only Workspace inspection. It does not open/resume the target, deliver bootstrap context, bind this conversation, or grant file/process/Git/Capability authority. Explicitly open the Workspace before modifying or executing against it.";
2426
+ const result = [
2427
+ `Inspected Workspace ${inspection.workspaceId} (${inspection.kind}).`,
2428
+ inspection.kind === "composite"
2429
+ ? `State: ${inspection.state}; members=${inspection.members.length}.`
2430
+ : inspection.location === "relay"
2431
+ ? `Route: ${inspection.routeState}; mode=${inspection.mode}; location=${inspection.location}. Remote lifecycle is not probed by inspection.`
2432
+ : `State: ${inspection.state}; mode=${inspection.mode}; location=${inspection.location}.`,
2433
+ "Task summary is included only when durable local Task state already exists; Task bodies are never returned.",
2434
+ instruction,
2435
+ ].join("\n");
2436
+ logToolCall(config, {
2437
+ tool: "open_workspace",
2438
+ action: "inspect",
2439
+ success: true,
2440
+ durationMs: Math.round(performance.now() - startedAt),
2441
+ });
2442
+ return {
2443
+ content: [textBlock(result)],
2444
+ structuredContent: {
2445
+ action: "inspect",
2446
+ workspaceId: inspection.workspaceId,
2447
+ kind: inspection.kind,
2448
+ inspection,
2449
+ instruction,
2450
+ },
2451
+ };
2452
+ }
2164
2453
  if (action === "member") {
2165
2454
  if (!workspaceId || !compositeWorkspaces.has(workspaceId)) {
2166
2455
  throw new Error("open_workspace action=member requires an existing Composite Workspace workspaceId.");
@@ -2398,6 +2687,15 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
2398
2687
  const composite = workspaceId !== undefined
2399
2688
  ? compositeWorkspaces.open(workspaceId)
2400
2689
  : compositeWorkspaces.create(name ?? "");
2690
+ workspaceTasks.initializeWorkspace(composite.id);
2691
+ const compositeTaskContext = compositeCapabilityContext(composite.id, compositeTaskGuides);
2692
+ const compositeCapabilityCatalog = capabilityRegistry.catalog(compositeTaskContext);
2693
+ const compositeCapabilityGuides = compositeTaskGuides.map((guide) => ({
2694
+ name: guide.name,
2695
+ description: guide.description,
2696
+ whenToRead: guide.whenToRead,
2697
+ path: formatPathForPrompt(guide.filePath),
2698
+ }));
2401
2699
  const memberContext = memberName
2402
2700
  ? await loadCompositeMemberContext(composite.id, memberName, context ?? "auto", conversationScopeId, protectedWorkspaceIds)
2403
2701
  : undefined;
@@ -2408,6 +2706,9 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
2408
2706
  ? `Members: ${composite.members.map((member) => `${member.name} — ${member.purpose}`).join("; ")}.`
2409
2707
  : "This Composite Workspace currently has no members.",
2410
2708
  "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.",
2709
+ compositeCapabilityCatalog.length > 0
2710
+ ? `Composite-owned capabilities: ${compositeCapabilityCatalog.map((entry) => entry.name).join(", ")}. Use these without member because their state belongs to the Composite Workspace itself.`
2711
+ : undefined,
2411
2712
  composite.members.length > 0
2412
2713
  ? "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."
2413
2714
  : undefined,
@@ -2435,6 +2736,8 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
2435
2736
  status: composite.status,
2436
2737
  state: composite.status,
2437
2738
  members: composite.members,
2739
+ capabilityCatalog: compositeCapabilityCatalog,
2740
+ capabilityGuides: compositeCapabilityGuides,
2438
2741
  ...(memberContext ? { memberContext } : {}),
2439
2742
  instruction,
2440
2743
  },
@@ -2547,6 +2850,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
2547
2850
  conversationScopeId,
2548
2851
  protectedWorkspaceIds,
2549
2852
  });
2853
+ workspaceTasks.initializeWorkspace(workspace.id);
2550
2854
  const knownWorktrees = await workspaces.listKnownWorktrees(workspace);
2551
2855
  const staleWorkspaces = await workspaces.listStaleWorkspaces(workspace);
2552
2856
  const capabilityFingerprint = buildCapabilityFingerprint(config, FORGERELAY_VERSION, {
@@ -2598,7 +2902,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
2598
2902
  const loadedAgentsFiles = includeBootstrapContext ? cardAgentsFiles : [];
2599
2903
  const availableAgentsFileOutputs = includeBootstrapContext ? cardAvailableAgentsFiles : [];
2600
2904
  const workspaceContextInstruction = "For later open_workspace calls, context=\"auto\" avoids repeating unchanged bootstrap context; use context=\"none\" when only the workspace handle/metadata is needed, or context=\"full\" to force a refresh.";
2601
- const workspaceManagementInstruction = "When you need to inspect known Workspaces, continue earlier work, or organize Workspace state, use open_workspace(action=\"list\") to inspect candidates, then resume a selected workspaceId or ask the user before close_workspace cleanup.";
2905
+ const workspaceManagementInstruction = "Use open_workspace(action=\"list\") for lightweight Workspace inventory. Use action=\"inspect\" with one known workspaceId for bounded read-only metadata without opening/resuming it. Explicitly open a Workspace before executing or mutating against it, and ask the user before close_workspace cleanup.";
2602
2906
  const cardInstruction = config.skillsEnabled
2603
2907
  ? `Use this workspaceId in all subsequent tool calls for this project. Follow loaded agentsFiles instructions. Read an availableAgentsFiles path before working under it. When a task matches an available skill, load it with read(path=\"skills://<name>\") before proceeding. When a task matches a capability guide, read its advertised path before proceeding. ${workspaceContextInstruction} ${workspaceManagementInstruction}`
2604
2908
  : `Use this workspaceId in all subsequent tool calls for this project. Follow loaded agentsFiles instructions. Read an availableAgentsFiles path before working under it. When a task matches a capability guide, read its advertised path before proceeding. ${workspaceContextInstruction} ${workspaceManagementInstruction}`;
@@ -2808,16 +3112,87 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
2808
3112
  openWorldHint: true,
2809
3113
  },
2810
3114
  }, async ({ workspaceId, member, name, action, arguments: capabilityArguments, file }, extra) => {
3115
+ if (name === "workspace.tasks" && compositeWorkspaces.has(workspaceId)) {
3116
+ if (member !== undefined) {
3117
+ throw new Error(`workspace.tasks belongs to Composite Workspace ${workspaceId} itself and does not accept member.`);
3118
+ }
3119
+ if (!compositeWorkspaces.isActive(workspaceId)) {
3120
+ throw new Error(`Composite Workspace ${workspaceId} is closed. Reopen it with open_workspace before use.`);
3121
+ }
3122
+ const startedAt = performance.now();
3123
+ const context = compositeCapabilityContext(workspaceId, compositeTaskGuides);
3124
+ try {
3125
+ if (action === "run") {
3126
+ const execution = await capabilityRegistry.run(name, capabilityArguments ?? {}, context, {
3127
+ nativeFile: file,
3128
+ signal: extra.signal,
3129
+ requestMeta: extra._meta,
3130
+ sessionId: extra.sessionId,
3131
+ });
3132
+ const result = {
3133
+ content: [textBlock(`Capability ${name} completed.\n${JSON.stringify(execution.value, null, 2)}`)],
3134
+ structuredContent: { name, action, result: execution.value },
3135
+ };
3136
+ logToolCall(config, {
3137
+ tool: toolNames.capability,
3138
+ capability: name,
3139
+ action,
3140
+ success: true,
3141
+ durationMs: Math.round(performance.now() - startedAt),
3142
+ });
3143
+ return result;
3144
+ }
3145
+ const capability = capabilityRegistry.describe(name, context);
3146
+ const result = {
3147
+ content: [textBlock([
3148
+ `${capability.name}: ${capability.description}`,
3149
+ `Available: ${capability.available}`,
3150
+ `Guide: ${capability.guide.path}`,
3151
+ capability.guide.readBeforeFirstUse
3152
+ ? "Read the guide before first use when this contract is unfamiliar."
3153
+ : undefined,
3154
+ ].filter(Boolean).join("\n"))],
3155
+ structuredContent: { name, action, capability },
3156
+ };
3157
+ logToolCall(config, {
3158
+ tool: toolNames.capability,
3159
+ capability: name,
3160
+ action,
3161
+ success: true,
3162
+ durationMs: Math.round(performance.now() - startedAt),
3163
+ });
3164
+ return result;
3165
+ }
3166
+ catch (error) {
3167
+ if (extra.signal.aborted)
3168
+ throw error;
3169
+ const capabilityError = error instanceof CapabilityError
3170
+ ? error
3171
+ : new CapabilityError("execution_failed", error instanceof Error ? error.message : String(error));
3172
+ return {
3173
+ content: [textBlock(`${capabilityError.code}: ${capabilityError.message}`)],
3174
+ structuredContent: {
3175
+ name,
3176
+ action,
3177
+ error: { code: capabilityError.code, message: capabilityError.message },
3178
+ },
3179
+ isError: true,
3180
+ };
3181
+ }
3182
+ }
2811
3183
  const target = resolveExecutionTarget(workspaceId, member);
2812
3184
  const executionWorkspaceId = target.executionWorkspaceId;
2813
3185
  const executionContext = await prepareExecutionContext(target, extra._meta, extra.signal, extra.sessionId);
2814
3186
  if (remoteWorkspaces.has(executionWorkspaceId)) {
2815
- return presentExecutionResult(await remoteWorkspaces.capability(executionWorkspaceId, {
3187
+ const response = await remoteWorkspaces.capability(executionWorkspaceId, {
2816
3188
  name,
2817
3189
  action,
2818
3190
  ...(capabilityArguments !== undefined ? { arguments: capabilityArguments } : {}),
2819
3191
  ...(file !== undefined ? { file } : {}),
2820
- }, hostScopeIdFor(extra._meta, extra.sessionId)), target);
3192
+ }, hostScopeIdFor(extra._meta, extra.sessionId));
3193
+ return action === "run" && name !== "workspace.tasks"
3194
+ ? presentSemanticWorkResult(response, target)
3195
+ : presentExecutionResult(response, target);
2821
3196
  }
2822
3197
  if (action === "run" && name === "batch.execute") {
2823
3198
  const workspace = workspaces.getWorkspace(executionWorkspaceId);
@@ -2841,7 +3216,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
2841
3216
  success: true,
2842
3217
  durationMs: Math.round(performance.now() - startedAt),
2843
3218
  });
2844
- return presentExecutionResult(result, target);
3219
+ return presentSemanticWorkResult(result, target);
2845
3220
  }
2846
3221
  catch (error) {
2847
3222
  if (extra.signal.aborted)
@@ -2868,7 +3243,10 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
2868
3243
  }
2869
3244
  }
2870
3245
  if (action === "run") {
2871
- return presentExecutionResult(await coreOperations.capabilityRun({ workspaceId: executionWorkspaceId, name, arguments: capabilityArguments, file }, executionContext), target);
3246
+ const response = await coreOperations.capabilityRun({ workspaceId: executionWorkspaceId, name, arguments: capabilityArguments, file }, executionContext);
3247
+ return name === "workspace.tasks"
3248
+ ? presentExecutionResult(response, target)
3249
+ : presentSemanticWorkResult(response, target);
2872
3250
  }
2873
3251
  const workspace = workspaces.getWorkspace(executionWorkspaceId);
2874
3252
  return runActivityToolWithHooks(activityLifecycle, hooks, workspace, hostScopeIdFor(extra._meta, extra.sessionId), activityRequestFor({ workspaceId: executionWorkspaceId, name, action, arguments: capabilityArguments, file }, executionContext), {
@@ -2972,6 +3350,10 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
2972
3350
  const composite = action === "delete"
2973
3351
  ? compositeWorkspaces.dissolve(workspaceId)
2974
3352
  : compositeWorkspaces.close(workspaceId);
3353
+ if (action === "delete") {
3354
+ workspaceTasks.deleteWorkspace(workspaceId);
3355
+ taskReminders.forget(workspaceId);
3356
+ }
2975
3357
  compositeActivity.forgetComposite(workspaceId);
2976
3358
  workspacePanelStates.delete(workspaceId);
2977
3359
  const result = [
@@ -3038,6 +3420,8 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
3038
3420
  payload: { workspaceId: session.id, action: "delete", mode: session.mode },
3039
3421
  operation: async () => {
3040
3422
  workspaces.deleteWorkspace(session.id);
3423
+ workspaceTasks.deleteWorkspace(session.id);
3424
+ taskReminders.forget(session.id);
3041
3425
  await reviewCheckpoints.releaseWorkspace(session.id);
3042
3426
  const result = `Deleted ForgeRelay Workspace ${session.id}. Physical project files were not removed.`;
3043
3427
  return {
@@ -3080,6 +3464,8 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
3080
3464
  payload: { workspaceId: session.id, action: "delete", mode: session.mode },
3081
3465
  operation: async () => {
3082
3466
  workspaces.deleteWorkspace(session.id);
3467
+ workspaceTasks.deleteWorkspace(session.id);
3468
+ taskReminders.forget(session.id);
3083
3469
  await reviewCheckpoints.releaseWorkspace(session.id);
3084
3470
  const result = `Deleted closed managed-worktree Workspace ${session.id}. Its already-removed worktree backing was not recreated.`;
3085
3471
  return {
@@ -3142,6 +3528,8 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
3142
3528
  await Promise.all(physicalWorkspaceIds.map((id) => reviewCheckpoints.releaseWorkspace(id)));
3143
3529
  if (action === "delete") {
3144
3530
  workspaces.deleteWorkspace(workspace.id);
3531
+ workspaceTasks.deleteWorkspace(workspace.id);
3532
+ taskReminders.forget(workspace.id);
3145
3533
  }
3146
3534
  const result = [
3147
3535
  action === "delete"
@@ -3237,7 +3625,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
3237
3625
  member: z
3238
3626
  .string()
3239
3627
  .optional()
3240
- .describe("Required for a Composite Workspace; explicit member name that owns this operation."),
3628
+ .describe("Required for Composite member-scoped file reads. Omit only when reading an advertised Composite-owned capability guide."),
3241
3629
  path: z
3242
3630
  .string()
3243
3631
  .optional()
@@ -3279,14 +3667,37 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
3279
3667
  if ((path === undefined) === (paths === undefined)) {
3280
3668
  throw new Error("read requires exactly one of path or paths.");
3281
3669
  }
3670
+ if (compositeWorkspaces.has(workspaceId) && member === undefined && path !== undefined) {
3671
+ const guide = compositeTaskGuides.find((candidate) => formatPathForPrompt(candidate.filePath) === path || candidate.filePath === path);
3672
+ if (guide) {
3673
+ if (!compositeWorkspaces.isActive(workspaceId)) {
3674
+ throw new Error(`Composite Workspace ${workspaceId} is closed. Reopen it with open_workspace before use.`);
3675
+ }
3676
+ const startedAt = performance.now();
3677
+ const raw = readFileSync(guide.filePath, "utf8");
3678
+ const start = (offset ?? 1) - 1;
3679
+ const end = limit === undefined ? undefined : start + limit;
3680
+ const result = raw.split("\n").slice(start, end).join("\n");
3681
+ logToolCall(config, {
3682
+ tool: toolNames.read,
3683
+ path: formatPathForPrompt(guide.filePath),
3684
+ success: true,
3685
+ durationMs: Math.round(performance.now() - startedAt),
3686
+ });
3687
+ return {
3688
+ content: [textBlock(result)],
3689
+ structuredContent: { result },
3690
+ };
3691
+ }
3692
+ }
3282
3693
  const target = resolveExecutionTarget(workspaceId, member);
3283
3694
  const executionWorkspaceId = target.executionWorkspaceId;
3284
3695
  const executionContext = await prepareExecutionContext(target, extra._meta, extra.signal, extra.sessionId);
3285
3696
  if (remoteWorkspaces.has(executionWorkspaceId)) {
3286
- return presentExecutionResult(await remoteWorkspaces.read(executionWorkspaceId, { path, paths, offset, limit }, hostScopeIdFor(extra._meta, extra.sessionId)), target);
3697
+ return presentSemanticWorkResult(await remoteWorkspaces.read(executionWorkspaceId, { path, paths, offset, limit }, hostScopeIdFor(extra._meta, extra.sessionId)), target);
3287
3698
  }
3288
3699
  if (path !== undefined) {
3289
- return presentExecutionResult(await coreOperations.read({ workspaceId: executionWorkspaceId, path, offset, limit }, executionContext), target);
3700
+ return presentSemanticWorkResult(await coreOperations.read({ workspaceId: executionWorkspaceId, path, offset, limit }, executionContext), target);
3290
3701
  }
3291
3702
  const workspace = workspaces.getWorkspace(executionWorkspaceId);
3292
3703
  let response;
@@ -3339,7 +3750,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
3339
3750
  : { type: "succeeded" }, activityRelationFor(executionContext));
3340
3751
  if (!response)
3341
3752
  throw new Error("Bulk Read completed without a response.");
3342
- return presentExecutionResult(response, target);
3753
+ return presentSemanticWorkResult(response, target);
3343
3754
  });
3344
3755
  if (config.toolMode !== "codex") {
3345
3756
  registerAppTool(server, toolNames.write, {
@@ -3363,9 +3774,9 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
3363
3774
  const executionWorkspaceId = target.executionWorkspaceId;
3364
3775
  const executionContext = await prepareExecutionContext(target, extra._meta, extra.signal, extra.sessionId);
3365
3776
  if (remoteWorkspaces.has(executionWorkspaceId)) {
3366
- return presentExecutionResult(await remoteWorkspaces.write(executionWorkspaceId, input, hostScopeIdFor(extra._meta, extra.sessionId)), target);
3777
+ return presentSemanticWorkResult(await remoteWorkspaces.write(executionWorkspaceId, input, hostScopeIdFor(extra._meta, extra.sessionId)), target);
3367
3778
  }
3368
- return presentExecutionResult(await coreOperations.write({ workspaceId: executionWorkspaceId, ...input }, executionContext), target);
3779
+ return presentSemanticWorkResult(await coreOperations.write({ workspaceId: executionWorkspaceId, ...input }, executionContext), target);
3369
3780
  });
3370
3781
  registerAppTool(server, toolNames.edit, {
3371
3782
  title: "Edit file",
@@ -3416,12 +3827,12 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
3416
3827
  const executionWorkspaceId = target.executionWorkspaceId;
3417
3828
  const executionContext = await prepareExecutionContext(target, extra._meta, extra.signal, extra.sessionId);
3418
3829
  if (remoteWorkspaces.has(executionWorkspaceId)) {
3419
- return presentExecutionResult(await remoteWorkspaces.edit(executionWorkspaceId, { path, paths, edits }, hostScopeIdFor(extra._meta, extra.sessionId)), target);
3830
+ return presentSemanticWorkResult(await remoteWorkspaces.edit(executionWorkspaceId, { path, paths, edits }, hostScopeIdFor(extra._meta, extra.sessionId)), target);
3420
3831
  }
3421
3832
  if (path !== undefined) {
3422
- return presentExecutionResult(await coreOperations.edit({ workspaceId: executionWorkspaceId, path, edits }, executionContext), target);
3833
+ return presentSemanticWorkResult(await coreOperations.edit({ workspaceId: executionWorkspaceId, path, edits }, executionContext), target);
3423
3834
  }
3424
- return presentExecutionResult(await nativeBulkMutations.edit({ workspaceId: executionWorkspaceId, paths: paths, edits }, executionContext), target);
3835
+ return presentSemanticWorkResult(await nativeBulkMutations.edit({ workspaceId: executionWorkspaceId, paths: paths, edits }, executionContext), target);
3425
3836
  });
3426
3837
  }
3427
3838
  registerAppTool(server, toolNames.rename, {
@@ -3445,9 +3856,9 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
3445
3856
  const executionWorkspaceId = target.executionWorkspaceId;
3446
3857
  const executionContext = await prepareExecutionContext(target, extra._meta, extra.signal, extra.sessionId);
3447
3858
  if (remoteWorkspaces.has(executionWorkspaceId)) {
3448
- return presentExecutionResult(await remoteWorkspaces.rename(executionWorkspaceId, { path, newPath }, hostScopeIdFor(extra._meta, extra.sessionId)), target);
3859
+ return presentSemanticWorkResult(await remoteWorkspaces.rename(executionWorkspaceId, { path, newPath }, hostScopeIdFor(extra._meta, extra.sessionId)), target);
3449
3860
  }
3450
- return presentExecutionResult(await coreOperations.rename({ workspaceId: executionWorkspaceId, path, newPath }, executionContext), target);
3861
+ return presentSemanticWorkResult(await coreOperations.rename({ workspaceId: executionWorkspaceId, path, newPath }, executionContext), target);
3451
3862
  });
3452
3863
  registerAppTool(server, toolNames.delete, {
3453
3864
  title: "Delete path",
@@ -3488,12 +3899,12 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
3488
3899
  const executionWorkspaceId = target.executionWorkspaceId;
3489
3900
  const executionContext = await prepareExecutionContext(target, extra._meta, extra.signal, extra.sessionId);
3490
3901
  if (remoteWorkspaces.has(executionWorkspaceId)) {
3491
- return presentExecutionResult(await remoteWorkspaces.delete(executionWorkspaceId, { path, paths, recursive }, hostScopeIdFor(extra._meta, extra.sessionId)), target);
3902
+ return presentSemanticWorkResult(await remoteWorkspaces.delete(executionWorkspaceId, { path, paths, recursive }, hostScopeIdFor(extra._meta, extra.sessionId)), target);
3492
3903
  }
3493
3904
  if (path !== undefined) {
3494
- return presentExecutionResult(await coreOperations.delete({ workspaceId: executionWorkspaceId, path, recursive }, executionContext), target);
3905
+ return presentSemanticWorkResult(await coreOperations.delete({ workspaceId: executionWorkspaceId, path, recursive }, executionContext), target);
3495
3906
  }
3496
- return presentExecutionResult(await nativeBulkMutations.delete({ workspaceId: executionWorkspaceId, paths: paths, recursive }, executionContext), target);
3907
+ return presentSemanticWorkResult(await nativeBulkMutations.delete({ workspaceId: executionWorkspaceId, paths: paths, recursive }, executionContext), target);
3497
3908
  });
3498
3909
  if (config.toolMode === "codex") {
3499
3910
  registerAppTool(server, "apply_patch", {
@@ -3524,7 +3935,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
3524
3935
  const executionWorkspaceId = target.executionWorkspaceId;
3525
3936
  const executionContext = await prepareExecutionContext(target, extra._meta, extra.signal, extra.sessionId);
3526
3937
  if (remoteWorkspaces.has(executionWorkspaceId)) {
3527
- return presentExecutionResult(await remoteWorkspaces.applyPatch(executionWorkspaceId, { patch }, hostScopeIdFor(extra._meta, extra.sessionId)), target);
3938
+ return presentSemanticWorkResult(await remoteWorkspaces.applyPatch(executionWorkspaceId, { patch }, hostScopeIdFor(extra._meta, extra.sessionId)), target);
3528
3939
  }
3529
3940
  const workspace = workspaces.getWorkspace(executionWorkspaceId);
3530
3941
  return runActivityToolWithHooks(activityLifecycle, hooks, workspace, hostScopeIdFor(extra._meta, extra.sessionId), activityRequestFor({ workspaceId: executionWorkspaceId, patch }, executionContext), {
@@ -3574,7 +3985,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
3574
3985
  },
3575
3986
  };
3576
3987
  },
3577
- }, activityRelationFor(executionContext)).then((result) => presentExecutionResult(result, target));
3988
+ }, activityRelationFor(executionContext)).then((result) => presentSemanticWorkResult(result, target));
3578
3989
  });
3579
3990
  }
3580
3991
  if (config.toolMode !== "codex") {
@@ -3664,7 +4075,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
3664
4075
  const executionWorkspaceId = target.executionWorkspaceId;
3665
4076
  const executionContext = await prepareExecutionContext(target, extra._meta, extra.signal, extra.sessionId);
3666
4077
  if (remoteWorkspaces.has(executionWorkspaceId)) {
3667
- return presentExecutionResult(await remoteWorkspaces.bash(executionWorkspaceId, {
4078
+ const response = await remoteWorkspaces.bash(executionWorkspaceId, {
3668
4079
  action,
3669
4080
  ...(command !== undefined ? { command } : {}),
3670
4081
  ...(processId !== undefined ? { processId } : {}),
@@ -3678,7 +4089,10 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
3678
4089
  ...(yieldTimeMs !== undefined ? { yieldTimeMs } : {}),
3679
4090
  ...(timeoutMs !== undefined ? { timeoutMs } : {}),
3680
4091
  ...(maxOutputTokens !== undefined ? { maxOutputTokens } : {}),
3681
- }, hostScopeIdFor(extra._meta, extra.sessionId)), target);
4092
+ }, hostScopeIdFor(extra._meta, extra.sessionId));
4093
+ return action === "run"
4094
+ ? presentSemanticWorkResult(response, target)
4095
+ : presentExecutionResult(response, target);
3682
4096
  }
3683
4097
  const workspace = workspaces.getWorkspace(executionWorkspaceId);
3684
4098
  if (action === "run") {
@@ -3687,7 +4101,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
3687
4101
  if (processId !== undefined || outputId !== undefined || input !== undefined || interrupt !== undefined) {
3688
4102
  throw new Error("bash action=run does not accept processId, outputId, input, or interrupt.");
3689
4103
  }
3690
- return presentExecutionResult(await coreOperations.shellRun({
4104
+ return presentSemanticWorkResult(await coreOperations.shellRun({
3691
4105
  workspaceId: executionWorkspaceId,
3692
4106
  command,
3693
4107
  surface: "bash",
@@ -3781,6 +4195,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
3781
4195
  resolve: resolveExecutionTarget,
3782
4196
  prepare: prepareExecutionContext,
3783
4197
  present: presentExecutionResult,
4198
+ presentSemantic: presentSemanticWorkResult,
3784
4199
  isRemote: (workspaceId) => remoteWorkspaces.has(workspaceId),
3785
4200
  execCommandRemote: (workspaceId, input, conversationScopeId) => remoteWorkspaces.execCommand(workspaceId, input, conversationScopeId),
3786
4201
  writeStdinRemote: (workspaceId, input, conversationScopeId) => remoteWorkspaces.writeStdin(workspaceId, input, conversationScopeId),