@akira-tl/forgerelay 0.8.3 → 0.8.5

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
@@ -53,6 +53,7 @@ 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";
56
57
  import { WorkspaceTaskStore } from "./workspace-tasks.js";
57
58
  import { formatAgentsPath, WorkspaceRegistry } from "./workspaces.js";
58
59
  import { formatAvailableSubagentProfile, summarizeSubagentProfile } from "./subagents/profiles.js";
@@ -259,6 +260,84 @@ const workspaceInventoryPageOutputSchema = z.object({
259
260
  limit: z.number().int().positive(),
260
261
  hasMore: z.boolean(),
261
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
+ status: z.string().optional(),
315
+ state: z.enum(["active", "stale", "invalid", "closed"]).optional(),
316
+ mode: z.enum(["checkout", "worktree"]),
317
+ sourceRoot: z.string().optional(),
318
+ branch: z.string().optional(),
319
+ targetBranch: z.string().optional(),
320
+ managed: z.boolean().optional(),
321
+ createdAt: z.string().optional(),
322
+ lastUsedAt: z.string().optional(),
323
+ idleMs: z.number().nonnegative().optional(),
324
+ rootValid: z.boolean().optional(),
325
+ taskSummary: workspaceTaskInspectionSummaryOutputSchema.optional(),
326
+ relay: z.string(),
327
+ executionLocation: z.string(),
328
+ }),
329
+ z.object({
330
+ workspaceId: z.string(),
331
+ kind: z.literal("composite"),
332
+ name: z.string(),
333
+ status: z.enum(["active", "closed"]),
334
+ state: z.enum(["active", "closed"]),
335
+ createdAt: z.string(),
336
+ lastUsedAt: z.string(),
337
+ members: z.array(workspaceInspectionMemberOutputSchema),
338
+ taskSummary: workspaceTaskInspectionSummaryOutputSchema.optional(),
339
+ }),
340
+ ]);
262
341
  const reviewFileOutputSchema = z.object({
263
342
  path: z.string(),
264
343
  previousPath: z.string().optional(),
@@ -345,6 +424,17 @@ function logFailedToolResponse(config, fields, content, startedAt) {
345
424
  function textBlock(text) {
346
425
  return { type: "text", text };
347
426
  }
427
+ function attachWorkspaceTaskReminder(result, reminder) {
428
+ if (!reminder || toolResultIsError(result) || typeof result !== "object" || result === null)
429
+ return result;
430
+ const content = result.content;
431
+ if (!Array.isArray(content))
432
+ return result;
433
+ return {
434
+ ...result,
435
+ content: [...content, textBlock(reminder)],
436
+ };
437
+ }
348
438
  function textSummary(content) {
349
439
  const text = contentText(content);
350
440
  return {
@@ -853,33 +943,43 @@ function requireCapabilityWorkspaceRoot(context) {
853
943
  function runWorkspaceTasksCapability(store, workspaceId, input) {
854
944
  switch (input.operation) {
855
945
  case "get":
856
- return store.read(workspaceId);
946
+ if (input.level === "headers")
947
+ return store.readHeaders(workspaceId, input.listId);
948
+ if (input.level === "detail")
949
+ return store.readTaskDetail(workspaceId, input.listId, input.taskId);
950
+ return store.readSummary(workspaceId);
857
951
  case "list.create":
858
- return store.createList(workspaceId, { name: input.name, position: input.position });
952
+ store.createList(workspaceId, { name: input.name, position: input.position });
953
+ return store.readSummary(workspaceId);
859
954
  case "list.update":
860
- return store.updateList(workspaceId, input.listId, {
955
+ store.updateList(workspaceId, input.listId, {
861
956
  name: input.name,
862
957
  state: input.state,
863
958
  position: input.position,
864
959
  });
960
+ return store.readSummary(workspaceId);
865
961
  case "list.delete":
866
- return store.deleteList(workspaceId, input.listId);
962
+ store.deleteList(workspaceId, input.listId);
963
+ return store.readSummary(workspaceId);
867
964
  case "task.create":
868
- return store.createTask(workspaceId, input.listId, {
965
+ store.createTask(workspaceId, input.listId, {
869
966
  subject: input.subject,
870
967
  content: input.content,
871
968
  status: input.status,
872
969
  position: input.position,
873
970
  });
971
+ return store.readHeaders(workspaceId, input.listId);
874
972
  case "task.update":
875
- return store.updateTask(workspaceId, input.listId, input.taskId, {
973
+ store.updateTask(workspaceId, input.listId, input.taskId, {
876
974
  subject: input.subject,
877
975
  content: input.content,
878
976
  status: input.status,
879
977
  position: input.position,
880
978
  });
979
+ return store.readHeaders(workspaceId, input.listId);
881
980
  case "task.delete":
882
- return store.deleteTask(workspaceId, input.listId, input.taskId);
981
+ store.deleteTask(workspaceId, input.listId, input.taskId);
982
+ return store.readHeaders(workspaceId, input.listId);
883
983
  }
884
984
  }
885
985
  async function reviewWorkspaceChanges(reviewCheckpoints, workspace) {
@@ -1106,7 +1206,7 @@ function registerProcessTools(server, config, workspaces, processSessions, hooks
1106
1206
  const target = routing.resolve(workspaceId, member);
1107
1207
  const context = await routing.prepare(target, extra._meta, extra.signal, extra.sessionId);
1108
1208
  if (routing.isRemote(target.executionWorkspaceId)) {
1109
- return routing.present(await routing.execCommandRemote(target.executionWorkspaceId, {
1209
+ return routing.presentSemantic(await routing.execCommandRemote(target.executionWorkspaceId, {
1110
1210
  cmd,
1111
1211
  ...(tty !== undefined ? { tty } : {}),
1112
1212
  ...(columns !== undefined ? { columns } : {}),
@@ -1117,7 +1217,7 @@ function registerProcessTools(server, config, workspaces, processSessions, hooks
1117
1217
  ...(maxOutputTokens !== undefined ? { maxOutputTokens } : {}),
1118
1218
  }, routing.hostScopeIdFor(extra._meta, extra.sessionId)), target);
1119
1219
  }
1120
- return routing.present(await shellRun({
1220
+ return routing.presentSemantic(await shellRun({
1121
1221
  workspaceId: target.executionWorkspaceId,
1122
1222
  command: cmd,
1123
1223
  surface: "exec_command",
@@ -1242,9 +1342,13 @@ function registerProcessTools(server, config, workspaces, processSessions, hooks
1242
1342
  }
1243
1343
  export function createMcpServer(config, workspaces, reviewCheckpoints, processSessions, subagentProviders, incomingArtifactAdapters, codeIntelligence, activityLifecycle, bashOutputStore, activityQueries, options = {}) {
1244
1344
  const connectionScopeId = `mcp-connection:${randomUUID()}`;
1245
- const remoteWorkspaces = new RemoteWorkspaceRelay(config.configDir, config.stateDir);
1246
- const compositeWorkspaces = new CompositeWorkspaceRegistry(config.stateDir);
1345
+ const remoteWorkspaces = options.remoteWorkspaces
1346
+ ?? new RemoteWorkspaceRelay(config.configDir, config.stateDir);
1347
+ const compositeWorkspaces = options.compositeWorkspaces
1348
+ ?? new CompositeWorkspaceRegistry(config.stateDir);
1247
1349
  const workspaceTasks = new WorkspaceTaskStore(config.stateDir);
1350
+ const taskReminders = options.taskReminders
1351
+ ?? new WorkspaceTaskReminderTracker(config.taskReminderInterval, workspaceTasks);
1248
1352
  const compositeTaskGuides = loadCapabilityGuides(config).filter((guide) => guide.name === "workspace-tasks");
1249
1353
  const compositeActivity = new CompositeActivityCoordinator(compositeWorkspaces, activityQueries, remoteWorkspaces);
1250
1354
  const resolveExecutionTarget = (workspaceId, memberName) => {
@@ -1278,6 +1382,25 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
1278
1382
  : remapCompositeToolResult(result, target.executionWorkspaceId, target.compositeWorkspaceId, target.memberName);
1279
1383
  return subagentMcp.decorateResult(target.executionWorkspaceId, presented);
1280
1384
  };
1385
+ const taskReminderWorkspaceIdFor = (target) => {
1386
+ if (target.compositeWorkspaceId)
1387
+ return target.compositeWorkspaceId;
1388
+ if (remoteWorkspaces.has(target.executionWorkspaceId))
1389
+ return undefined;
1390
+ try {
1391
+ return workspaces.getWorkspace(target.executionWorkspaceId).id;
1392
+ }
1393
+ catch {
1394
+ return undefined;
1395
+ }
1396
+ };
1397
+ const presentSemanticWorkResult = (result, target) => {
1398
+ const presented = presentExecutionResult(result, target);
1399
+ if (toolResultIsError(presented))
1400
+ return presented;
1401
+ const reminderWorkspaceId = taskReminderWorkspaceIdFor(target);
1402
+ return attachWorkspaceTaskReminder(presented, reminderWorkspaceId ? taskReminders.recordWork(reminderWorkspaceId) : undefined);
1403
+ };
1281
1404
  const hostScopeIdFor = (requestMeta, transportSessionId) => hostConversationScopeId(requestMeta, transportSessionId, connectionScopeId);
1282
1405
  const prepareExecutionContext = async (target, requestMeta, signal, sessionId) => {
1283
1406
  const conversationScopeId = hostScopeIdFor(requestMeta, sessionId);
@@ -1304,9 +1427,12 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
1304
1427
  ...subagentMcp.registryDependencies,
1305
1428
  workspaceTasks: {
1306
1429
  available: true,
1307
- run: async (input, context) => ({
1308
- value: runWorkspaceTasksCapability(workspaceTasks, context.workspaceId, input),
1309
- }),
1430
+ run: async (input, context) => {
1431
+ const value = runWorkspaceTasksCapability(workspaceTasks, context.workspaceId, input);
1432
+ if (input.operation !== "get")
1433
+ taskReminders.reset(context.workspaceId);
1434
+ return { value };
1435
+ },
1310
1436
  },
1311
1437
  batchExecute: {
1312
1438
  available: batchExecuteAvailable,
@@ -2039,9 +2165,9 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
2039
2165
  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.",
2040
2166
  inputSchema: {
2041
2167
  action: z
2042
- .enum(["open", "list", "member"])
2168
+ .enum(["open", "list", "inspect", "member"])
2043
2169
  .optional()
2044
- .describe("Defaults to open. Use list to inspect known Workspaces. Use member to add/remove a named execution member on an existing Composite Workspace."),
2170
+ .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."),
2045
2171
  memberAction: z
2046
2172
  .enum(["add", "update", "remove"])
2047
2173
  .optional()
@@ -2081,7 +2207,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
2081
2207
  workspaceId: z
2082
2208
  .string()
2083
2209
  .optional()
2084
- .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."),
2210
+ .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."),
2085
2211
  mode: z
2086
2212
  .enum(["checkout", "worktree"])
2087
2213
  .optional()
@@ -2133,7 +2259,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
2133
2259
  .describe("For action=list, maximum records to return. Defaults to 50; maximum 100."),
2134
2260
  },
2135
2261
  outputSchema: {
2136
- action: z.enum(["open", "list", "member"]),
2262
+ action: z.enum(["open", "list", "inspect", "member"]),
2137
2263
  workspaceId: z.string().optional(),
2138
2264
  memberAction: z.enum(["add", "update", "remove"]).optional(),
2139
2265
  kind: z.enum(["workspace", "composite"]).optional(),
@@ -2208,6 +2334,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
2208
2334
  })).optional(),
2209
2335
  summary: workspaceInventorySummaryOutputSchema.optional(),
2210
2336
  page: workspaceInventoryPageOutputSchema.optional(),
2337
+ inspection: workspaceInspectionOutputSchema.optional(),
2211
2338
  instruction: z.string(),
2212
2339
  },
2213
2340
  _meta: {},
@@ -2221,6 +2348,136 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
2221
2348
  const startedAt = performance.now();
2222
2349
  const conversationScopeId = openAiConversationScopeId(_meta);
2223
2350
  const protectedWorkspaceIds = processSessions.activeWorkspaceIds();
2351
+ const inspectTaskSummary = (targetWorkspaceId) => {
2352
+ try {
2353
+ const summary = workspaceTasks.inspectSummary(targetWorkspaceId);
2354
+ if (!summary)
2355
+ return undefined;
2356
+ const { fingerprint: _fingerprint, ...inspectionSummary } = summary;
2357
+ return inspectionSummary;
2358
+ }
2359
+ catch {
2360
+ return undefined;
2361
+ }
2362
+ };
2363
+ const inspectCompositeMember = async (entry) => {
2364
+ if (remoteWorkspaces.has(entry.workspaceId)) {
2365
+ try {
2366
+ const inspected = await remoteWorkspaces.inspectWorkspace(entry.workspaceId);
2367
+ return {
2368
+ name: entry.name,
2369
+ purpose: entry.purpose,
2370
+ workspaceId: entry.workspaceId,
2371
+ known: true,
2372
+ location: inspected.location,
2373
+ routeState: inspected.routeState,
2374
+ state: inspected.state,
2375
+ status: inspected.status,
2376
+ mode: inspected.mode,
2377
+ rootValid: inspected.rootValid,
2378
+ };
2379
+ }
2380
+ catch {
2381
+ return {
2382
+ name: entry.name,
2383
+ purpose: entry.purpose,
2384
+ workspaceId: entry.workspaceId,
2385
+ known: false,
2386
+ };
2387
+ }
2388
+ }
2389
+ try {
2390
+ const inspected = await workspaces.inspectWorkspace(entry.workspaceId);
2391
+ return {
2392
+ name: entry.name,
2393
+ purpose: entry.purpose,
2394
+ workspaceId: entry.workspaceId,
2395
+ known: true,
2396
+ location: inspected.location,
2397
+ state: inspected.state,
2398
+ status: inspected.status,
2399
+ mode: inspected.mode,
2400
+ rootValid: inspected.rootValid,
2401
+ };
2402
+ }
2403
+ catch {
2404
+ return {
2405
+ name: entry.name,
2406
+ purpose: entry.purpose,
2407
+ workspaceId: entry.workspaceId,
2408
+ known: false,
2409
+ };
2410
+ }
2411
+ };
2412
+ if (action === "inspect") {
2413
+ if (!workspaceId) {
2414
+ throw new Error("open_workspace action=inspect requires workspaceId.");
2415
+ }
2416
+ if (memberAction !== undefined || member !== undefined || kind !== undefined || name !== undefined ||
2417
+ memberName !== undefined || path !== undefined || relay !== undefined || mode !== undefined ||
2418
+ baseRef !== undefined || newWorktree !== undefined || newWorkspace !== undefined || context !== undefined ||
2419
+ root !== undefined || status !== undefined || state !== undefined || staleOnly !== undefined ||
2420
+ offset !== undefined || limit !== undefined) {
2421
+ throw new Error("open_workspace action=inspect accepts only workspaceId. It never opens, resumes, binds, or mutates the inspected Workspace.");
2422
+ }
2423
+ let inspection;
2424
+ if (compositeWorkspaces.has(workspaceId)) {
2425
+ const composite = compositeWorkspaces.get(workspaceId);
2426
+ const members = await Promise.all(composite.members.map(inspectCompositeMember));
2427
+ const taskSummary = inspectTaskSummary(composite.id);
2428
+ inspection = {
2429
+ workspaceId: composite.id,
2430
+ kind: "composite",
2431
+ name: composite.name,
2432
+ status: composite.status,
2433
+ state: composite.status,
2434
+ createdAt: composite.createdAt,
2435
+ lastUsedAt: composite.lastUsedAt,
2436
+ members,
2437
+ ...(taskSummary ? { taskSummary } : {}),
2438
+ };
2439
+ }
2440
+ else if (remoteWorkspaces.has(workspaceId)) {
2441
+ inspection = await remoteWorkspaces.inspectWorkspace(workspaceId);
2442
+ }
2443
+ else {
2444
+ const inspected = await workspaces.inspectWorkspace(workspaceId);
2445
+ const taskSummary = inspectTaskSummary(inspected.workspaceId);
2446
+ inspection = {
2447
+ ...inspected,
2448
+ ...(taskSummary ? { taskSummary } : {}),
2449
+ };
2450
+ }
2451
+ 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.";
2452
+ const result = [
2453
+ `Inspected Workspace ${inspection.workspaceId} (${inspection.kind}).`,
2454
+ inspection.kind === "composite"
2455
+ ? `State: ${inspection.state}; members=${inspection.members.length}.`
2456
+ : inspection.location === "relay"
2457
+ ? inspection.state
2458
+ ? `State: ${inspection.state}; route=${inspection.routeState}; mode=${inspection.mode}; location=${inspection.location}. Lifecycle and Task facts come from the Execution ForgeRelay.`
2459
+ : `Route: ${inspection.routeState}; mode=${inspection.mode}; location=${inspection.location}.`
2460
+ : `State: ${inspection.state}; mode=${inspection.mode}; location=${inspection.location}.`,
2461
+ "Task summary is included only when durable Task state already exists on the owning Workspace; Task bodies are never returned.",
2462
+ instruction,
2463
+ ].join("\n");
2464
+ logToolCall(config, {
2465
+ tool: "open_workspace",
2466
+ action: "inspect",
2467
+ success: true,
2468
+ durationMs: Math.round(performance.now() - startedAt),
2469
+ });
2470
+ return {
2471
+ content: [textBlock(result)],
2472
+ structuredContent: {
2473
+ action: "inspect",
2474
+ workspaceId: inspection.workspaceId,
2475
+ kind: inspection.kind,
2476
+ inspection,
2477
+ instruction,
2478
+ },
2479
+ };
2480
+ }
2224
2481
  if (action === "member") {
2225
2482
  if (!workspaceId || !compositeWorkspaces.has(workspaceId)) {
2226
2483
  throw new Error("open_workspace action=member requires an existing Composite Workspace workspaceId.");
@@ -2525,10 +2782,16 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
2525
2782
  if (name !== undefined || memberName !== undefined) {
2526
2783
  throw new Error("open_workspace name/memberName are only valid for a Composite Workspace.");
2527
2784
  }
2528
- if (relay !== undefined) {
2529
- if (workspaceId !== undefined) {
2530
- throw new Error("Relayed open_workspace requires a path; resuming a relayed workspace is not available in this tracer bullet.");
2785
+ if (workspaceId !== undefined && remoteWorkspaces.has(workspaceId)) {
2786
+ if (path !== undefined || relay !== undefined || mode !== undefined || baseRef !== undefined ||
2787
+ newWorktree !== undefined || newWorkspace !== undefined) {
2788
+ throw new Error("Resuming a relayed Workspace by workspaceId accepts context only.");
2531
2789
  }
2790
+ const resumed = await remoteWorkspaces.resumeWorkspace(workspaceId, context ?? "auto", hostScopeIdFor(_meta, sessionId));
2791
+ rememberWorkspacePanelState(workspaceId, resumed);
2792
+ return resumed;
2793
+ }
2794
+ if (relay !== undefined) {
2532
2795
  if (!path)
2533
2796
  throw new Error("Relayed open_workspace requires path.");
2534
2797
  const opened = await remoteWorkspaces.openWorkspace(relay, {
@@ -2673,7 +2936,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
2673
2936
  const loadedAgentsFiles = includeBootstrapContext ? cardAgentsFiles : [];
2674
2937
  const availableAgentsFileOutputs = includeBootstrapContext ? cardAvailableAgentsFiles : [];
2675
2938
  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.";
2676
- 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.";
2939
+ 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.";
2677
2940
  const cardInstruction = config.skillsEnabled
2678
2941
  ? `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}`
2679
2942
  : `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}`;
@@ -2955,12 +3218,15 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
2955
3218
  const executionWorkspaceId = target.executionWorkspaceId;
2956
3219
  const executionContext = await prepareExecutionContext(target, extra._meta, extra.signal, extra.sessionId);
2957
3220
  if (remoteWorkspaces.has(executionWorkspaceId)) {
2958
- return presentExecutionResult(await remoteWorkspaces.capability(executionWorkspaceId, {
3221
+ const response = await remoteWorkspaces.capability(executionWorkspaceId, {
2959
3222
  name,
2960
3223
  action,
2961
3224
  ...(capabilityArguments !== undefined ? { arguments: capabilityArguments } : {}),
2962
3225
  ...(file !== undefined ? { file } : {}),
2963
- }, hostScopeIdFor(extra._meta, extra.sessionId)), target);
3226
+ }, hostScopeIdFor(extra._meta, extra.sessionId));
3227
+ return action === "run" && name !== "workspace.tasks"
3228
+ ? presentSemanticWorkResult(response, target)
3229
+ : presentExecutionResult(response, target);
2964
3230
  }
2965
3231
  if (action === "run" && name === "batch.execute") {
2966
3232
  const workspace = workspaces.getWorkspace(executionWorkspaceId);
@@ -2984,7 +3250,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
2984
3250
  success: true,
2985
3251
  durationMs: Math.round(performance.now() - startedAt),
2986
3252
  });
2987
- return presentExecutionResult(result, target);
3253
+ return presentSemanticWorkResult(result, target);
2988
3254
  }
2989
3255
  catch (error) {
2990
3256
  if (extra.signal.aborted)
@@ -3011,7 +3277,10 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
3011
3277
  }
3012
3278
  }
3013
3279
  if (action === "run") {
3014
- return presentExecutionResult(await coreOperations.capabilityRun({ workspaceId: executionWorkspaceId, name, arguments: capabilityArguments, file }, executionContext), target);
3280
+ const response = await coreOperations.capabilityRun({ workspaceId: executionWorkspaceId, name, arguments: capabilityArguments, file }, executionContext);
3281
+ return name === "workspace.tasks"
3282
+ ? presentExecutionResult(response, target)
3283
+ : presentSemanticWorkResult(response, target);
3015
3284
  }
3016
3285
  const workspace = workspaces.getWorkspace(executionWorkspaceId);
3017
3286
  return runActivityToolWithHooks(activityLifecycle, hooks, workspace, hostScopeIdFor(extra._meta, extra.sessionId), activityRequestFor({ workspaceId: executionWorkspaceId, name, action, arguments: capabilityArguments, file }, executionContext), {
@@ -3071,7 +3340,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
3071
3340
  });
3072
3341
  registerAppTool(server, toolNames.closeWorkspace, {
3073
3342
  title: "Close workspace",
3074
- description: "Close or explicitly delete one Workspace after the user chooses cleanup. action=close (default) preserves checkout, managed-worktree, and Composite identity for later reopen. action=delete permanently removes ForgeRelay-owned state. Managed-worktree-backed Workspaces still finalize safely when active and require commitMessage. Composite delete dissolves only Composite-owned state and never closes member Workspaces. Checkout project files are never deleted; relayed delete remains unavailable.",
3343
+ description: "Close or explicitly delete one Workspace after the user chooses cleanup. action=close (default) preserves checkout, managed-worktree, Composite, and relayed identity for later reopen. action=delete permanently removes ForgeRelay-owned state. Managed-worktree-backed Workspaces still finalize safely when active and require commitMessage. Composite delete dissolves only Composite-owned state and never closes member Workspaces. Checkout project files are never deleted.",
3075
3344
  inputSchema: {
3076
3345
  workspaceId: z.string().describe("Workspace identifier to close or delete."),
3077
3346
  action: z
@@ -3115,8 +3384,10 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
3115
3384
  const composite = action === "delete"
3116
3385
  ? compositeWorkspaces.dissolve(workspaceId)
3117
3386
  : compositeWorkspaces.close(workspaceId);
3118
- if (action === "delete")
3387
+ if (action === "delete") {
3119
3388
  workspaceTasks.deleteWorkspace(workspaceId);
3389
+ taskReminders.forget(workspaceId);
3390
+ }
3120
3391
  compositeActivity.forgetComposite(workspaceId);
3121
3392
  workspacePanelStates.delete(workspaceId);
3122
3393
  const result = [
@@ -3156,10 +3427,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
3156
3427
  };
3157
3428
  }
3158
3429
  if (remoteWorkspaces.has(workspaceId)) {
3159
- if (action === "delete") {
3160
- throw new Error("close_workspace action=delete is not available for relayed Workspaces until Workspace Relay lifecycle parity is implemented.");
3161
- }
3162
- const response = await remoteWorkspaces.closeWorkspace(workspaceId, commitMessage, hostScopeIdFor(extra._meta, extra.sessionId));
3430
+ const response = await remoteWorkspaces.closeWorkspace(workspaceId, { action, ...(commitMessage !== undefined ? { commitMessage } : {}) }, hostScopeIdFor(extra._meta, extra.sessionId));
3163
3431
  workspacePanelStates.delete(workspaceId);
3164
3432
  return response;
3165
3433
  }
@@ -3184,6 +3452,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
3184
3452
  operation: async () => {
3185
3453
  workspaces.deleteWorkspace(session.id);
3186
3454
  workspaceTasks.deleteWorkspace(session.id);
3455
+ taskReminders.forget(session.id);
3187
3456
  await reviewCheckpoints.releaseWorkspace(session.id);
3188
3457
  const result = `Deleted ForgeRelay Workspace ${session.id}. Physical project files were not removed.`;
3189
3458
  return {
@@ -3227,6 +3496,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
3227
3496
  operation: async () => {
3228
3497
  workspaces.deleteWorkspace(session.id);
3229
3498
  workspaceTasks.deleteWorkspace(session.id);
3499
+ taskReminders.forget(session.id);
3230
3500
  await reviewCheckpoints.releaseWorkspace(session.id);
3231
3501
  const result = `Deleted closed managed-worktree Workspace ${session.id}. Its already-removed worktree backing was not recreated.`;
3232
3502
  return {
@@ -3290,6 +3560,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
3290
3560
  if (action === "delete") {
3291
3561
  workspaces.deleteWorkspace(workspace.id);
3292
3562
  workspaceTasks.deleteWorkspace(workspace.id);
3563
+ taskReminders.forget(workspace.id);
3293
3564
  }
3294
3565
  const result = [
3295
3566
  action === "delete"
@@ -3454,10 +3725,10 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
3454
3725
  const executionWorkspaceId = target.executionWorkspaceId;
3455
3726
  const executionContext = await prepareExecutionContext(target, extra._meta, extra.signal, extra.sessionId);
3456
3727
  if (remoteWorkspaces.has(executionWorkspaceId)) {
3457
- return presentExecutionResult(await remoteWorkspaces.read(executionWorkspaceId, { path, paths, offset, limit }, hostScopeIdFor(extra._meta, extra.sessionId)), target);
3728
+ return presentSemanticWorkResult(await remoteWorkspaces.read(executionWorkspaceId, { path, paths, offset, limit }, hostScopeIdFor(extra._meta, extra.sessionId)), target);
3458
3729
  }
3459
3730
  if (path !== undefined) {
3460
- return presentExecutionResult(await coreOperations.read({ workspaceId: executionWorkspaceId, path, offset, limit }, executionContext), target);
3731
+ return presentSemanticWorkResult(await coreOperations.read({ workspaceId: executionWorkspaceId, path, offset, limit }, executionContext), target);
3461
3732
  }
3462
3733
  const workspace = workspaces.getWorkspace(executionWorkspaceId);
3463
3734
  let response;
@@ -3510,7 +3781,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
3510
3781
  : { type: "succeeded" }, activityRelationFor(executionContext));
3511
3782
  if (!response)
3512
3783
  throw new Error("Bulk Read completed without a response.");
3513
- return presentExecutionResult(response, target);
3784
+ return presentSemanticWorkResult(response, target);
3514
3785
  });
3515
3786
  if (config.toolMode !== "codex") {
3516
3787
  registerAppTool(server, toolNames.write, {
@@ -3534,9 +3805,9 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
3534
3805
  const executionWorkspaceId = target.executionWorkspaceId;
3535
3806
  const executionContext = await prepareExecutionContext(target, extra._meta, extra.signal, extra.sessionId);
3536
3807
  if (remoteWorkspaces.has(executionWorkspaceId)) {
3537
- return presentExecutionResult(await remoteWorkspaces.write(executionWorkspaceId, input, hostScopeIdFor(extra._meta, extra.sessionId)), target);
3808
+ return presentSemanticWorkResult(await remoteWorkspaces.write(executionWorkspaceId, input, hostScopeIdFor(extra._meta, extra.sessionId)), target);
3538
3809
  }
3539
- return presentExecutionResult(await coreOperations.write({ workspaceId: executionWorkspaceId, ...input }, executionContext), target);
3810
+ return presentSemanticWorkResult(await coreOperations.write({ workspaceId: executionWorkspaceId, ...input }, executionContext), target);
3540
3811
  });
3541
3812
  registerAppTool(server, toolNames.edit, {
3542
3813
  title: "Edit file",
@@ -3587,12 +3858,12 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
3587
3858
  const executionWorkspaceId = target.executionWorkspaceId;
3588
3859
  const executionContext = await prepareExecutionContext(target, extra._meta, extra.signal, extra.sessionId);
3589
3860
  if (remoteWorkspaces.has(executionWorkspaceId)) {
3590
- return presentExecutionResult(await remoteWorkspaces.edit(executionWorkspaceId, { path, paths, edits }, hostScopeIdFor(extra._meta, extra.sessionId)), target);
3861
+ return presentSemanticWorkResult(await remoteWorkspaces.edit(executionWorkspaceId, { path, paths, edits }, hostScopeIdFor(extra._meta, extra.sessionId)), target);
3591
3862
  }
3592
3863
  if (path !== undefined) {
3593
- return presentExecutionResult(await coreOperations.edit({ workspaceId: executionWorkspaceId, path, edits }, executionContext), target);
3864
+ return presentSemanticWorkResult(await coreOperations.edit({ workspaceId: executionWorkspaceId, path, edits }, executionContext), target);
3594
3865
  }
3595
- return presentExecutionResult(await nativeBulkMutations.edit({ workspaceId: executionWorkspaceId, paths: paths, edits }, executionContext), target);
3866
+ return presentSemanticWorkResult(await nativeBulkMutations.edit({ workspaceId: executionWorkspaceId, paths: paths, edits }, executionContext), target);
3596
3867
  });
3597
3868
  }
3598
3869
  registerAppTool(server, toolNames.rename, {
@@ -3616,9 +3887,9 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
3616
3887
  const executionWorkspaceId = target.executionWorkspaceId;
3617
3888
  const executionContext = await prepareExecutionContext(target, extra._meta, extra.signal, extra.sessionId);
3618
3889
  if (remoteWorkspaces.has(executionWorkspaceId)) {
3619
- return presentExecutionResult(await remoteWorkspaces.rename(executionWorkspaceId, { path, newPath }, hostScopeIdFor(extra._meta, extra.sessionId)), target);
3890
+ return presentSemanticWorkResult(await remoteWorkspaces.rename(executionWorkspaceId, { path, newPath }, hostScopeIdFor(extra._meta, extra.sessionId)), target);
3620
3891
  }
3621
- return presentExecutionResult(await coreOperations.rename({ workspaceId: executionWorkspaceId, path, newPath }, executionContext), target);
3892
+ return presentSemanticWorkResult(await coreOperations.rename({ workspaceId: executionWorkspaceId, path, newPath }, executionContext), target);
3622
3893
  });
3623
3894
  registerAppTool(server, toolNames.delete, {
3624
3895
  title: "Delete path",
@@ -3659,12 +3930,12 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
3659
3930
  const executionWorkspaceId = target.executionWorkspaceId;
3660
3931
  const executionContext = await prepareExecutionContext(target, extra._meta, extra.signal, extra.sessionId);
3661
3932
  if (remoteWorkspaces.has(executionWorkspaceId)) {
3662
- return presentExecutionResult(await remoteWorkspaces.delete(executionWorkspaceId, { path, paths, recursive }, hostScopeIdFor(extra._meta, extra.sessionId)), target);
3933
+ return presentSemanticWorkResult(await remoteWorkspaces.delete(executionWorkspaceId, { path, paths, recursive }, hostScopeIdFor(extra._meta, extra.sessionId)), target);
3663
3934
  }
3664
3935
  if (path !== undefined) {
3665
- return presentExecutionResult(await coreOperations.delete({ workspaceId: executionWorkspaceId, path, recursive }, executionContext), target);
3936
+ return presentSemanticWorkResult(await coreOperations.delete({ workspaceId: executionWorkspaceId, path, recursive }, executionContext), target);
3666
3937
  }
3667
- return presentExecutionResult(await nativeBulkMutations.delete({ workspaceId: executionWorkspaceId, paths: paths, recursive }, executionContext), target);
3938
+ return presentSemanticWorkResult(await nativeBulkMutations.delete({ workspaceId: executionWorkspaceId, paths: paths, recursive }, executionContext), target);
3668
3939
  });
3669
3940
  if (config.toolMode === "codex") {
3670
3941
  registerAppTool(server, "apply_patch", {
@@ -3695,7 +3966,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
3695
3966
  const executionWorkspaceId = target.executionWorkspaceId;
3696
3967
  const executionContext = await prepareExecutionContext(target, extra._meta, extra.signal, extra.sessionId);
3697
3968
  if (remoteWorkspaces.has(executionWorkspaceId)) {
3698
- return presentExecutionResult(await remoteWorkspaces.applyPatch(executionWorkspaceId, { patch }, hostScopeIdFor(extra._meta, extra.sessionId)), target);
3969
+ return presentSemanticWorkResult(await remoteWorkspaces.applyPatch(executionWorkspaceId, { patch }, hostScopeIdFor(extra._meta, extra.sessionId)), target);
3699
3970
  }
3700
3971
  const workspace = workspaces.getWorkspace(executionWorkspaceId);
3701
3972
  return runActivityToolWithHooks(activityLifecycle, hooks, workspace, hostScopeIdFor(extra._meta, extra.sessionId), activityRequestFor({ workspaceId: executionWorkspaceId, patch }, executionContext), {
@@ -3745,7 +4016,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
3745
4016
  },
3746
4017
  };
3747
4018
  },
3748
- }, activityRelationFor(executionContext)).then((result) => presentExecutionResult(result, target));
4019
+ }, activityRelationFor(executionContext)).then((result) => presentSemanticWorkResult(result, target));
3749
4020
  });
3750
4021
  }
3751
4022
  if (config.toolMode !== "codex") {
@@ -3835,7 +4106,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
3835
4106
  const executionWorkspaceId = target.executionWorkspaceId;
3836
4107
  const executionContext = await prepareExecutionContext(target, extra._meta, extra.signal, extra.sessionId);
3837
4108
  if (remoteWorkspaces.has(executionWorkspaceId)) {
3838
- return presentExecutionResult(await remoteWorkspaces.bash(executionWorkspaceId, {
4109
+ const response = await remoteWorkspaces.bash(executionWorkspaceId, {
3839
4110
  action,
3840
4111
  ...(command !== undefined ? { command } : {}),
3841
4112
  ...(processId !== undefined ? { processId } : {}),
@@ -3849,7 +4120,10 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
3849
4120
  ...(yieldTimeMs !== undefined ? { yieldTimeMs } : {}),
3850
4121
  ...(timeoutMs !== undefined ? { timeoutMs } : {}),
3851
4122
  ...(maxOutputTokens !== undefined ? { maxOutputTokens } : {}),
3852
- }, hostScopeIdFor(extra._meta, extra.sessionId)), target);
4123
+ }, hostScopeIdFor(extra._meta, extra.sessionId));
4124
+ return action === "run"
4125
+ ? presentSemanticWorkResult(response, target)
4126
+ : presentExecutionResult(response, target);
3853
4127
  }
3854
4128
  const workspace = workspaces.getWorkspace(executionWorkspaceId);
3855
4129
  if (action === "run") {
@@ -3858,7 +4132,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
3858
4132
  if (processId !== undefined || outputId !== undefined || input !== undefined || interrupt !== undefined) {
3859
4133
  throw new Error("bash action=run does not accept processId, outputId, input, or interrupt.");
3860
4134
  }
3861
- return presentExecutionResult(await coreOperations.shellRun({
4135
+ return presentSemanticWorkResult(await coreOperations.shellRun({
3862
4136
  workspaceId: executionWorkspaceId,
3863
4137
  command,
3864
4138
  surface: "bash",
@@ -3952,6 +4226,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
3952
4226
  resolve: resolveExecutionTarget,
3953
4227
  prepare: prepareExecutionContext,
3954
4228
  present: presentExecutionResult,
4229
+ presentSemantic: presentSemanticWorkResult,
3955
4230
  isRemote: (workspaceId) => remoteWorkspaces.has(workspaceId),
3956
4231
  execCommandRemote: (workspaceId, input, conversationScopeId) => remoteWorkspaces.execCommand(workspaceId, input, conversationScopeId),
3957
4232
  writeStdinRemote: (workspaceId, input, conversationScopeId) => remoteWorkspaces.writeStdin(workspaceId, input, conversationScopeId),
@@ -3982,6 +4257,10 @@ export function createServer(config = loadConfig(), options = {}) {
3982
4257
  });
3983
4258
  const workspaceStore = createWorkspaceStore(config.stateDir);
3984
4259
  const workspaces = new WorkspaceRegistry(config, workspaceStore);
4260
+ const sharedRemoteWorkspaces = new RemoteWorkspaceRelay(config.configDir, config.stateDir);
4261
+ const sharedCompositeWorkspaces = new CompositeWorkspaceRegistry(config.stateDir);
4262
+ const sharedWorkspaceTasks = new WorkspaceTaskStore(config.stateDir);
4263
+ const sharedTaskReminders = new WorkspaceTaskReminderTracker(config.taskReminderInterval, sharedWorkspaceTasks);
3985
4264
  const activityAuditStore = new ActivityAuditStore(config.stateDir);
3986
4265
  const bashOutputStore = new BashOutputStore(config.stateDir);
3987
4266
  const hostTurnStore = new HostTurnStore(config.stateDir);
@@ -4176,7 +4455,11 @@ export function createServer(config = loadConfig(), options = {}) {
4176
4455
  });
4177
4456
  }
4178
4457
  };
4179
- const server = createMcpServer(config, workspaces, reviewCheckpoints, processSessions, subagentProviders, incomingArtifactAdapters, codeIntelligence, activityLifecycle, bashOutputStore, activityQueries);
4458
+ const server = createMcpServer(config, workspaces, reviewCheckpoints, processSessions, subagentProviders, incomingArtifactAdapters, codeIntelligence, activityLifecycle, bashOutputStore, activityQueries, {
4459
+ taskReminders: sharedTaskReminders,
4460
+ remoteWorkspaces: sharedRemoteWorkspaces,
4461
+ compositeWorkspaces: sharedCompositeWorkspaces,
4462
+ });
4180
4463
  await server.connect(transport);
4181
4464
  }
4182
4465
  else {