@akira-tl/forgerelay 0.8.3 → 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/CHANGELOG.md +10 -0
- package/capabilities/workspace-tasks/GUIDE.md +23 -2
- package/dist/capability-registry.js +15 -1
- package/dist/config.js +11 -0
- package/dist/remote-workspace-relay.js +15 -0
- package/dist/server.js +284 -40
- package/dist/workspace-task-reminders.js +42 -0
- package/dist/workspace-tasks.js +69 -0
- package/dist/workspaces.js +62 -30
- package/docs/chatgpt-coding-workflow.md +13 -3
- package/docs/configuration.md +46 -11
- package/package.json +2 -2
- package/scripts/debug/accept.mjs +178 -18
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,74 @@ 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
|
+
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
|
+
]);
|
|
262
331
|
const reviewFileOutputSchema = z.object({
|
|
263
332
|
path: z.string(),
|
|
264
333
|
previousPath: z.string().optional(),
|
|
@@ -345,6 +414,17 @@ function logFailedToolResponse(config, fields, content, startedAt) {
|
|
|
345
414
|
function textBlock(text) {
|
|
346
415
|
return { type: "text", text };
|
|
347
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
|
+
}
|
|
348
428
|
function textSummary(content) {
|
|
349
429
|
const text = contentText(content);
|
|
350
430
|
return {
|
|
@@ -853,33 +933,43 @@ function requireCapabilityWorkspaceRoot(context) {
|
|
|
853
933
|
function runWorkspaceTasksCapability(store, workspaceId, input) {
|
|
854
934
|
switch (input.operation) {
|
|
855
935
|
case "get":
|
|
856
|
-
|
|
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);
|
|
857
941
|
case "list.create":
|
|
858
|
-
|
|
942
|
+
store.createList(workspaceId, { name: input.name, position: input.position });
|
|
943
|
+
return store.readSummary(workspaceId);
|
|
859
944
|
case "list.update":
|
|
860
|
-
|
|
945
|
+
store.updateList(workspaceId, input.listId, {
|
|
861
946
|
name: input.name,
|
|
862
947
|
state: input.state,
|
|
863
948
|
position: input.position,
|
|
864
949
|
});
|
|
950
|
+
return store.readSummary(workspaceId);
|
|
865
951
|
case "list.delete":
|
|
866
|
-
|
|
952
|
+
store.deleteList(workspaceId, input.listId);
|
|
953
|
+
return store.readSummary(workspaceId);
|
|
867
954
|
case "task.create":
|
|
868
|
-
|
|
955
|
+
store.createTask(workspaceId, input.listId, {
|
|
869
956
|
subject: input.subject,
|
|
870
957
|
content: input.content,
|
|
871
958
|
status: input.status,
|
|
872
959
|
position: input.position,
|
|
873
960
|
});
|
|
961
|
+
return store.readHeaders(workspaceId, input.listId);
|
|
874
962
|
case "task.update":
|
|
875
|
-
|
|
963
|
+
store.updateTask(workspaceId, input.listId, input.taskId, {
|
|
876
964
|
subject: input.subject,
|
|
877
965
|
content: input.content,
|
|
878
966
|
status: input.status,
|
|
879
967
|
position: input.position,
|
|
880
968
|
});
|
|
969
|
+
return store.readHeaders(workspaceId, input.listId);
|
|
881
970
|
case "task.delete":
|
|
882
|
-
|
|
971
|
+
store.deleteTask(workspaceId, input.listId, input.taskId);
|
|
972
|
+
return store.readHeaders(workspaceId, input.listId);
|
|
883
973
|
}
|
|
884
974
|
}
|
|
885
975
|
async function reviewWorkspaceChanges(reviewCheckpoints, workspace) {
|
|
@@ -1106,7 +1196,7 @@ function registerProcessTools(server, config, workspaces, processSessions, hooks
|
|
|
1106
1196
|
const target = routing.resolve(workspaceId, member);
|
|
1107
1197
|
const context = await routing.prepare(target, extra._meta, extra.signal, extra.sessionId);
|
|
1108
1198
|
if (routing.isRemote(target.executionWorkspaceId)) {
|
|
1109
|
-
return routing.
|
|
1199
|
+
return routing.presentSemantic(await routing.execCommandRemote(target.executionWorkspaceId, {
|
|
1110
1200
|
cmd,
|
|
1111
1201
|
...(tty !== undefined ? { tty } : {}),
|
|
1112
1202
|
...(columns !== undefined ? { columns } : {}),
|
|
@@ -1117,7 +1207,7 @@ function registerProcessTools(server, config, workspaces, processSessions, hooks
|
|
|
1117
1207
|
...(maxOutputTokens !== undefined ? { maxOutputTokens } : {}),
|
|
1118
1208
|
}, routing.hostScopeIdFor(extra._meta, extra.sessionId)), target);
|
|
1119
1209
|
}
|
|
1120
|
-
return routing.
|
|
1210
|
+
return routing.presentSemantic(await shellRun({
|
|
1121
1211
|
workspaceId: target.executionWorkspaceId,
|
|
1122
1212
|
command: cmd,
|
|
1123
1213
|
surface: "exec_command",
|
|
@@ -1245,6 +1335,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
1245
1335
|
const remoteWorkspaces = new RemoteWorkspaceRelay(config.configDir, config.stateDir);
|
|
1246
1336
|
const compositeWorkspaces = new CompositeWorkspaceRegistry(config.stateDir);
|
|
1247
1337
|
const workspaceTasks = new WorkspaceTaskStore(config.stateDir);
|
|
1338
|
+
const taskReminders = new WorkspaceTaskReminderTracker(config.taskReminderInterval, workspaceTasks);
|
|
1248
1339
|
const compositeTaskGuides = loadCapabilityGuides(config).filter((guide) => guide.name === "workspace-tasks");
|
|
1249
1340
|
const compositeActivity = new CompositeActivityCoordinator(compositeWorkspaces, activityQueries, remoteWorkspaces);
|
|
1250
1341
|
const resolveExecutionTarget = (workspaceId, memberName) => {
|
|
@@ -1278,6 +1369,25 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
1278
1369
|
: remapCompositeToolResult(result, target.executionWorkspaceId, target.compositeWorkspaceId, target.memberName);
|
|
1279
1370
|
return subagentMcp.decorateResult(target.executionWorkspaceId, presented);
|
|
1280
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
|
+
};
|
|
1281
1391
|
const hostScopeIdFor = (requestMeta, transportSessionId) => hostConversationScopeId(requestMeta, transportSessionId, connectionScopeId);
|
|
1282
1392
|
const prepareExecutionContext = async (target, requestMeta, signal, sessionId) => {
|
|
1283
1393
|
const conversationScopeId = hostScopeIdFor(requestMeta, sessionId);
|
|
@@ -1304,9 +1414,12 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
1304
1414
|
...subagentMcp.registryDependencies,
|
|
1305
1415
|
workspaceTasks: {
|
|
1306
1416
|
available: true,
|
|
1307
|
-
run: async (input, context) =>
|
|
1308
|
-
value
|
|
1309
|
-
|
|
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
|
+
},
|
|
1310
1423
|
},
|
|
1311
1424
|
batchExecute: {
|
|
1312
1425
|
available: batchExecuteAvailable,
|
|
@@ -2039,9 +2152,9 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
2039
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.",
|
|
2040
2153
|
inputSchema: {
|
|
2041
2154
|
action: z
|
|
2042
|
-
.enum(["open", "list", "member"])
|
|
2155
|
+
.enum(["open", "list", "inspect", "member"])
|
|
2043
2156
|
.optional()
|
|
2044
|
-
.describe("Defaults to open. Use list
|
|
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."),
|
|
2045
2158
|
memberAction: z
|
|
2046
2159
|
.enum(["add", "update", "remove"])
|
|
2047
2160
|
.optional()
|
|
@@ -2081,7 +2194,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
2081
2194
|
workspaceId: z
|
|
2082
2195
|
.string()
|
|
2083
2196
|
.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
|
|
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."),
|
|
2085
2198
|
mode: z
|
|
2086
2199
|
.enum(["checkout", "worktree"])
|
|
2087
2200
|
.optional()
|
|
@@ -2133,7 +2246,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
2133
2246
|
.describe("For action=list, maximum records to return. Defaults to 50; maximum 100."),
|
|
2134
2247
|
},
|
|
2135
2248
|
outputSchema: {
|
|
2136
|
-
action: z.enum(["open", "list", "member"]),
|
|
2249
|
+
action: z.enum(["open", "list", "inspect", "member"]),
|
|
2137
2250
|
workspaceId: z.string().optional(),
|
|
2138
2251
|
memberAction: z.enum(["add", "update", "remove"]).optional(),
|
|
2139
2252
|
kind: z.enum(["workspace", "composite"]).optional(),
|
|
@@ -2208,6 +2321,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
2208
2321
|
})).optional(),
|
|
2209
2322
|
summary: workspaceInventorySummaryOutputSchema.optional(),
|
|
2210
2323
|
page: workspaceInventoryPageOutputSchema.optional(),
|
|
2324
|
+
inspection: workspaceInspectionOutputSchema.optional(),
|
|
2211
2325
|
instruction: z.string(),
|
|
2212
2326
|
},
|
|
2213
2327
|
_meta: {},
|
|
@@ -2221,6 +2335,121 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
2221
2335
|
const startedAt = performance.now();
|
|
2222
2336
|
const conversationScopeId = openAiConversationScopeId(_meta);
|
|
2223
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
|
+
}
|
|
2224
2453
|
if (action === "member") {
|
|
2225
2454
|
if (!workspaceId || !compositeWorkspaces.has(workspaceId)) {
|
|
2226
2455
|
throw new Error("open_workspace action=member requires an existing Composite Workspace workspaceId.");
|
|
@@ -2673,7 +2902,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
2673
2902
|
const loadedAgentsFiles = includeBootstrapContext ? cardAgentsFiles : [];
|
|
2674
2903
|
const availableAgentsFileOutputs = includeBootstrapContext ? cardAvailableAgentsFiles : [];
|
|
2675
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.";
|
|
2676
|
-
const workspaceManagementInstruction = "
|
|
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.";
|
|
2677
2906
|
const cardInstruction = config.skillsEnabled
|
|
2678
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}`
|
|
2679
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}`;
|
|
@@ -2955,12 +3184,15 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
2955
3184
|
const executionWorkspaceId = target.executionWorkspaceId;
|
|
2956
3185
|
const executionContext = await prepareExecutionContext(target, extra._meta, extra.signal, extra.sessionId);
|
|
2957
3186
|
if (remoteWorkspaces.has(executionWorkspaceId)) {
|
|
2958
|
-
|
|
3187
|
+
const response = await remoteWorkspaces.capability(executionWorkspaceId, {
|
|
2959
3188
|
name,
|
|
2960
3189
|
action,
|
|
2961
3190
|
...(capabilityArguments !== undefined ? { arguments: capabilityArguments } : {}),
|
|
2962
3191
|
...(file !== undefined ? { file } : {}),
|
|
2963
|
-
}, hostScopeIdFor(extra._meta, extra.sessionId))
|
|
3192
|
+
}, hostScopeIdFor(extra._meta, extra.sessionId));
|
|
3193
|
+
return action === "run" && name !== "workspace.tasks"
|
|
3194
|
+
? presentSemanticWorkResult(response, target)
|
|
3195
|
+
: presentExecutionResult(response, target);
|
|
2964
3196
|
}
|
|
2965
3197
|
if (action === "run" && name === "batch.execute") {
|
|
2966
3198
|
const workspace = workspaces.getWorkspace(executionWorkspaceId);
|
|
@@ -2984,7 +3216,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
2984
3216
|
success: true,
|
|
2985
3217
|
durationMs: Math.round(performance.now() - startedAt),
|
|
2986
3218
|
});
|
|
2987
|
-
return
|
|
3219
|
+
return presentSemanticWorkResult(result, target);
|
|
2988
3220
|
}
|
|
2989
3221
|
catch (error) {
|
|
2990
3222
|
if (extra.signal.aborted)
|
|
@@ -3011,7 +3243,10 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
3011
3243
|
}
|
|
3012
3244
|
}
|
|
3013
3245
|
if (action === "run") {
|
|
3014
|
-
|
|
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);
|
|
3015
3250
|
}
|
|
3016
3251
|
const workspace = workspaces.getWorkspace(executionWorkspaceId);
|
|
3017
3252
|
return runActivityToolWithHooks(activityLifecycle, hooks, workspace, hostScopeIdFor(extra._meta, extra.sessionId), activityRequestFor({ workspaceId: executionWorkspaceId, name, action, arguments: capabilityArguments, file }, executionContext), {
|
|
@@ -3115,8 +3350,10 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
3115
3350
|
const composite = action === "delete"
|
|
3116
3351
|
? compositeWorkspaces.dissolve(workspaceId)
|
|
3117
3352
|
: compositeWorkspaces.close(workspaceId);
|
|
3118
|
-
if (action === "delete")
|
|
3353
|
+
if (action === "delete") {
|
|
3119
3354
|
workspaceTasks.deleteWorkspace(workspaceId);
|
|
3355
|
+
taskReminders.forget(workspaceId);
|
|
3356
|
+
}
|
|
3120
3357
|
compositeActivity.forgetComposite(workspaceId);
|
|
3121
3358
|
workspacePanelStates.delete(workspaceId);
|
|
3122
3359
|
const result = [
|
|
@@ -3184,6 +3421,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
3184
3421
|
operation: async () => {
|
|
3185
3422
|
workspaces.deleteWorkspace(session.id);
|
|
3186
3423
|
workspaceTasks.deleteWorkspace(session.id);
|
|
3424
|
+
taskReminders.forget(session.id);
|
|
3187
3425
|
await reviewCheckpoints.releaseWorkspace(session.id);
|
|
3188
3426
|
const result = `Deleted ForgeRelay Workspace ${session.id}. Physical project files were not removed.`;
|
|
3189
3427
|
return {
|
|
@@ -3227,6 +3465,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
3227
3465
|
operation: async () => {
|
|
3228
3466
|
workspaces.deleteWorkspace(session.id);
|
|
3229
3467
|
workspaceTasks.deleteWorkspace(session.id);
|
|
3468
|
+
taskReminders.forget(session.id);
|
|
3230
3469
|
await reviewCheckpoints.releaseWorkspace(session.id);
|
|
3231
3470
|
const result = `Deleted closed managed-worktree Workspace ${session.id}. Its already-removed worktree backing was not recreated.`;
|
|
3232
3471
|
return {
|
|
@@ -3290,6 +3529,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
3290
3529
|
if (action === "delete") {
|
|
3291
3530
|
workspaces.deleteWorkspace(workspace.id);
|
|
3292
3531
|
workspaceTasks.deleteWorkspace(workspace.id);
|
|
3532
|
+
taskReminders.forget(workspace.id);
|
|
3293
3533
|
}
|
|
3294
3534
|
const result = [
|
|
3295
3535
|
action === "delete"
|
|
@@ -3454,10 +3694,10 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
3454
3694
|
const executionWorkspaceId = target.executionWorkspaceId;
|
|
3455
3695
|
const executionContext = await prepareExecutionContext(target, extra._meta, extra.signal, extra.sessionId);
|
|
3456
3696
|
if (remoteWorkspaces.has(executionWorkspaceId)) {
|
|
3457
|
-
return
|
|
3697
|
+
return presentSemanticWorkResult(await remoteWorkspaces.read(executionWorkspaceId, { path, paths, offset, limit }, hostScopeIdFor(extra._meta, extra.sessionId)), target);
|
|
3458
3698
|
}
|
|
3459
3699
|
if (path !== undefined) {
|
|
3460
|
-
return
|
|
3700
|
+
return presentSemanticWorkResult(await coreOperations.read({ workspaceId: executionWorkspaceId, path, offset, limit }, executionContext), target);
|
|
3461
3701
|
}
|
|
3462
3702
|
const workspace = workspaces.getWorkspace(executionWorkspaceId);
|
|
3463
3703
|
let response;
|
|
@@ -3510,7 +3750,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
3510
3750
|
: { type: "succeeded" }, activityRelationFor(executionContext));
|
|
3511
3751
|
if (!response)
|
|
3512
3752
|
throw new Error("Bulk Read completed without a response.");
|
|
3513
|
-
return
|
|
3753
|
+
return presentSemanticWorkResult(response, target);
|
|
3514
3754
|
});
|
|
3515
3755
|
if (config.toolMode !== "codex") {
|
|
3516
3756
|
registerAppTool(server, toolNames.write, {
|
|
@@ -3534,9 +3774,9 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
3534
3774
|
const executionWorkspaceId = target.executionWorkspaceId;
|
|
3535
3775
|
const executionContext = await prepareExecutionContext(target, extra._meta, extra.signal, extra.sessionId);
|
|
3536
3776
|
if (remoteWorkspaces.has(executionWorkspaceId)) {
|
|
3537
|
-
return
|
|
3777
|
+
return presentSemanticWorkResult(await remoteWorkspaces.write(executionWorkspaceId, input, hostScopeIdFor(extra._meta, extra.sessionId)), target);
|
|
3538
3778
|
}
|
|
3539
|
-
return
|
|
3779
|
+
return presentSemanticWorkResult(await coreOperations.write({ workspaceId: executionWorkspaceId, ...input }, executionContext), target);
|
|
3540
3780
|
});
|
|
3541
3781
|
registerAppTool(server, toolNames.edit, {
|
|
3542
3782
|
title: "Edit file",
|
|
@@ -3587,12 +3827,12 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
3587
3827
|
const executionWorkspaceId = target.executionWorkspaceId;
|
|
3588
3828
|
const executionContext = await prepareExecutionContext(target, extra._meta, extra.signal, extra.sessionId);
|
|
3589
3829
|
if (remoteWorkspaces.has(executionWorkspaceId)) {
|
|
3590
|
-
return
|
|
3830
|
+
return presentSemanticWorkResult(await remoteWorkspaces.edit(executionWorkspaceId, { path, paths, edits }, hostScopeIdFor(extra._meta, extra.sessionId)), target);
|
|
3591
3831
|
}
|
|
3592
3832
|
if (path !== undefined) {
|
|
3593
|
-
return
|
|
3833
|
+
return presentSemanticWorkResult(await coreOperations.edit({ workspaceId: executionWorkspaceId, path, edits }, executionContext), target);
|
|
3594
3834
|
}
|
|
3595
|
-
return
|
|
3835
|
+
return presentSemanticWorkResult(await nativeBulkMutations.edit({ workspaceId: executionWorkspaceId, paths: paths, edits }, executionContext), target);
|
|
3596
3836
|
});
|
|
3597
3837
|
}
|
|
3598
3838
|
registerAppTool(server, toolNames.rename, {
|
|
@@ -3616,9 +3856,9 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
3616
3856
|
const executionWorkspaceId = target.executionWorkspaceId;
|
|
3617
3857
|
const executionContext = await prepareExecutionContext(target, extra._meta, extra.signal, extra.sessionId);
|
|
3618
3858
|
if (remoteWorkspaces.has(executionWorkspaceId)) {
|
|
3619
|
-
return
|
|
3859
|
+
return presentSemanticWorkResult(await remoteWorkspaces.rename(executionWorkspaceId, { path, newPath }, hostScopeIdFor(extra._meta, extra.sessionId)), target);
|
|
3620
3860
|
}
|
|
3621
|
-
return
|
|
3861
|
+
return presentSemanticWorkResult(await coreOperations.rename({ workspaceId: executionWorkspaceId, path, newPath }, executionContext), target);
|
|
3622
3862
|
});
|
|
3623
3863
|
registerAppTool(server, toolNames.delete, {
|
|
3624
3864
|
title: "Delete path",
|
|
@@ -3659,12 +3899,12 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
3659
3899
|
const executionWorkspaceId = target.executionWorkspaceId;
|
|
3660
3900
|
const executionContext = await prepareExecutionContext(target, extra._meta, extra.signal, extra.sessionId);
|
|
3661
3901
|
if (remoteWorkspaces.has(executionWorkspaceId)) {
|
|
3662
|
-
return
|
|
3902
|
+
return presentSemanticWorkResult(await remoteWorkspaces.delete(executionWorkspaceId, { path, paths, recursive }, hostScopeIdFor(extra._meta, extra.sessionId)), target);
|
|
3663
3903
|
}
|
|
3664
3904
|
if (path !== undefined) {
|
|
3665
|
-
return
|
|
3905
|
+
return presentSemanticWorkResult(await coreOperations.delete({ workspaceId: executionWorkspaceId, path, recursive }, executionContext), target);
|
|
3666
3906
|
}
|
|
3667
|
-
return
|
|
3907
|
+
return presentSemanticWorkResult(await nativeBulkMutations.delete({ workspaceId: executionWorkspaceId, paths: paths, recursive }, executionContext), target);
|
|
3668
3908
|
});
|
|
3669
3909
|
if (config.toolMode === "codex") {
|
|
3670
3910
|
registerAppTool(server, "apply_patch", {
|
|
@@ -3695,7 +3935,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
3695
3935
|
const executionWorkspaceId = target.executionWorkspaceId;
|
|
3696
3936
|
const executionContext = await prepareExecutionContext(target, extra._meta, extra.signal, extra.sessionId);
|
|
3697
3937
|
if (remoteWorkspaces.has(executionWorkspaceId)) {
|
|
3698
|
-
return
|
|
3938
|
+
return presentSemanticWorkResult(await remoteWorkspaces.applyPatch(executionWorkspaceId, { patch }, hostScopeIdFor(extra._meta, extra.sessionId)), target);
|
|
3699
3939
|
}
|
|
3700
3940
|
const workspace = workspaces.getWorkspace(executionWorkspaceId);
|
|
3701
3941
|
return runActivityToolWithHooks(activityLifecycle, hooks, workspace, hostScopeIdFor(extra._meta, extra.sessionId), activityRequestFor({ workspaceId: executionWorkspaceId, patch }, executionContext), {
|
|
@@ -3745,7 +3985,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
3745
3985
|
},
|
|
3746
3986
|
};
|
|
3747
3987
|
},
|
|
3748
|
-
}, activityRelationFor(executionContext)).then((result) =>
|
|
3988
|
+
}, activityRelationFor(executionContext)).then((result) => presentSemanticWorkResult(result, target));
|
|
3749
3989
|
});
|
|
3750
3990
|
}
|
|
3751
3991
|
if (config.toolMode !== "codex") {
|
|
@@ -3835,7 +4075,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
3835
4075
|
const executionWorkspaceId = target.executionWorkspaceId;
|
|
3836
4076
|
const executionContext = await prepareExecutionContext(target, extra._meta, extra.signal, extra.sessionId);
|
|
3837
4077
|
if (remoteWorkspaces.has(executionWorkspaceId)) {
|
|
3838
|
-
|
|
4078
|
+
const response = await remoteWorkspaces.bash(executionWorkspaceId, {
|
|
3839
4079
|
action,
|
|
3840
4080
|
...(command !== undefined ? { command } : {}),
|
|
3841
4081
|
...(processId !== undefined ? { processId } : {}),
|
|
@@ -3849,7 +4089,10 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
3849
4089
|
...(yieldTimeMs !== undefined ? { yieldTimeMs } : {}),
|
|
3850
4090
|
...(timeoutMs !== undefined ? { timeoutMs } : {}),
|
|
3851
4091
|
...(maxOutputTokens !== undefined ? { maxOutputTokens } : {}),
|
|
3852
|
-
}, hostScopeIdFor(extra._meta, extra.sessionId))
|
|
4092
|
+
}, hostScopeIdFor(extra._meta, extra.sessionId));
|
|
4093
|
+
return action === "run"
|
|
4094
|
+
? presentSemanticWorkResult(response, target)
|
|
4095
|
+
: presentExecutionResult(response, target);
|
|
3853
4096
|
}
|
|
3854
4097
|
const workspace = workspaces.getWorkspace(executionWorkspaceId);
|
|
3855
4098
|
if (action === "run") {
|
|
@@ -3858,7 +4101,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
3858
4101
|
if (processId !== undefined || outputId !== undefined || input !== undefined || interrupt !== undefined) {
|
|
3859
4102
|
throw new Error("bash action=run does not accept processId, outputId, input, or interrupt.");
|
|
3860
4103
|
}
|
|
3861
|
-
return
|
|
4104
|
+
return presentSemanticWorkResult(await coreOperations.shellRun({
|
|
3862
4105
|
workspaceId: executionWorkspaceId,
|
|
3863
4106
|
command,
|
|
3864
4107
|
surface: "bash",
|
|
@@ -3952,6 +4195,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
3952
4195
|
resolve: resolveExecutionTarget,
|
|
3953
4196
|
prepare: prepareExecutionContext,
|
|
3954
4197
|
present: presentExecutionResult,
|
|
4198
|
+
presentSemantic: presentSemanticWorkResult,
|
|
3955
4199
|
isRemote: (workspaceId) => remoteWorkspaces.has(workspaceId),
|
|
3956
4200
|
execCommandRemote: (workspaceId, input, conversationScopeId) => remoteWorkspaces.execCommand(workspaceId, input, conversationScopeId),
|
|
3957
4201
|
writeStdinRemote: (workspaceId, input, conversationScopeId) => remoteWorkspaces.writeStdin(workspaceId, input, conversationScopeId),
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
const TASK_REMINDER = "Reminder: this Workspace has unfinished active Tasks. Review workspace.tasks and update Task state when material progress, requirements, blockers, or conclusions changed.";
|
|
2
|
+
export class WorkspaceTaskReminderTracker {
|
|
3
|
+
interval;
|
|
4
|
+
tasks;
|
|
5
|
+
callsSinceUpdate = new Map();
|
|
6
|
+
constructor(interval, tasks) {
|
|
7
|
+
this.interval = interval;
|
|
8
|
+
this.tasks = tasks;
|
|
9
|
+
}
|
|
10
|
+
reset(workspaceId) {
|
|
11
|
+
this.callsSinceUpdate.delete(workspaceId);
|
|
12
|
+
}
|
|
13
|
+
forget(workspaceId) {
|
|
14
|
+
this.callsSinceUpdate.delete(workspaceId);
|
|
15
|
+
}
|
|
16
|
+
recordWork(workspaceId) {
|
|
17
|
+
if (this.interval === 0)
|
|
18
|
+
return undefined;
|
|
19
|
+
let summary;
|
|
20
|
+
try {
|
|
21
|
+
summary = this.tasks.readSummary(workspaceId);
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
// Reminder delivery is advisory. Invalid external Task state must still be
|
|
25
|
+
// surfaced by workspace.tasks itself, not turn unrelated work into failure.
|
|
26
|
+
this.callsSinceUpdate.delete(workspaceId);
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
const hasUnfinishedActiveTasks = summary.lists.some((list) => list.state === "active" && list.unfinishedTaskCount > 0);
|
|
30
|
+
if (!hasUnfinishedActiveTasks) {
|
|
31
|
+
this.callsSinceUpdate.delete(workspaceId);
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
const next = (this.callsSinceUpdate.get(workspaceId) ?? 0) + 1;
|
|
35
|
+
if (next < this.interval) {
|
|
36
|
+
this.callsSinceUpdate.set(workspaceId, next);
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
this.callsSinceUpdate.set(workspaceId, 0);
|
|
40
|
+
return TASK_REMINDER;
|
|
41
|
+
}
|
|
42
|
+
}
|