@akira-tl/forgerelay 0.8.1 → 0.8.3
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 +13 -0
- package/README.md +4 -2
- package/capabilities/workspace-tasks/GUIDE.md +29 -0
- package/dist/capabilities.js +6 -0
- package/dist/capability-registry.js +82 -21
- package/dist/composite-activity.js +1 -1
- package/dist/composite-workspaces.js +31 -10
- package/dist/git-worktrees.js +22 -0
- package/dist/mcp/server-instructions.js +1 -1
- package/dist/server.js +290 -60
- package/dist/subagents/sessions/capability.js +3 -0
- package/dist/workspace-store.js +25 -0
- package/dist/workspace-tasks.js +351 -0
- package/dist/workspaces.js +95 -13
- package/docs/chatgpt-coding-workflow.md +17 -2
- package/docs/configuration.md +21 -6
- package/package.json +2 -2
- package/scripts/debug/accept.mjs +255 -2
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,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 { WorkspaceTaskStore } from "./workspace-tasks.js";
|
|
56
57
|
import { formatAgentsPath, WorkspaceRegistry } from "./workspaces.js";
|
|
57
58
|
import { formatAvailableSubagentProfile, summarizeSubagentProfile } from "./subagents/profiles.js";
|
|
58
59
|
import { formatSubagentProviderAvailabilitySummary, formatUnavailableSubagentProvider, getSubagentProviderAvailabilitySnapshot, } from "./subagents/providers/availability.js";
|
|
@@ -821,6 +822,7 @@ function workspaceHookInvocation(workspace) {
|
|
|
821
822
|
function capabilityContextFor(workspace) {
|
|
822
823
|
return {
|
|
823
824
|
workspaceId: workspace.id,
|
|
825
|
+
workspaceKind: "workspace",
|
|
824
826
|
workspaceRoot: workspace.root,
|
|
825
827
|
guides: workspace.capabilityGuides.map((guide) => ({
|
|
826
828
|
name: guide.name,
|
|
@@ -830,6 +832,56 @@ function capabilityContextFor(workspace) {
|
|
|
830
832
|
})),
|
|
831
833
|
};
|
|
832
834
|
}
|
|
835
|
+
function compositeCapabilityContext(workspaceId, guides) {
|
|
836
|
+
return {
|
|
837
|
+
workspaceId,
|
|
838
|
+
workspaceKind: "composite",
|
|
839
|
+
guides: guides.map((guide) => ({
|
|
840
|
+
name: guide.name,
|
|
841
|
+
description: guide.description,
|
|
842
|
+
whenToRead: guide.whenToRead,
|
|
843
|
+
path: formatPathForPrompt(guide.filePath),
|
|
844
|
+
})),
|
|
845
|
+
};
|
|
846
|
+
}
|
|
847
|
+
function requireCapabilityWorkspaceRoot(context) {
|
|
848
|
+
if (!context.workspaceRoot) {
|
|
849
|
+
throw new CapabilityError("capability_unavailable", `Capability execution requires a filesystem-backed Workspace; ${context.workspaceId} is ${context.workspaceKind}.`);
|
|
850
|
+
}
|
|
851
|
+
return context.workspaceRoot;
|
|
852
|
+
}
|
|
853
|
+
function runWorkspaceTasksCapability(store, workspaceId, input) {
|
|
854
|
+
switch (input.operation) {
|
|
855
|
+
case "get":
|
|
856
|
+
return store.read(workspaceId);
|
|
857
|
+
case "list.create":
|
|
858
|
+
return store.createList(workspaceId, { name: input.name, position: input.position });
|
|
859
|
+
case "list.update":
|
|
860
|
+
return store.updateList(workspaceId, input.listId, {
|
|
861
|
+
name: input.name,
|
|
862
|
+
state: input.state,
|
|
863
|
+
position: input.position,
|
|
864
|
+
});
|
|
865
|
+
case "list.delete":
|
|
866
|
+
return store.deleteList(workspaceId, input.listId);
|
|
867
|
+
case "task.create":
|
|
868
|
+
return store.createTask(workspaceId, input.listId, {
|
|
869
|
+
subject: input.subject,
|
|
870
|
+
content: input.content,
|
|
871
|
+
status: input.status,
|
|
872
|
+
position: input.position,
|
|
873
|
+
});
|
|
874
|
+
case "task.update":
|
|
875
|
+
return store.updateTask(workspaceId, input.listId, input.taskId, {
|
|
876
|
+
subject: input.subject,
|
|
877
|
+
content: input.content,
|
|
878
|
+
status: input.status,
|
|
879
|
+
position: input.position,
|
|
880
|
+
});
|
|
881
|
+
case "task.delete":
|
|
882
|
+
return store.deleteTask(workspaceId, input.listId, input.taskId);
|
|
883
|
+
}
|
|
884
|
+
}
|
|
833
885
|
async function reviewWorkspaceChanges(reviewCheckpoints, workspace) {
|
|
834
886
|
return reviewCheckpoints.reviewChanges({
|
|
835
887
|
workspaceId: workspace.id,
|
|
@@ -1192,6 +1244,8 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
1192
1244
|
const connectionScopeId = `mcp-connection:${randomUUID()}`;
|
|
1193
1245
|
const remoteWorkspaces = new RemoteWorkspaceRelay(config.configDir, config.stateDir);
|
|
1194
1246
|
const compositeWorkspaces = new CompositeWorkspaceRegistry(config.stateDir);
|
|
1247
|
+
const workspaceTasks = new WorkspaceTaskStore(config.stateDir);
|
|
1248
|
+
const compositeTaskGuides = loadCapabilityGuides(config).filter((guide) => guide.name === "workspace-tasks");
|
|
1195
1249
|
const compositeActivity = new CompositeActivityCoordinator(compositeWorkspaces, activityQueries, remoteWorkspaces);
|
|
1196
1250
|
const resolveExecutionTarget = (workspaceId, memberName) => {
|
|
1197
1251
|
if (!compositeWorkspaces.has(workspaceId)) {
|
|
@@ -1248,6 +1302,12 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
1248
1302
|
const capabilityRegistry = createCapabilityRegistry({
|
|
1249
1303
|
inspectHooks: (workspaceRoot) => checkHookConfiguration(workspaceRoot, config.hooks),
|
|
1250
1304
|
...subagentMcp.registryDependencies,
|
|
1305
|
+
workspaceTasks: {
|
|
1306
|
+
available: true,
|
|
1307
|
+
run: async (input, context) => ({
|
|
1308
|
+
value: runWorkspaceTasksCapability(workspaceTasks, context.workspaceId, input),
|
|
1309
|
+
}),
|
|
1310
|
+
},
|
|
1251
1311
|
batchExecute: {
|
|
1252
1312
|
available: batchExecuteAvailable,
|
|
1253
1313
|
unavailableReason: batchExecuteAvailable
|
|
@@ -1270,7 +1330,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
1270
1330
|
run: async (input, context, options) => {
|
|
1271
1331
|
try {
|
|
1272
1332
|
return {
|
|
1273
|
-
value: await codeIntelligence.run(context
|
|
1333
|
+
value: await codeIntelligence.run(requireCapabilityWorkspaceRoot(context), input, { signal: options.signal }),
|
|
1274
1334
|
};
|
|
1275
1335
|
}
|
|
1276
1336
|
catch (error) {
|
|
@@ -1289,7 +1349,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
1289
1349
|
run: async (context) => {
|
|
1290
1350
|
const review = await reviewWorkspaceChanges(reviewCheckpoints, {
|
|
1291
1351
|
id: context.workspaceId,
|
|
1292
|
-
root: context
|
|
1352
|
+
root: requireCapabilityWorkspaceRoot(context),
|
|
1293
1353
|
});
|
|
1294
1354
|
return {
|
|
1295
1355
|
value: {
|
|
@@ -1317,7 +1377,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
1317
1377
|
const downloaded = await downloadIncomingArtifact({
|
|
1318
1378
|
registry: incomingArtifactRegistry,
|
|
1319
1379
|
workspaceId: context.workspaceId,
|
|
1320
|
-
workspaceRoot: context
|
|
1380
|
+
workspaceRoot: requireCapabilityWorkspaceRoot(context),
|
|
1321
1381
|
maxFileBytes: config.artifactMaxFileBytes,
|
|
1322
1382
|
file: input.file,
|
|
1323
1383
|
path: input.path,
|
|
@@ -2078,6 +2138,8 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
2078
2138
|
memberAction: z.enum(["add", "update", "remove"]).optional(),
|
|
2079
2139
|
kind: z.enum(["workspace", "composite"]).optional(),
|
|
2080
2140
|
name: z.string().optional(),
|
|
2141
|
+
status: z.string().optional(),
|
|
2142
|
+
state: z.enum(["active", "stale", "invalid", "closed"]).optional(),
|
|
2081
2143
|
members: z.array(z.object({
|
|
2082
2144
|
name: z.string(),
|
|
2083
2145
|
purpose: z.string(),
|
|
@@ -2134,6 +2196,8 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
2134
2196
|
workspaceId: z.string(),
|
|
2135
2197
|
kind: z.literal("composite"),
|
|
2136
2198
|
name: z.string(),
|
|
2199
|
+
status: z.enum(["active", "closed"]),
|
|
2200
|
+
state: z.enum(["active", "closed"]),
|
|
2137
2201
|
members: z.array(z.object({
|
|
2138
2202
|
name: z.string(),
|
|
2139
2203
|
purpose: z.string(),
|
|
@@ -2161,6 +2225,9 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
2161
2225
|
if (!workspaceId || !compositeWorkspaces.has(workspaceId)) {
|
|
2162
2226
|
throw new Error("open_workspace action=member requires an existing Composite Workspace workspaceId.");
|
|
2163
2227
|
}
|
|
2228
|
+
if (!compositeWorkspaces.isActive(workspaceId)) {
|
|
2229
|
+
throw new Error(`Composite Workspace ${workspaceId} is closed. Reopen it with open_workspace before changing members.`);
|
|
2230
|
+
}
|
|
2164
2231
|
if (!memberAction || !member) {
|
|
2165
2232
|
throw new Error("open_workspace action=member requires memberAction and member.");
|
|
2166
2233
|
}
|
|
@@ -2294,25 +2361,30 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
2294
2361
|
newWorkspace !== undefined || context !== undefined) {
|
|
2295
2362
|
throw new Error("open_workspace action=list does not accept path, relay, name, memberName, baseRef, newWorktree, newWorkspace, or context. Use kind/root/workspaceId/mode/status/state/staleOnly for inventory filters.");
|
|
2296
2363
|
}
|
|
2364
|
+
const compositeInventory = () => compositeWorkspaces.list()
|
|
2365
|
+
.filter((entry) => workspaceId === undefined || entry.id === workspaceId)
|
|
2366
|
+
.filter((entry) => status === undefined || entry.status === status)
|
|
2367
|
+
.filter((entry) => state === undefined || entry.status === state)
|
|
2368
|
+
.map((entry) => ({
|
|
2369
|
+
workspaceId: entry.id,
|
|
2370
|
+
kind: entry.kind,
|
|
2371
|
+
name: entry.name,
|
|
2372
|
+
status: entry.status,
|
|
2373
|
+
state: entry.status,
|
|
2374
|
+
members: entry.members,
|
|
2375
|
+
createdAt: entry.createdAt,
|
|
2376
|
+
lastUsedAt: entry.lastUsedAt,
|
|
2377
|
+
}));
|
|
2297
2378
|
if (kind === "composite") {
|
|
2298
|
-
if (root !== undefined || mode !== undefined ||
|
|
2299
|
-
|
|
2300
|
-
throw new Error("Composite Workspace inventory does not accept root/mode/
|
|
2379
|
+
if (root !== undefined || mode !== undefined || staleOnly !== undefined ||
|
|
2380
|
+
offset !== undefined || limit !== undefined) {
|
|
2381
|
+
throw new Error("Composite Workspace inventory does not accept root/mode/staleOnly/offset/limit filters; use workspaceId/status/state when selecting Composite Workspaces.");
|
|
2301
2382
|
}
|
|
2302
|
-
const composites =
|
|
2303
|
-
|
|
2304
|
-
.map((entry) => ({
|
|
2305
|
-
workspaceId: entry.id,
|
|
2306
|
-
kind: entry.kind,
|
|
2307
|
-
name: entry.name,
|
|
2308
|
-
members: entry.members,
|
|
2309
|
-
createdAt: entry.createdAt,
|
|
2310
|
-
lastUsedAt: entry.lastUsedAt,
|
|
2311
|
-
}));
|
|
2312
|
-
const instruction = "Resume a Composite Workspace with open_workspace(action=\"open\", workspaceId=...). Use close_workspace only when the user chooses to dissolve it.";
|
|
2383
|
+
const composites = compositeInventory();
|
|
2384
|
+
const instruction = "Open a Composite Workspace by workspaceId to resume or reopen it. close_workspace preserves its identity; action=delete permanently dissolves only Composite-owned state.";
|
|
2313
2385
|
const result = [
|
|
2314
2386
|
`Composite Workspace inventory: ${composites.length} matching record${composites.length === 1 ? "" : "s"}.`,
|
|
2315
|
-
...composites.map((entry) => `${entry.name} [${entry.workspaceId}] members=${entry.members.length} last-used=${entry.lastUsedAt}`),
|
|
2387
|
+
...composites.map((entry) => `${entry.name} [${entry.workspaceId}] state=${entry.state} members=${entry.members.length} last-used=${entry.lastUsedAt}`),
|
|
2316
2388
|
instruction,
|
|
2317
2389
|
].join("\n");
|
|
2318
2390
|
return {
|
|
@@ -2325,20 +2397,13 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
2325
2397
|
};
|
|
2326
2398
|
}
|
|
2327
2399
|
const inventory = await workspaces.listWorkspaces({ workspaceId, mode, root, status, state, staleOnly, offset, limit }, { conversationScopeId, protectedWorkspaceIds });
|
|
2328
|
-
const composites = kind === "workspace"
|
|
2400
|
+
const composites = kind === "workspace" || root !== undefined || mode !== undefined || staleOnly
|
|
2329
2401
|
? []
|
|
2330
|
-
:
|
|
2331
|
-
workspaceId: entry.id,
|
|
2332
|
-
kind: entry.kind,
|
|
2333
|
-
name: entry.name,
|
|
2334
|
-
members: entry.members,
|
|
2335
|
-
createdAt: entry.createdAt,
|
|
2336
|
-
lastUsedAt: entry.lastUsedAt,
|
|
2337
|
-
}));
|
|
2402
|
+
: compositeInventory();
|
|
2338
2403
|
const nextOffset = inventory.page.offset + inventory.page.limit;
|
|
2339
2404
|
const instruction = [
|
|
2340
2405
|
"Resume a selected workspaceId with open_workspace(action=\"open\", workspaceId=...).",
|
|
2341
|
-
"Use close_workspace only after the user chooses cleanup
|
|
2406
|
+
"Use close_workspace only after the user chooses cleanup; Composite close preserves identity, while action=delete dissolves only Composite-owned state. Never close inventory entries automatically.",
|
|
2342
2407
|
inventory.page.hasMore
|
|
2343
2408
|
? `More matching workspaces are available; continue with offset=${nextOffset}.`
|
|
2344
2409
|
: undefined,
|
|
@@ -2356,7 +2421,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
2356
2421
|
`root=${entry.root}`,
|
|
2357
2422
|
`last-used=${entry.lastUsedAt}`,
|
|
2358
2423
|
].filter(Boolean).join(" ")),
|
|
2359
|
-
...composites.map((entry) => `${entry.name} [${entry.workspaceId}] kind=composite members=${entry.members.length}`),
|
|
2424
|
+
...composites.map((entry) => `${entry.name} [${entry.workspaceId}] kind=composite state=${entry.state} members=${entry.members.length}`),
|
|
2360
2425
|
instruction,
|
|
2361
2426
|
].join("\n");
|
|
2362
2427
|
logToolCall(config, {
|
|
@@ -2393,6 +2458,15 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
2393
2458
|
const composite = workspaceId !== undefined
|
|
2394
2459
|
? compositeWorkspaces.open(workspaceId)
|
|
2395
2460
|
: compositeWorkspaces.create(name ?? "");
|
|
2461
|
+
workspaceTasks.initializeWorkspace(composite.id);
|
|
2462
|
+
const compositeTaskContext = compositeCapabilityContext(composite.id, compositeTaskGuides);
|
|
2463
|
+
const compositeCapabilityCatalog = capabilityRegistry.catalog(compositeTaskContext);
|
|
2464
|
+
const compositeCapabilityGuides = compositeTaskGuides.map((guide) => ({
|
|
2465
|
+
name: guide.name,
|
|
2466
|
+
description: guide.description,
|
|
2467
|
+
whenToRead: guide.whenToRead,
|
|
2468
|
+
path: formatPathForPrompt(guide.filePath),
|
|
2469
|
+
}));
|
|
2396
2470
|
const memberContext = memberName
|
|
2397
2471
|
? await loadCompositeMemberContext(composite.id, memberName, context ?? "auto", conversationScopeId, protectedWorkspaceIds)
|
|
2398
2472
|
: undefined;
|
|
@@ -2403,10 +2477,13 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
2403
2477
|
? `Members: ${composite.members.map((member) => `${member.name} — ${member.purpose}`).join("; ")}.`
|
|
2404
2478
|
: "This Composite Workspace currently has no members.",
|
|
2405
2479
|
"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.",
|
|
2480
|
+
compositeCapabilityCatalog.length > 0
|
|
2481
|
+
? `Composite-owned capabilities: ${compositeCapabilityCatalog.map((entry) => entry.name).join(", ")}. Use these without member because their state belongs to the Composite Workspace itself.`
|
|
2482
|
+
: undefined,
|
|
2406
2483
|
composite.members.length > 0
|
|
2407
2484
|
? "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."
|
|
2408
2485
|
: undefined,
|
|
2409
|
-
"Use
|
|
2486
|
+
"close_workspace preserves this Composite identity for later reopen. Use action=delete only when the user explicitly wants to dissolve the Composite relationship; neither operation closes or cleans up member Workspaces.",
|
|
2410
2487
|
].join("\n\n");
|
|
2411
2488
|
const response = {
|
|
2412
2489
|
content: [textBlock(instruction)],
|
|
@@ -2419,7 +2496,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
2419
2496
|
path: composite.name,
|
|
2420
2497
|
members: composite.members,
|
|
2421
2498
|
instruction,
|
|
2422
|
-
summary: { members: composite.members.length },
|
|
2499
|
+
summary: { members: composite.members.length, status: composite.status },
|
|
2423
2500
|
},
|
|
2424
2501
|
},
|
|
2425
2502
|
structuredContent: {
|
|
@@ -2427,7 +2504,11 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
2427
2504
|
workspaceId: composite.id,
|
|
2428
2505
|
kind: "composite",
|
|
2429
2506
|
name: composite.name,
|
|
2507
|
+
status: composite.status,
|
|
2508
|
+
state: composite.status,
|
|
2430
2509
|
members: composite.members,
|
|
2510
|
+
capabilityCatalog: compositeCapabilityCatalog,
|
|
2511
|
+
capabilityGuides: compositeCapabilityGuides,
|
|
2431
2512
|
...(memberContext ? { memberContext } : {}),
|
|
2432
2513
|
instruction,
|
|
2433
2514
|
},
|
|
@@ -2540,6 +2621,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
2540
2621
|
conversationScopeId,
|
|
2541
2622
|
protectedWorkspaceIds,
|
|
2542
2623
|
});
|
|
2624
|
+
workspaceTasks.initializeWorkspace(workspace.id);
|
|
2543
2625
|
const knownWorktrees = await workspaces.listKnownWorktrees(workspace);
|
|
2544
2626
|
const staleWorkspaces = await workspaces.listStaleWorkspaces(workspace);
|
|
2545
2627
|
const capabilityFingerprint = buildCapabilityFingerprint(config, FORGERELAY_VERSION, {
|
|
@@ -2801,6 +2883,74 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
2801
2883
|
openWorldHint: true,
|
|
2802
2884
|
},
|
|
2803
2885
|
}, async ({ workspaceId, member, name, action, arguments: capabilityArguments, file }, extra) => {
|
|
2886
|
+
if (name === "workspace.tasks" && compositeWorkspaces.has(workspaceId)) {
|
|
2887
|
+
if (member !== undefined) {
|
|
2888
|
+
throw new Error(`workspace.tasks belongs to Composite Workspace ${workspaceId} itself and does not accept member.`);
|
|
2889
|
+
}
|
|
2890
|
+
if (!compositeWorkspaces.isActive(workspaceId)) {
|
|
2891
|
+
throw new Error(`Composite Workspace ${workspaceId} is closed. Reopen it with open_workspace before use.`);
|
|
2892
|
+
}
|
|
2893
|
+
const startedAt = performance.now();
|
|
2894
|
+
const context = compositeCapabilityContext(workspaceId, compositeTaskGuides);
|
|
2895
|
+
try {
|
|
2896
|
+
if (action === "run") {
|
|
2897
|
+
const execution = await capabilityRegistry.run(name, capabilityArguments ?? {}, context, {
|
|
2898
|
+
nativeFile: file,
|
|
2899
|
+
signal: extra.signal,
|
|
2900
|
+
requestMeta: extra._meta,
|
|
2901
|
+
sessionId: extra.sessionId,
|
|
2902
|
+
});
|
|
2903
|
+
const result = {
|
|
2904
|
+
content: [textBlock(`Capability ${name} completed.\n${JSON.stringify(execution.value, null, 2)}`)],
|
|
2905
|
+
structuredContent: { name, action, result: execution.value },
|
|
2906
|
+
};
|
|
2907
|
+
logToolCall(config, {
|
|
2908
|
+
tool: toolNames.capability,
|
|
2909
|
+
capability: name,
|
|
2910
|
+
action,
|
|
2911
|
+
success: true,
|
|
2912
|
+
durationMs: Math.round(performance.now() - startedAt),
|
|
2913
|
+
});
|
|
2914
|
+
return result;
|
|
2915
|
+
}
|
|
2916
|
+
const capability = capabilityRegistry.describe(name, context);
|
|
2917
|
+
const result = {
|
|
2918
|
+
content: [textBlock([
|
|
2919
|
+
`${capability.name}: ${capability.description}`,
|
|
2920
|
+
`Available: ${capability.available}`,
|
|
2921
|
+
`Guide: ${capability.guide.path}`,
|
|
2922
|
+
capability.guide.readBeforeFirstUse
|
|
2923
|
+
? "Read the guide before first use when this contract is unfamiliar."
|
|
2924
|
+
: undefined,
|
|
2925
|
+
].filter(Boolean).join("\n"))],
|
|
2926
|
+
structuredContent: { name, action, capability },
|
|
2927
|
+
};
|
|
2928
|
+
logToolCall(config, {
|
|
2929
|
+
tool: toolNames.capability,
|
|
2930
|
+
capability: name,
|
|
2931
|
+
action,
|
|
2932
|
+
success: true,
|
|
2933
|
+
durationMs: Math.round(performance.now() - startedAt),
|
|
2934
|
+
});
|
|
2935
|
+
return result;
|
|
2936
|
+
}
|
|
2937
|
+
catch (error) {
|
|
2938
|
+
if (extra.signal.aborted)
|
|
2939
|
+
throw error;
|
|
2940
|
+
const capabilityError = error instanceof CapabilityError
|
|
2941
|
+
? error
|
|
2942
|
+
: new CapabilityError("execution_failed", error instanceof Error ? error.message : String(error));
|
|
2943
|
+
return {
|
|
2944
|
+
content: [textBlock(`${capabilityError.code}: ${capabilityError.message}`)],
|
|
2945
|
+
structuredContent: {
|
|
2946
|
+
name,
|
|
2947
|
+
action,
|
|
2948
|
+
error: { code: capabilityError.code, message: capabilityError.message },
|
|
2949
|
+
},
|
|
2950
|
+
isError: true,
|
|
2951
|
+
};
|
|
2952
|
+
}
|
|
2953
|
+
}
|
|
2804
2954
|
const target = resolveExecutionTarget(workspaceId, member);
|
|
2805
2955
|
const executionWorkspaceId = target.executionWorkspaceId;
|
|
2806
2956
|
const executionContext = await prepareExecutionContext(target, extra._meta, extra.signal, extra.sessionId);
|
|
@@ -2921,13 +3071,13 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
2921
3071
|
});
|
|
2922
3072
|
registerAppTool(server, toolNames.closeWorkspace, {
|
|
2923
3073
|
title: "Close workspace",
|
|
2924
|
-
description: "Close or explicitly delete one Workspace after the user chooses cleanup. action=close (default) preserves checkout identity for later reopen. action=delete permanently removes ForgeRelay-owned
|
|
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.",
|
|
2925
3075
|
inputSchema: {
|
|
2926
3076
|
workspaceId: z.string().describe("Workspace identifier to close or delete."),
|
|
2927
3077
|
action: z
|
|
2928
3078
|
.enum(["close", "delete"])
|
|
2929
3079
|
.optional()
|
|
2930
|
-
.describe("Defaults to close. close preserves checkout identity for later reopen; delete
|
|
3080
|
+
.describe("Defaults to close. close preserves checkout identity, managed-worktree identity, and Composite identity for later reopen; delete removes ForgeRelay-owned state. Composite delete dissolves only the Composite relationship. Active managed worktrees still require safe finalization and commitMessage; checkout project files are never deleted."),
|
|
2931
3081
|
commitMessage: z
|
|
2932
3082
|
.string()
|
|
2933
3083
|
.min(1)
|
|
@@ -2945,6 +3095,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
2945
3095
|
purpose: z.string(),
|
|
2946
3096
|
workspaceId: z.string(),
|
|
2947
3097
|
})).optional(),
|
|
3098
|
+
status: z.enum(["active", "closed"]).optional(),
|
|
2948
3099
|
dissolved: z.boolean().optional(),
|
|
2949
3100
|
sourceRoot: z.string().optional(),
|
|
2950
3101
|
branch: z.string().optional(),
|
|
@@ -2958,21 +3109,24 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
2958
3109
|
annotations: WRITE_TOOL_ANNOTATIONS,
|
|
2959
3110
|
}, async ({ workspaceId, action = "close", commitMessage }, extra) => {
|
|
2960
3111
|
if (compositeWorkspaces.has(workspaceId)) {
|
|
2961
|
-
if (action === "delete") {
|
|
2962
|
-
throw new Error("close_workspace action=delete is not available for Composite Workspaces until the Composite persistent lifecycle stage.");
|
|
2963
|
-
}
|
|
2964
3112
|
if (commitMessage !== undefined) {
|
|
2965
|
-
throw new Error("close_workspace commitMessage is not valid
|
|
3113
|
+
throw new Error("close_workspace commitMessage is not valid for a Composite Workspace.");
|
|
2966
3114
|
}
|
|
2967
|
-
const composite =
|
|
3115
|
+
const composite = action === "delete"
|
|
3116
|
+
? compositeWorkspaces.dissolve(workspaceId)
|
|
3117
|
+
: compositeWorkspaces.close(workspaceId);
|
|
3118
|
+
if (action === "delete")
|
|
3119
|
+
workspaceTasks.deleteWorkspace(workspaceId);
|
|
2968
3120
|
compositeActivity.forgetComposite(workspaceId);
|
|
2969
3121
|
workspacePanelStates.delete(workspaceId);
|
|
2970
3122
|
const result = [
|
|
2971
|
-
|
|
3123
|
+
action === "delete"
|
|
3124
|
+
? `Deleted Composite Workspace ${composite.name} (${workspaceId}); its Composite relationship and ForgeRelay-owned Composite state were dissolved.`
|
|
3125
|
+
: `Closed Composite Workspace ${composite.name} (${workspaceId}); its identity and member topology were preserved for later reopen.`,
|
|
2972
3126
|
composite.members.length > 0
|
|
2973
3127
|
? `Preserved member Workspaces: ${composite.members.map((member) => `${member.name} [${member.workspaceId}]`).join(", ")}.`
|
|
2974
3128
|
: "The Composite Workspace had no members.",
|
|
2975
|
-
"Member Workspace handles, managed worktrees, processes, files, and Workspace Relay routes were not closed or
|
|
3129
|
+
"Member Workspace handles, managed worktrees, processes, files, and Workspace Relay routes were not closed, finalized, deleted, or otherwise mutated.",
|
|
2976
3130
|
].join("\n");
|
|
2977
3131
|
return {
|
|
2978
3132
|
content: [textBlock(result)],
|
|
@@ -2980,22 +3134,24 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
2980
3134
|
tool: toolNames.closeWorkspace,
|
|
2981
3135
|
card: {
|
|
2982
3136
|
workspaceId,
|
|
2983
|
-
action
|
|
3137
|
+
action,
|
|
2984
3138
|
kind: "composite",
|
|
2985
3139
|
name: composite.name,
|
|
2986
3140
|
members: composite.members,
|
|
2987
|
-
|
|
3141
|
+
...(action === "close" ? { status: "closed" } : {}),
|
|
3142
|
+
dissolved: action === "delete",
|
|
2988
3143
|
payload: { content: [textBlock(result)] },
|
|
2989
3144
|
},
|
|
2990
3145
|
},
|
|
2991
3146
|
structuredContent: {
|
|
2992
3147
|
result,
|
|
2993
3148
|
workspaceId,
|
|
2994
|
-
action
|
|
3149
|
+
action,
|
|
2995
3150
|
kind: "composite",
|
|
2996
3151
|
name: composite.name,
|
|
2997
3152
|
members: composite.members,
|
|
2998
|
-
|
|
3153
|
+
...(action === "close" ? { status: "closed" } : {}),
|
|
3154
|
+
dissolved: action === "delete",
|
|
2999
3155
|
},
|
|
3000
3156
|
};
|
|
3001
3157
|
}
|
|
@@ -3007,14 +3163,11 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
3007
3163
|
workspacePanelStates.delete(workspaceId);
|
|
3008
3164
|
return response;
|
|
3009
3165
|
}
|
|
3010
|
-
|
|
3166
|
+
const session = workspaces.getWorkspaceSession(workspaceId);
|
|
3167
|
+
if (action === "delete" && session.mode === "checkout") {
|
|
3011
3168
|
if (commitMessage !== undefined) {
|
|
3012
3169
|
throw new Error("close_workspace commitMessage is not valid with action=delete for a checkout Workspace.");
|
|
3013
3170
|
}
|
|
3014
|
-
const session = workspaces.getWorkspaceSession(workspaceId);
|
|
3015
|
-
if (session.mode !== "checkout") {
|
|
3016
|
-
throw new Error("close_workspace action=delete is not available for managed-worktree-backed Workspaces until their persistent lifecycle stage.");
|
|
3017
|
-
}
|
|
3018
3171
|
if (processSessions.activeWorkspaceIds().has(session.id)) {
|
|
3019
3172
|
throw new Error(`Workspace ${session.id} still owns a running process. Poll, interrupt, or wait for it before deleting this Workspace.`);
|
|
3020
3173
|
}
|
|
@@ -3030,6 +3183,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
3030
3183
|
payload: { workspaceId: session.id, action: "delete", mode: session.mode },
|
|
3031
3184
|
operation: async () => {
|
|
3032
3185
|
workspaces.deleteWorkspace(session.id);
|
|
3186
|
+
workspaceTasks.deleteWorkspace(session.id);
|
|
3033
3187
|
await reviewCheckpoints.releaseWorkspace(session.id);
|
|
3034
3188
|
const result = `Deleted ForgeRelay Workspace ${session.id}. Physical project files were not removed.`;
|
|
3035
3189
|
return {
|
|
@@ -3055,12 +3209,59 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
3055
3209
|
workspacePanelStates.delete(session.id);
|
|
3056
3210
|
return response;
|
|
3057
3211
|
}
|
|
3058
|
-
|
|
3212
|
+
if (action === "delete" && session.mode === "worktree" && session.status === "closed") {
|
|
3213
|
+
if (commitMessage !== undefined) {
|
|
3214
|
+
throw new Error("close_workspace commitMessage is not needed when deleting an already-closed managed-worktree Workspace.");
|
|
3215
|
+
}
|
|
3216
|
+
const hookRoot = session.sourceRoot ?? session.root;
|
|
3217
|
+
const response = await runToolWithHooks(hooks, {
|
|
3218
|
+
signal: extra.signal,
|
|
3219
|
+
tool: toolNames.closeWorkspace,
|
|
3220
|
+
invocation: {
|
|
3221
|
+
workspaceId: session.id,
|
|
3222
|
+
workspaceRoot: hookRoot,
|
|
3223
|
+
workspaceMode: session.mode,
|
|
3224
|
+
sourceRoot: session.sourceRoot,
|
|
3225
|
+
},
|
|
3226
|
+
payload: { workspaceId: session.id, action: "delete", mode: session.mode },
|
|
3227
|
+
operation: async () => {
|
|
3228
|
+
workspaces.deleteWorkspace(session.id);
|
|
3229
|
+
workspaceTasks.deleteWorkspace(session.id);
|
|
3230
|
+
await reviewCheckpoints.releaseWorkspace(session.id);
|
|
3231
|
+
const result = `Deleted closed managed-worktree Workspace ${session.id}. Its already-removed worktree backing was not recreated.`;
|
|
3232
|
+
return {
|
|
3233
|
+
content: [textBlock(result)],
|
|
3234
|
+
_meta: {
|
|
3235
|
+
tool: toolNames.closeWorkspace,
|
|
3236
|
+
card: {
|
|
3237
|
+
workspaceId: session.id,
|
|
3238
|
+
action: "delete",
|
|
3239
|
+
mode: "worktree",
|
|
3240
|
+
sourceRoot: session.sourceRoot,
|
|
3241
|
+
targetBranch: session.targetBranch,
|
|
3242
|
+
payload: { content: [textBlock(result)] },
|
|
3243
|
+
},
|
|
3244
|
+
},
|
|
3245
|
+
structuredContent: {
|
|
3246
|
+
result,
|
|
3247
|
+
workspaceId: session.id,
|
|
3248
|
+
action: "delete",
|
|
3249
|
+
mode: "worktree",
|
|
3250
|
+
sourceRoot: session.sourceRoot,
|
|
3251
|
+
targetBranch: session.targetBranch,
|
|
3252
|
+
},
|
|
3253
|
+
};
|
|
3254
|
+
},
|
|
3255
|
+
});
|
|
3256
|
+
workspacePanelStates.delete(session.id);
|
|
3257
|
+
return response;
|
|
3258
|
+
}
|
|
3259
|
+
const workspace = workspaces.getWorkspace(session.id);
|
|
3059
3260
|
const response = await runToolWithHooks(hooks, {
|
|
3060
3261
|
signal: extra.signal,
|
|
3061
3262
|
tool: toolNames.closeWorkspace,
|
|
3062
3263
|
invocation: workspaceHookInvocation(workspace),
|
|
3063
|
-
payload: { workspaceId, action
|
|
3264
|
+
payload: { workspaceId: workspace.id, action, commitMessage, mode: workspace.mode },
|
|
3064
3265
|
afterCwd: (response) => "sourceRoot" in response.structuredContent &&
|
|
3065
3266
|
typeof response.structuredContent.sourceRoot === "string"
|
|
3066
3267
|
? response.structuredContent.sourceRoot
|
|
@@ -3068,7 +3269,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
3068
3269
|
operation: async () => {
|
|
3069
3270
|
if (workspace.mode === "worktree") {
|
|
3070
3271
|
if (!commitMessage) {
|
|
3071
|
-
throw new Error(`Managed-worktree-backed
|
|
3272
|
+
throw new Error(`Managed-worktree-backed Workspace ${workspace.id} requires commitMessage when ${action === "delete" ? "deleting active work" : "closing"}.`);
|
|
3072
3273
|
}
|
|
3073
3274
|
const physicalWorkspaceIds = workspaces.workspaceIdsForPhysicalWorkspace(workspace);
|
|
3074
3275
|
const busyWorkspaceIds = physicalWorkspaceIds
|
|
@@ -3080,14 +3281,20 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
3080
3281
|
const retirement = await codeIntelligence.retireWorkspaceRoot(workspace.root);
|
|
3081
3282
|
let closed;
|
|
3082
3283
|
try {
|
|
3083
|
-
closed = await workspaces.closeWorktree(
|
|
3284
|
+
closed = await workspaces.closeWorktree(workspace.id, commitMessage);
|
|
3084
3285
|
}
|
|
3085
3286
|
finally {
|
|
3086
3287
|
codeIntelligence.restoreWorkspaceRoot(retirement.root);
|
|
3087
3288
|
}
|
|
3088
3289
|
await Promise.all(physicalWorkspaceIds.map((id) => reviewCheckpoints.releaseWorkspace(id)));
|
|
3290
|
+
if (action === "delete") {
|
|
3291
|
+
workspaces.deleteWorkspace(workspace.id);
|
|
3292
|
+
workspaceTasks.deleteWorkspace(workspace.id);
|
|
3293
|
+
}
|
|
3089
3294
|
const result = [
|
|
3090
|
-
|
|
3295
|
+
action === "delete"
|
|
3296
|
+
? `Safely finalized and deleted managed-worktree Workspace ${workspace.id}.`
|
|
3297
|
+
: `Closed managed-worktree-backed Workspace ${workspace.id}; its identity was preserved for later reopen.`,
|
|
3091
3298
|
`Merged ${closed.branch} into ${closed.targetBranch} by fast-forward.`,
|
|
3092
3299
|
`Source checkout: ${closed.sourceRoot}`,
|
|
3093
3300
|
`Commit: ${closed.commitSha}`,
|
|
@@ -3107,8 +3314,8 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
3107
3314
|
_meta: {
|
|
3108
3315
|
tool: toolNames.closeWorkspace,
|
|
3109
3316
|
card: {
|
|
3110
|
-
workspaceId,
|
|
3111
|
-
action
|
|
3317
|
+
workspaceId: workspace.id,
|
|
3318
|
+
action,
|
|
3112
3319
|
mode: "worktree",
|
|
3113
3320
|
sourceRoot: closed.sourceRoot,
|
|
3114
3321
|
branch: closed.branch,
|
|
@@ -3122,8 +3329,8 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
3122
3329
|
},
|
|
3123
3330
|
structuredContent: {
|
|
3124
3331
|
result,
|
|
3125
|
-
workspaceId,
|
|
3126
|
-
action
|
|
3332
|
+
workspaceId: workspace.id,
|
|
3333
|
+
action,
|
|
3127
3334
|
mode: "worktree",
|
|
3128
3335
|
sourceRoot: closed.sourceRoot,
|
|
3129
3336
|
branch: closed.branch,
|
|
@@ -3178,7 +3385,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
3178
3385
|
member: z
|
|
3179
3386
|
.string()
|
|
3180
3387
|
.optional()
|
|
3181
|
-
.describe("Required for
|
|
3388
|
+
.describe("Required for Composite member-scoped file reads. Omit only when reading an advertised Composite-owned capability guide."),
|
|
3182
3389
|
path: z
|
|
3183
3390
|
.string()
|
|
3184
3391
|
.optional()
|
|
@@ -3220,6 +3427,29 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
3220
3427
|
if ((path === undefined) === (paths === undefined)) {
|
|
3221
3428
|
throw new Error("read requires exactly one of path or paths.");
|
|
3222
3429
|
}
|
|
3430
|
+
if (compositeWorkspaces.has(workspaceId) && member === undefined && path !== undefined) {
|
|
3431
|
+
const guide = compositeTaskGuides.find((candidate) => formatPathForPrompt(candidate.filePath) === path || candidate.filePath === path);
|
|
3432
|
+
if (guide) {
|
|
3433
|
+
if (!compositeWorkspaces.isActive(workspaceId)) {
|
|
3434
|
+
throw new Error(`Composite Workspace ${workspaceId} is closed. Reopen it with open_workspace before use.`);
|
|
3435
|
+
}
|
|
3436
|
+
const startedAt = performance.now();
|
|
3437
|
+
const raw = readFileSync(guide.filePath, "utf8");
|
|
3438
|
+
const start = (offset ?? 1) - 1;
|
|
3439
|
+
const end = limit === undefined ? undefined : start + limit;
|
|
3440
|
+
const result = raw.split("\n").slice(start, end).join("\n");
|
|
3441
|
+
logToolCall(config, {
|
|
3442
|
+
tool: toolNames.read,
|
|
3443
|
+
path: formatPathForPrompt(guide.filePath),
|
|
3444
|
+
success: true,
|
|
3445
|
+
durationMs: Math.round(performance.now() - startedAt),
|
|
3446
|
+
});
|
|
3447
|
+
return {
|
|
3448
|
+
content: [textBlock(result)],
|
|
3449
|
+
structuredContent: { result },
|
|
3450
|
+
};
|
|
3451
|
+
}
|
|
3452
|
+
}
|
|
3223
3453
|
const target = resolveExecutionTarget(workspaceId, member);
|
|
3224
3454
|
const executionWorkspaceId = target.executionWorkspaceId;
|
|
3225
3455
|
const executionContext = await prepareExecutionContext(target, extra._meta, extra.signal, extra.sessionId);
|
|
@@ -19,6 +19,9 @@ export class SubagentSessionCapability {
|
|
|
19
19
|
this.ownerAliveOverride = options.ownerAlive;
|
|
20
20
|
}
|
|
21
21
|
async run(input, context, options) {
|
|
22
|
+
if (!context.workspaceRoot) {
|
|
23
|
+
throw new CapabilityError("capability_unavailable", "subagent.session requires a filesystem-backed Workspace.");
|
|
24
|
+
}
|
|
22
25
|
const manager = new SubagentSessionManager(this.config, {
|
|
23
26
|
launch: (request) => this.launch(request),
|
|
24
27
|
});
|