@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 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
- return {
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: route.root,
58
+ root,
38
59
  routeState: "known",
39
- mode: route.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
- this.routes.set(gatewayWorkspaceId, route);
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, commitMessage, conversationScopeId) {
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
- ...(commitMessage !== undefined ? { commitMessage } : {}),
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
- this.routes.delete(gatewayWorkspaceId);
240
- this.deletePersistedRoute(gatewayWorkspaceId);
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
- ? `Closed relayed worktree workspace ${gatewayWorkspaceId} on remote ${resolved.alias}.`
247
- : `Closed relayed checkout workspace ${gatewayWorkspaceId} on remote ${resolved.alias}.`;
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
- persistRoute(route) {
296
- this.updatePersistedRoutes((routes) => {
297
- routes.set(route.gatewayWorkspaceId, route);
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 (this.routes.has(workspaceId));
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 = new RemoteWorkspaceRelay(config.configDir, config.stateDir);
1336
- const compositeWorkspaces = new CompositeWorkspaceRegistry(config.stateDir);
1345
+ const remoteWorkspaces = options.remoteWorkspaces
1346
+ ?? new RemoteWorkspaceRelay(config.configDir, config.stateDir);
1347
+ const compositeWorkspaces = options.compositeWorkspaces
1348
+ ?? new CompositeWorkspaceRegistry(config.stateDir);
1337
1349
  const workspaceTasks = new WorkspaceTaskStore(config.stateDir);
1338
- const taskReminders = new WorkspaceTaskReminderTracker(config.taskReminderInterval, workspaceTasks);
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
- 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
- };
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
- ? `Route: ${inspection.routeState}; mode=${inspection.mode}; location=${inspection.location}. Remote lifecycle is not probed by inspection.`
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 local Task state already exists; Task bodies are never returned.",
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 (relay !== undefined) {
2758
- if (workspaceId !== undefined) {
2759
- throw new Error("Relayed open_workspace requires a path; resuming a relayed workspace is not available in this tracer bullet.");
2785
+ if (workspaceId !== undefined && remoteWorkspaces.has(workspaceId)) {
2786
+ if (path !== undefined || relay !== undefined || mode !== undefined || baseRef !== undefined ||
2787
+ newWorktree !== undefined || newWorkspace !== undefined) {
2788
+ throw new Error("Resuming a relayed Workspace by workspaceId accepts context only.");
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 Composite identity for later reopen. action=delete permanently removes ForgeRelay-owned state. Managed-worktree-backed Workspaces still finalize safely when active and require commitMessage. Composite delete dissolves only Composite-owned state and never closes member Workspaces. Checkout project files are never deleted; relayed delete remains unavailable.",
3343
+ description: "Close or explicitly delete one Workspace after the user chooses cleanup. action=close (default) preserves checkout, managed-worktree, Composite, and relayed identity for later reopen. action=delete permanently removes ForgeRelay-owned state. Managed-worktree-backed Workspaces still finalize safely when active and require commitMessage. Composite delete dissolves only Composite-owned state and never closes member Workspaces. Checkout project files are never deleted.",
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
- if (action === "delete") {
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.4",
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",