@akira-tl/forgerelay 0.8.4 → 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/CHANGELOG.md +15 -0
- package/README.md +2 -0
- package/dist/remote-workspace-relay.js +150 -22
- package/dist/server.js +64 -25
- package/package.json +4 -1
- package/scripts/debug/relay-accept.mjs +890 -0
- package/scripts/wiki/sync.mjs +246 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,21 @@ All notable ForgeRelay changes are documented here.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.8.5] - 2026-08-31
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- Added source-controlled GitHub Wiki pages with validation and publishing tooling.
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
|
|
15
|
+
- Relayed checkout and managed-worktree Workspaces now preserve stable Gateway identity and Execution-owned Task/lifecycle semantics across close, reopen, delete, inspection, and Composite member routing.
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
|
|
19
|
+
- Task reminders and relayed/Composite runtime state now survive independent MCP transport sessions instead of resetting or presenting stale session-local state.
|
|
20
|
+
- Managed-worktree Relay acceptance now tolerates Git-native LF/CRLF checkout normalization, keeping the release matrix portable across Linux, macOS, and Windows.
|
|
21
|
+
|
|
7
22
|
## [0.8.4] - 2026-08-31
|
|
8
23
|
|
|
9
24
|
### Added
|
package/README.md
CHANGED
|
@@ -348,6 +348,7 @@ Trusted Publishing setup.
|
|
|
348
348
|
|
|
349
349
|
## Documentation
|
|
350
350
|
|
|
351
|
+
- [GitHub Wiki](https://github.com/Akira-TL/forgerelay/wiki) — 面向用户的使用指南、概念说明与故障排查入口。
|
|
351
352
|
- [Setup Guide](docs/setup.md)
|
|
352
353
|
- [Local Debugging and 7677 Acceptance](docs/debugging.md)
|
|
353
354
|
- [ChatGPT Coding Workflow](docs/chatgpt-coding-workflow.md)
|
|
@@ -358,6 +359,7 @@ Trusted Publishing setup.
|
|
|
358
359
|
- [Troubleshooting](docs/gotchas.md)
|
|
359
360
|
- [Roadmap](docs/roadmap.md)
|
|
360
361
|
- [Versioning and Release Management](docs/versioning.md)
|
|
362
|
+
- [GitHub Wiki Maintenance](docs/maintenance/wiki.md)
|
|
361
363
|
- [Changelog](CHANGELOG.md)
|
|
362
364
|
- [Attribution Notice](NOTICE.md)
|
|
363
365
|
|
|
@@ -27,20 +27,57 @@ export class RemoteWorkspaceRelay {
|
|
|
27
27
|
this.loadRoutes();
|
|
28
28
|
return this.routes.has(workspaceId);
|
|
29
29
|
}
|
|
30
|
-
inspectWorkspace(gatewayWorkspaceId) {
|
|
30
|
+
async inspectWorkspace(gatewayWorkspaceId) {
|
|
31
31
|
const route = this.requireRoute(gatewayWorkspaceId);
|
|
32
32
|
const resolved = this.remoteByInstance(route.remoteInstanceId);
|
|
33
|
-
|
|
33
|
+
let result;
|
|
34
|
+
try {
|
|
35
|
+
result = await this.callRemoteTool(resolved.alias, resolved.remote, "open_workspace", {
|
|
36
|
+
action: "inspect",
|
|
37
|
+
workspaceId: route.remoteWorkspaceId,
|
|
38
|
+
});
|
|
39
|
+
assertRemoteToolSucceeded(resolved.alias, "open_workspace", result);
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
throw sanitizedRemoteError(error, route.remoteWorkspaceId, gatewayWorkspaceId);
|
|
43
|
+
}
|
|
44
|
+
const structured = result.structuredContent;
|
|
45
|
+
const remoteInspection = structured?.inspection;
|
|
46
|
+
if (!remoteInspection || remoteInspection.kind !== "workspace") {
|
|
47
|
+
throw new Error(`Remote ForgeRelay ${resolved.alias} inspection did not return a Workspace projection.`);
|
|
48
|
+
}
|
|
49
|
+
const root = stringField(remoteInspection, "root", "Remote Workspace inspection");
|
|
50
|
+
const mode = remoteInspection.mode;
|
|
51
|
+
if (mode !== "checkout" && mode !== "worktree") {
|
|
52
|
+
throw new Error(`Remote ForgeRelay ${resolved.alias} inspection did not return a valid Workspace mode.`);
|
|
53
|
+
}
|
|
54
|
+
const projection = {
|
|
34
55
|
workspaceId: route.gatewayWorkspaceId,
|
|
35
56
|
kind: "workspace",
|
|
36
57
|
location: "relay",
|
|
37
|
-
root
|
|
58
|
+
root,
|
|
38
59
|
routeState: "known",
|
|
39
|
-
mode
|
|
40
|
-
...(route.sourceRoot ? { sourceRoot: route.sourceRoot } : {}),
|
|
60
|
+
mode,
|
|
41
61
|
relay: resolved.alias,
|
|
42
62
|
executionLocation: `remote:${resolved.alias}`,
|
|
43
63
|
};
|
|
64
|
+
copyStringField(remoteInspection, projection, "status");
|
|
65
|
+
const state = remoteInspection.state;
|
|
66
|
+
if (state === "active" || state === "stale" || state === "invalid" || state === "closed") {
|
|
67
|
+
projection.state = state;
|
|
68
|
+
}
|
|
69
|
+
copyStringField(remoteInspection, projection, "sourceRoot");
|
|
70
|
+
copyStringField(remoteInspection, projection, "branch");
|
|
71
|
+
copyStringField(remoteInspection, projection, "targetBranch");
|
|
72
|
+
copyBooleanField(remoteInspection, projection, "managed");
|
|
73
|
+
copyStringField(remoteInspection, projection, "createdAt");
|
|
74
|
+
copyStringField(remoteInspection, projection, "lastUsedAt");
|
|
75
|
+
copyNumberField(remoteInspection, projection, "idleMs");
|
|
76
|
+
copyBooleanField(remoteInspection, projection, "rootValid");
|
|
77
|
+
const taskSummary = safeTaskSummary(remoteInspection.taskSummary);
|
|
78
|
+
if (taskSummary)
|
|
79
|
+
projection.taskSummary = taskSummary;
|
|
80
|
+
return projection;
|
|
44
81
|
}
|
|
45
82
|
async openWorkspace(alias, input, conversationScopeId) {
|
|
46
83
|
const resolved = this.remoteByAlias(alias);
|
|
@@ -66,18 +103,15 @@ export class RemoteWorkspaceRelay {
|
|
|
66
103
|
if (mode !== "checkout" && mode !== "worktree") {
|
|
67
104
|
throw new Error("Remote open_workspace response did not include a valid workspace mode.");
|
|
68
105
|
}
|
|
69
|
-
const gatewayWorkspaceId = this.allocateGatewayWorkspaceId();
|
|
70
106
|
const sourceRoot = typeof structured?.sourceRoot === "string" ? structured.sourceRoot : undefined;
|
|
71
|
-
const route = {
|
|
72
|
-
gatewayWorkspaceId,
|
|
107
|
+
const route = this.findOrCreateRoute({
|
|
73
108
|
remoteInstanceId: resolved.remote.instanceId,
|
|
74
109
|
remoteWorkspaceId,
|
|
75
110
|
root,
|
|
76
111
|
mode,
|
|
77
112
|
...(sourceRoot ? { sourceRoot } : {}),
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
this.persistRoute(route);
|
|
113
|
+
});
|
|
114
|
+
const gatewayWorkspaceId = route.gatewayWorkspaceId;
|
|
81
115
|
const remapContext = (value) => replaceExactWorkspaceId(value, remoteWorkspaceId, gatewayWorkspaceId);
|
|
82
116
|
const remoteInstruction = typeof structured?.instruction === "string"
|
|
83
117
|
? String(remapContext(structured.instruction))
|
|
@@ -117,10 +151,24 @@ export class RemoteWorkspaceRelay {
|
|
|
117
151
|
};
|
|
118
152
|
}
|
|
119
153
|
async resumeWorkspace(gatewayWorkspaceId, context = "auto", conversationScopeId) {
|
|
154
|
+
const route = this.requireRoute(gatewayWorkspaceId);
|
|
120
155
|
const result = await this.callWorkspaceTool(gatewayWorkspaceId, "open_workspace", { context }, conversationScopeId);
|
|
121
156
|
if (result.isError === true) {
|
|
122
157
|
throw new Error(`Remote open_workspace failed: ${toolResultText(result)}`);
|
|
123
158
|
}
|
|
159
|
+
const structured = result.structuredContent;
|
|
160
|
+
const root = typeof structured?.root === "string" ? structured.root : route.root;
|
|
161
|
+
const mode = structured?.mode === "checkout" || structured?.mode === "worktree"
|
|
162
|
+
? structured.mode
|
|
163
|
+
: route.mode;
|
|
164
|
+
const sourceRoot = typeof structured?.sourceRoot === "string" ? structured.sourceRoot : route.sourceRoot;
|
|
165
|
+
this.findOrCreateRoute({
|
|
166
|
+
remoteInstanceId: route.remoteInstanceId,
|
|
167
|
+
remoteWorkspaceId: route.remoteWorkspaceId,
|
|
168
|
+
root,
|
|
169
|
+
mode,
|
|
170
|
+
...(sourceRoot ? { sourceRoot } : {}),
|
|
171
|
+
});
|
|
124
172
|
return result;
|
|
125
173
|
}
|
|
126
174
|
async read(gatewayWorkspaceId, input, conversationScopeId) {
|
|
@@ -221,14 +269,16 @@ export class RemoteWorkspaceRelay {
|
|
|
221
269
|
throw sanitizedRemoteError(error, route.remoteWorkspaceId, gatewayWorkspaceId);
|
|
222
270
|
}
|
|
223
271
|
}
|
|
224
|
-
async closeWorkspace(gatewayWorkspaceId,
|
|
272
|
+
async closeWorkspace(gatewayWorkspaceId, input = {}, conversationScopeId) {
|
|
225
273
|
const route = this.requireRoute(gatewayWorkspaceId);
|
|
226
274
|
const resolved = this.remoteByInstance(route.remoteInstanceId);
|
|
275
|
+
const action = input.action ?? "close";
|
|
227
276
|
let result;
|
|
228
277
|
try {
|
|
229
278
|
result = await this.callRemoteTool(resolved.alias, resolved.remote, "close_workspace", {
|
|
230
279
|
workspaceId: route.remoteWorkspaceId,
|
|
231
|
-
|
|
280
|
+
action,
|
|
281
|
+
...(input.commitMessage !== undefined ? { commitMessage: input.commitMessage } : {}),
|
|
232
282
|
}, conversationScopeId);
|
|
233
283
|
assertRemoteToolSucceeded(resolved.alias, "close_workspace", result);
|
|
234
284
|
}
|
|
@@ -236,21 +286,26 @@ export class RemoteWorkspaceRelay {
|
|
|
236
286
|
throw sanitizedRemoteError(error, route.remoteWorkspaceId, gatewayWorkspaceId);
|
|
237
287
|
}
|
|
238
288
|
const remoteStructured = result.structuredContent;
|
|
239
|
-
|
|
240
|
-
|
|
289
|
+
if (action === "delete") {
|
|
290
|
+
this.routes.delete(gatewayWorkspaceId);
|
|
291
|
+
this.deletePersistedRoute(gatewayWorkspaceId);
|
|
292
|
+
}
|
|
241
293
|
for (const [turnId, routedWorkspaceId] of this.turnRoutes) {
|
|
242
294
|
if (routedWorkspaceId === gatewayWorkspaceId)
|
|
243
295
|
this.turnRoutes.delete(turnId);
|
|
244
296
|
}
|
|
297
|
+
const actionText = action === "delete" ? "Deleted" : "Closed";
|
|
245
298
|
const text = route.mode === "worktree"
|
|
246
|
-
?
|
|
247
|
-
:
|
|
299
|
+
? `${actionText} relayed worktree workspace ${gatewayWorkspaceId} on remote ${resolved.alias}.`
|
|
300
|
+
: `${actionText} relayed checkout workspace ${gatewayWorkspaceId} on remote ${resolved.alias}.`;
|
|
248
301
|
const structuredContent = {
|
|
249
302
|
result: text,
|
|
250
303
|
workspaceId: gatewayWorkspaceId,
|
|
304
|
+
action,
|
|
251
305
|
mode: route.mode,
|
|
252
306
|
};
|
|
253
307
|
for (const field of [
|
|
308
|
+
"status",
|
|
254
309
|
"sourceRoot",
|
|
255
310
|
"branch",
|
|
256
311
|
"targetBranch",
|
|
@@ -270,6 +325,7 @@ export class RemoteWorkspaceRelay {
|
|
|
270
325
|
tool: "close_workspace",
|
|
271
326
|
card: {
|
|
272
327
|
workspaceId: gatewayWorkspaceId,
|
|
328
|
+
action,
|
|
273
329
|
mode: route.mode,
|
|
274
330
|
payload: { content: [{ type: "text", text }] },
|
|
275
331
|
},
|
|
@@ -292,10 +348,26 @@ export class RemoteWorkspaceRelay {
|
|
|
292
348
|
this.routes.set(workspaceId, route);
|
|
293
349
|
}
|
|
294
350
|
}
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
351
|
+
findOrCreateRoute(input) {
|
|
352
|
+
let selected;
|
|
353
|
+
mkdirSync(this.routeStateDir, { recursive: true });
|
|
354
|
+
this.withRouteFileLock(() => {
|
|
355
|
+
const routes = this.readRoutesFromDisk();
|
|
356
|
+
const existing = [...routes.values()]
|
|
357
|
+
.filter((route) => route.remoteInstanceId === input.remoteInstanceId &&
|
|
358
|
+
route.remoteWorkspaceId === input.remoteWorkspaceId)
|
|
359
|
+
.sort((left, right) => left.gatewayWorkspaceId.localeCompare(right.gatewayWorkspaceId))[0];
|
|
360
|
+
selected = {
|
|
361
|
+
gatewayWorkspaceId: existing?.gatewayWorkspaceId ?? this.allocateGatewayWorkspaceId(routes),
|
|
362
|
+
...input,
|
|
363
|
+
};
|
|
364
|
+
routes.set(selected.gatewayWorkspaceId, selected);
|
|
365
|
+
this.writeRoutesToDisk(routes);
|
|
298
366
|
});
|
|
367
|
+
if (!selected)
|
|
368
|
+
throw new Error("Failed to persist relayed Workspace route.");
|
|
369
|
+
this.routes.set(selected.gatewayWorkspaceId, selected);
|
|
370
|
+
return selected;
|
|
299
371
|
}
|
|
300
372
|
deletePersistedRoute(workspaceId) {
|
|
301
373
|
this.updatePersistedRoutes((routes) => {
|
|
@@ -423,11 +495,11 @@ export class RemoteWorkspaceRelay {
|
|
|
423
495
|
writeForgeRelayRemote(alias, refreshed, this.authEnv);
|
|
424
496
|
return refreshed;
|
|
425
497
|
}
|
|
426
|
-
allocateGatewayWorkspaceId() {
|
|
498
|
+
allocateGatewayWorkspaceId(routes = this.routes) {
|
|
427
499
|
let workspaceId;
|
|
428
500
|
do {
|
|
429
501
|
workspaceId = `rws_${randomBytes(5).toString("hex")}`;
|
|
430
|
-
} while (
|
|
502
|
+
} while (routes.has(workspaceId));
|
|
431
503
|
return workspaceId;
|
|
432
504
|
}
|
|
433
505
|
}
|
|
@@ -443,6 +515,62 @@ function stringField(structured, field, label) {
|
|
|
443
515
|
}
|
|
444
516
|
return value;
|
|
445
517
|
}
|
|
518
|
+
function copyStringField(source, target, field) {
|
|
519
|
+
const value = source[field];
|
|
520
|
+
if (typeof value === "string")
|
|
521
|
+
target[field] = value;
|
|
522
|
+
}
|
|
523
|
+
function copyBooleanField(source, target, field) {
|
|
524
|
+
const value = source[field];
|
|
525
|
+
if (typeof value === "boolean")
|
|
526
|
+
target[field] = value;
|
|
527
|
+
}
|
|
528
|
+
function copyNumberField(source, target, field) {
|
|
529
|
+
const value = source[field];
|
|
530
|
+
if (typeof value === "number" && Number.isFinite(value) && value >= 0)
|
|
531
|
+
target[field] = value;
|
|
532
|
+
}
|
|
533
|
+
function safeTaskSummary(value) {
|
|
534
|
+
if (!value || typeof value !== "object")
|
|
535
|
+
return undefined;
|
|
536
|
+
const summary = value;
|
|
537
|
+
if (summary.level !== "summary" ||
|
|
538
|
+
summary.version !== 1 ||
|
|
539
|
+
typeof summary.revision !== "number" ||
|
|
540
|
+
!Number.isInteger(summary.revision) ||
|
|
541
|
+
summary.revision < 0 ||
|
|
542
|
+
!Array.isArray(summary.lists)) {
|
|
543
|
+
return undefined;
|
|
544
|
+
}
|
|
545
|
+
const lists = [];
|
|
546
|
+
for (const value of summary.lists) {
|
|
547
|
+
if (!value || typeof value !== "object")
|
|
548
|
+
return undefined;
|
|
549
|
+
const list = value;
|
|
550
|
+
if (typeof list.id !== "string" ||
|
|
551
|
+
typeof list.name !== "string" ||
|
|
552
|
+
(list.state !== "active" && list.state !== "archived") ||
|
|
553
|
+
typeof list.revision !== "number" || !Number.isInteger(list.revision) || list.revision <= 0 ||
|
|
554
|
+
typeof list.taskCount !== "number" || !Number.isInteger(list.taskCount) || list.taskCount < 0 ||
|
|
555
|
+
typeof list.unfinishedTaskCount !== "number" || !Number.isInteger(list.unfinishedTaskCount) || list.unfinishedTaskCount < 0) {
|
|
556
|
+
return undefined;
|
|
557
|
+
}
|
|
558
|
+
lists.push({
|
|
559
|
+
id: list.id,
|
|
560
|
+
name: list.name,
|
|
561
|
+
state: list.state,
|
|
562
|
+
revision: list.revision,
|
|
563
|
+
taskCount: list.taskCount,
|
|
564
|
+
unfinishedTaskCount: list.unfinishedTaskCount,
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
return {
|
|
568
|
+
level: "summary",
|
|
569
|
+
version: 1,
|
|
570
|
+
revision: summary.revision,
|
|
571
|
+
lists,
|
|
572
|
+
};
|
|
573
|
+
}
|
|
446
574
|
function toolResultText(result) {
|
|
447
575
|
return (result.content ?? [])
|
|
448
576
|
.filter((entry) => entry.type === "text")
|
package/dist/server.js
CHANGED
|
@@ -311,8 +311,18 @@ const workspaceInspectionOutputSchema = z.union([
|
|
|
311
311
|
location: z.literal("relay"),
|
|
312
312
|
root: z.string(),
|
|
313
313
|
routeState: z.literal("known"),
|
|
314
|
+
status: z.string().optional(),
|
|
315
|
+
state: z.enum(["active", "stale", "invalid", "closed"]).optional(),
|
|
314
316
|
mode: z.enum(["checkout", "worktree"]),
|
|
315
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(),
|
|
316
326
|
relay: z.string(),
|
|
317
327
|
executionLocation: z.string(),
|
|
318
328
|
}),
|
|
@@ -1332,10 +1342,13 @@ function registerProcessTools(server, config, workspaces, processSessions, hooks
|
|
|
1332
1342
|
}
|
|
1333
1343
|
export function createMcpServer(config, workspaces, reviewCheckpoints, processSessions, subagentProviders, incomingArtifactAdapters, codeIntelligence, activityLifecycle, bashOutputStore, activityQueries, options = {}) {
|
|
1334
1344
|
const connectionScopeId = `mcp-connection:${randomUUID()}`;
|
|
1335
|
-
const remoteWorkspaces =
|
|
1336
|
-
|
|
1345
|
+
const remoteWorkspaces = options.remoteWorkspaces
|
|
1346
|
+
?? new RemoteWorkspaceRelay(config.configDir, config.stateDir);
|
|
1347
|
+
const compositeWorkspaces = options.compositeWorkspaces
|
|
1348
|
+
?? new CompositeWorkspaceRegistry(config.stateDir);
|
|
1337
1349
|
const workspaceTasks = new WorkspaceTaskStore(config.stateDir);
|
|
1338
|
-
const taskReminders =
|
|
1350
|
+
const taskReminders = options.taskReminders
|
|
1351
|
+
?? new WorkspaceTaskReminderTracker(config.taskReminderInterval, workspaceTasks);
|
|
1339
1352
|
const compositeTaskGuides = loadCapabilityGuides(config).filter((guide) => guide.name === "workspace-tasks");
|
|
1340
1353
|
const compositeActivity = new CompositeActivityCoordinator(compositeWorkspaces, activityQueries, remoteWorkspaces);
|
|
1341
1354
|
const resolveExecutionTarget = (workspaceId, memberName) => {
|
|
@@ -2349,16 +2362,29 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
2349
2362
|
};
|
|
2350
2363
|
const inspectCompositeMember = async (entry) => {
|
|
2351
2364
|
if (remoteWorkspaces.has(entry.workspaceId)) {
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
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
|
+
}
|
|
2362
2388
|
}
|
|
2363
2389
|
try {
|
|
2364
2390
|
const inspected = await workspaces.inspectWorkspace(entry.workspaceId);
|
|
@@ -2412,7 +2438,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
2412
2438
|
};
|
|
2413
2439
|
}
|
|
2414
2440
|
else if (remoteWorkspaces.has(workspaceId)) {
|
|
2415
|
-
inspection = remoteWorkspaces.inspectWorkspace(workspaceId);
|
|
2441
|
+
inspection = await remoteWorkspaces.inspectWorkspace(workspaceId);
|
|
2416
2442
|
}
|
|
2417
2443
|
else {
|
|
2418
2444
|
const inspected = await workspaces.inspectWorkspace(workspaceId);
|
|
@@ -2428,9 +2454,11 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
2428
2454
|
inspection.kind === "composite"
|
|
2429
2455
|
? `State: ${inspection.state}; members=${inspection.members.length}.`
|
|
2430
2456
|
: inspection.location === "relay"
|
|
2431
|
-
?
|
|
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}.`
|
|
2432
2460
|
: `State: ${inspection.state}; mode=${inspection.mode}; location=${inspection.location}.`,
|
|
2433
|
-
"Task summary is included only when durable
|
|
2461
|
+
"Task summary is included only when durable Task state already exists on the owning Workspace; Task bodies are never returned.",
|
|
2434
2462
|
instruction,
|
|
2435
2463
|
].join("\n");
|
|
2436
2464
|
logToolCall(config, {
|
|
@@ -2754,10 +2782,16 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
2754
2782
|
if (name !== undefined || memberName !== undefined) {
|
|
2755
2783
|
throw new Error("open_workspace name/memberName are only valid for a Composite Workspace.");
|
|
2756
2784
|
}
|
|
2757
|
-
if (
|
|
2758
|
-
if (
|
|
2759
|
-
|
|
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.");
|
|
2760
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) {
|
|
2761
2795
|
if (!path)
|
|
2762
2796
|
throw new Error("Relayed open_workspace requires path.");
|
|
2763
2797
|
const opened = await remoteWorkspaces.openWorkspace(relay, {
|
|
@@ -3306,7 +3340,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
3306
3340
|
});
|
|
3307
3341
|
registerAppTool(server, toolNames.closeWorkspace, {
|
|
3308
3342
|
title: "Close workspace",
|
|
3309
|
-
description: "Close or explicitly delete one Workspace after the user chooses cleanup. action=close (default) preserves checkout, managed-worktree, and
|
|
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.",
|
|
3310
3344
|
inputSchema: {
|
|
3311
3345
|
workspaceId: z.string().describe("Workspace identifier to close or delete."),
|
|
3312
3346
|
action: z
|
|
@@ -3393,10 +3427,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
3393
3427
|
};
|
|
3394
3428
|
}
|
|
3395
3429
|
if (remoteWorkspaces.has(workspaceId)) {
|
|
3396
|
-
|
|
3397
|
-
throw new Error("close_workspace action=delete is not available for relayed Workspaces until Workspace Relay lifecycle parity is implemented.");
|
|
3398
|
-
}
|
|
3399
|
-
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));
|
|
3400
3431
|
workspacePanelStates.delete(workspaceId);
|
|
3401
3432
|
return response;
|
|
3402
3433
|
}
|
|
@@ -4226,6 +4257,10 @@ export function createServer(config = loadConfig(), options = {}) {
|
|
|
4226
4257
|
});
|
|
4227
4258
|
const workspaceStore = createWorkspaceStore(config.stateDir);
|
|
4228
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);
|
|
4229
4264
|
const activityAuditStore = new ActivityAuditStore(config.stateDir);
|
|
4230
4265
|
const bashOutputStore = new BashOutputStore(config.stateDir);
|
|
4231
4266
|
const hostTurnStore = new HostTurnStore(config.stateDir);
|
|
@@ -4420,7 +4455,11 @@ export function createServer(config = loadConfig(), options = {}) {
|
|
|
4420
4455
|
});
|
|
4421
4456
|
}
|
|
4422
4457
|
};
|
|
4423
|
-
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
|
+
});
|
|
4424
4463
|
await server.connect(transport);
|
|
4425
4464
|
}
|
|
4426
4465
|
else {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@akira-tl/forgerelay",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.5",
|
|
4
4
|
"description": "Local development control plane for MCP coding agents.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"homepage": "https://github.com/Akira-TL/forgerelay#readme",
|
|
@@ -40,7 +40,10 @@
|
|
|
40
40
|
"dev": "node scripts/debug/serve.mjs",
|
|
41
41
|
"debug:serve": "node scripts/debug/serve.mjs",
|
|
42
42
|
"debug:accept": "node scripts/debug/accept.mjs",
|
|
43
|
+
"debug:accept:relay": "node scripts/debug/relay-accept.mjs",
|
|
43
44
|
"lsp:interop": "node scripts/lsp-interop.mjs",
|
|
45
|
+
"wiki:check": "node scripts/wiki/sync.mjs check",
|
|
46
|
+
"wiki:publish": "node scripts/wiki/sync.mjs publish",
|
|
44
47
|
"ci:verify": "node scripts/ci/verify.mjs",
|
|
45
48
|
"release:parity": "node scripts/release-parity.mjs",
|
|
46
49
|
"release:pack": "node scripts/release/pack.mjs",
|