@akira-tl/forgerelay 0.8.9 → 0.8.10

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.
Files changed (45) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/capabilities/code-intelligence/GUIDE.md +7 -3
  3. package/capabilities/subagents/GUIDE.md +9 -0
  4. package/capabilities/workspace/workspace-tasks/GUIDE.md +6 -0
  5. package/dist/cli/init.js +180 -0
  6. package/dist/cli/setup-support.js +65 -0
  7. package/dist/cli.js +9 -94
  8. package/dist/lsp/code-intelligence.js +17 -3
  9. package/dist/lsp/runtime/diagnostic-snapshots.js +52 -1
  10. package/dist/lsp/runtime/managed-language-servers.js +172 -0
  11. package/dist/lsp/runtime/manager.js +37 -2
  12. package/dist/lsp/runtime/process-launch.js +3 -0
  13. package/dist/lsp/test-support/server-fixture.js +5 -2
  14. package/dist/mcp/process/process-platform.js +21 -9
  15. package/dist/mcp/process/process-sessions.js +3 -3
  16. package/dist/mcp/process/tools.js +6 -6
  17. package/dist/mcp/server/core/capability-registry.js +7 -2
  18. package/dist/mcp/server/core/tool-support.js +8 -5
  19. package/dist/mcp/server/operations/runtime/filesystem-tools.js +74 -19
  20. package/dist/mcp/server/operations/runtime/mutation-diagnostics.js +54 -0
  21. package/dist/mcp/server/operations/runtime/operation-runtime.js +8 -7
  22. package/dist/mcp/server/workspace/runtime/workspace-open-presentation.js +28 -8
  23. package/dist/mcp/server/workspace/runtime/workspace-open.js +4 -1
  24. package/dist/mcp/server/workspace/runtime/workspace-tools.js +3 -3
  25. package/dist/mcp/server-instructions.js +1 -1
  26. package/dist/runtime/config/config.js +1 -0
  27. package/dist/runtime/config/user-config.js +1 -10
  28. package/dist/server.js +36 -6
  29. package/dist/workspaces/context.js +35 -0
  30. package/dist/workspaces/relay/transport/remote-transport.js +30 -3
  31. package/dist/workspaces/resources/resource-monitor.js +377 -0
  32. package/dist/workspaces/resources/skills.js +13 -15
  33. package/dist/workspaces/sessions.js +3 -0
  34. package/dist/workspaces.js +11 -0
  35. package/docs/chatgpt-coding-workflow.md +4 -3
  36. package/docs/configuration.md +14 -9
  37. package/package.json +2 -3
  38. package/scripts/release/publish.mjs +12 -9
  39. package/scripts/release/release-gate.test.mjs +3 -1
  40. package/scripts/release/release-version.test.mjs +13 -1
  41. package/scripts/release-parity.mjs +1 -1
  42. package/scripts/release-proof.mjs +15 -0
  43. package/scripts/release-proof.test.mjs +27 -1
  44. package/scripts/release-version.mjs +16 -1
  45. package/skills/subagent-delegation/SKILL.md +0 -132
@@ -1,8 +1,10 @@
1
1
  import { readFileSync } from "node:fs";
2
2
  import { tmpdir } from "node:os";
3
+ import { isAbsolute } from "node:path";
3
4
  import { registerAppTool } from "@modelcontextprotocol/ext-apps/server";
4
5
  import * as z from "zod/v4";
5
6
  import { applyPatch } from "../../../filesystem/apply-patch.js";
7
+ import { readFileTool } from "../../../filesystem/filesystem-tools.js";
6
8
  import { toolNames } from "../../../server-instructions.js";
7
9
  import { executeBulkRead } from "../../../operations/bulk-read.js";
8
10
  import { formatPathForPrompt } from "../../../../workspaces/resources/skills.js";
@@ -10,6 +12,7 @@ import { activityRelationFor, activityRequestFor, runActivityTool, runActivityTo
10
12
  import { workspaceHookInvocation } from "../../core/capability-support.js";
11
13
  import { resultOutputSchema, workspaceAgentsFileOutputSchema } from "../../core/schemas.js";
12
14
  import { contentText, logToolCall, textBlock, toolResultAgentsFiles, toolResultContent, toolResultIsError, toolResultText, workspaceLogContext, } from "../../core/tool-support.js";
15
+ import { appendAutomaticMutationDiagnostics } from "./mutation-diagnostics.js";
13
16
  const WRITE_TOOL_ANNOTATIONS = {
14
17
  readOnlyHint: false,
15
18
  destructiveHint: true,
@@ -23,14 +26,15 @@ const EDIT_TOOL_ANNOTATIONS = {
23
26
  openWorldHint: false,
24
27
  };
25
28
  export function registerFilesystemTools(options) {
26
- const { server, config, workspaces, compositeWorkspaces, compositeTaskGuides, remoteWorkspaces, coreOperations, nativeBulkMutations, activityLifecycle, hooks, toolDescriptions, resolveExecutionTarget, prepareExecutionContext, presentSemanticWorkResult, hostScopeIdFor, } = options;
29
+ const { server, config, workspaces, compositeWorkspaces, compositeTaskGuides, remoteWorkspaces, coreOperations, nativeBulkMutations, activityLifecycle, codeIntelligence, hooks, toolDescriptions, resolveExecutionTarget, prepareExecutionContext, presentSemanticWorkResult, hostScopeIdFor, } = options;
27
30
  registerAppTool(server, toolNames.read, {
28
31
  title: "Read file",
29
32
  description: toolDescriptions.read,
30
33
  inputSchema: {
31
34
  workspaceId: z
32
35
  .string()
33
- .describe("Workspace identifier returned by open_workspace."),
36
+ .optional()
37
+ .describe("Workspace identifier returned by open_workspace. Omit only for read-only inspection of absolute paths already inside configured allowedRoots."),
34
38
  member: z
35
39
  .string()
36
40
  .optional()
@@ -76,6 +80,54 @@ export function registerFilesystemTools(options) {
76
80
  if ((path === undefined) === (paths === undefined)) {
77
81
  throw new Error("read requires exactly one of path or paths.");
78
82
  }
83
+ if (workspaceId === undefined) {
84
+ if (member !== undefined)
85
+ throw new Error("read without workspaceId does not accept member.");
86
+ const requestedPaths = path !== undefined ? [path] : paths;
87
+ for (const requestedPath of requestedPaths) {
88
+ if (!isAbsolute(requestedPath)) {
89
+ throw new Error("read without workspaceId requires absolute paths inside configured allowedRoots.");
90
+ }
91
+ }
92
+ const startedAt = performance.now();
93
+ const children = await Promise.all(requestedPaths.map(async (requestedPath) => {
94
+ const response = await readFileTool({ path: requestedPath, offset, limit }, {
95
+ cwd: process.cwd(),
96
+ root: config.allowedRoots[0] ?? process.cwd(),
97
+ readRoots: config.allowedRoots,
98
+ });
99
+ return {
100
+ path: requestedPath,
101
+ status: response.isError ? "error" : "done",
102
+ response,
103
+ result: contentText(response.content),
104
+ };
105
+ }));
106
+ const failed = children.filter((child) => child.status === "error").length;
107
+ const content = children.flatMap((child) => requestedPaths.length === 1
108
+ ? child.response.content
109
+ : [textBlock(`--- ${child.path} · ${child.status} ---`), ...child.response.content]);
110
+ logToolCall(config, {
111
+ tool: toolNames.read,
112
+ path: requestedPaths.length === 1 ? requestedPaths[0] : `${requestedPaths.length} unscoped files`,
113
+ success: failed === 0,
114
+ durationMs: Math.round(performance.now() - startedAt),
115
+ });
116
+ return {
117
+ content,
118
+ ...(failed > 0 ? { isError: true } : {}),
119
+ structuredContent: {
120
+ result: contentText(content),
121
+ ...(requestedPaths.length > 1
122
+ ? {
123
+ results: children.map(({ path: childPath, status, result }) => ({ path: childPath, status, result })),
124
+ files: children.length,
125
+ failed,
126
+ }
127
+ : {}),
128
+ },
129
+ };
130
+ }
79
131
  if (compositeWorkspaces.has(workspaceId) && member === undefined && path !== undefined) {
80
132
  const guide = compositeTaskGuides.find((candidate) => formatPathForPrompt(candidate.filePath) === path || candidate.filePath === path);
81
133
  if (guide) {
@@ -103,10 +155,10 @@ export function registerFilesystemTools(options) {
103
155
  const executionWorkspaceId = target.executionWorkspaceId;
104
156
  const executionContext = await prepareExecutionContext(target, extra._meta, extra.signal, extra.sessionId);
105
157
  if (remoteWorkspaces.has(executionWorkspaceId)) {
106
- return presentSemanticWorkResult(await remoteWorkspaces.read(executionWorkspaceId, { path, paths, offset, limit }, hostScopeIdFor(extra._meta, extra.sessionId)), target);
158
+ return presentSemanticWorkResult(await remoteWorkspaces.read(executionWorkspaceId, { path, paths, offset, limit }, hostScopeIdFor(extra._meta, extra.sessionId)), target, hostScopeIdFor(extra._meta, extra.sessionId));
107
159
  }
108
160
  if (path !== undefined) {
109
- return presentSemanticWorkResult(await coreOperations.read({ workspaceId: executionWorkspaceId, path, offset, limit }, executionContext), target);
161
+ return presentSemanticWorkResult(await coreOperations.read({ workspaceId: executionWorkspaceId, path, offset, limit }, executionContext), target, hostScopeIdFor(extra._meta, extra.sessionId));
110
162
  }
111
163
  const workspace = workspaces.getWorkspace(executionWorkspaceId);
112
164
  let response;
@@ -159,7 +211,7 @@ export function registerFilesystemTools(options) {
159
211
  : { type: "succeeded" }, activityRelationFor(executionContext));
160
212
  if (!response)
161
213
  throw new Error("Bulk Read completed without a response.");
162
- return presentSemanticWorkResult(response, target);
214
+ return presentSemanticWorkResult(response, target, hostScopeIdFor(extra._meta, extra.sessionId));
163
215
  });
164
216
  if (config.toolMode !== "codex") {
165
217
  registerAppTool(server, toolNames.write, {
@@ -183,9 +235,9 @@ export function registerFilesystemTools(options) {
183
235
  const executionWorkspaceId = target.executionWorkspaceId;
184
236
  const executionContext = await prepareExecutionContext(target, extra._meta, extra.signal, extra.sessionId);
185
237
  if (remoteWorkspaces.has(executionWorkspaceId)) {
186
- return presentSemanticWorkResult(await remoteWorkspaces.write(executionWorkspaceId, input, hostScopeIdFor(extra._meta, extra.sessionId)), target);
238
+ return presentSemanticWorkResult(await remoteWorkspaces.write(executionWorkspaceId, input, hostScopeIdFor(extra._meta, extra.sessionId)), target, hostScopeIdFor(extra._meta, extra.sessionId));
187
239
  }
188
- return presentSemanticWorkResult(await coreOperations.write({ workspaceId: executionWorkspaceId, ...input }, executionContext), target);
240
+ return presentSemanticWorkResult(await coreOperations.write({ workspaceId: executionWorkspaceId, ...input }, executionContext), target, hostScopeIdFor(extra._meta, extra.sessionId));
189
241
  });
190
242
  registerAppTool(server, toolNames.edit, {
191
243
  title: "Edit file",
@@ -236,12 +288,12 @@ export function registerFilesystemTools(options) {
236
288
  const executionWorkspaceId = target.executionWorkspaceId;
237
289
  const executionContext = await prepareExecutionContext(target, extra._meta, extra.signal, extra.sessionId);
238
290
  if (remoteWorkspaces.has(executionWorkspaceId)) {
239
- return presentSemanticWorkResult(await remoteWorkspaces.edit(executionWorkspaceId, { path, paths, edits }, hostScopeIdFor(extra._meta, extra.sessionId)), target);
291
+ return presentSemanticWorkResult(await remoteWorkspaces.edit(executionWorkspaceId, { path, paths, edits }, hostScopeIdFor(extra._meta, extra.sessionId)), target, hostScopeIdFor(extra._meta, extra.sessionId));
240
292
  }
241
293
  if (path !== undefined) {
242
- return presentSemanticWorkResult(await coreOperations.edit({ workspaceId: executionWorkspaceId, path, edits }, executionContext), target);
294
+ return presentSemanticWorkResult(await coreOperations.edit({ workspaceId: executionWorkspaceId, path, edits }, executionContext), target, hostScopeIdFor(extra._meta, extra.sessionId));
243
295
  }
244
- return presentSemanticWorkResult(await nativeBulkMutations.edit({ workspaceId: executionWorkspaceId, paths: paths, edits }, executionContext), target);
296
+ return presentSemanticWorkResult(await nativeBulkMutations.edit({ workspaceId: executionWorkspaceId, paths: paths, edits }, executionContext), target, hostScopeIdFor(extra._meta, extra.sessionId));
245
297
  });
246
298
  }
247
299
  registerAppTool(server, toolNames.rename, {
@@ -265,9 +317,9 @@ export function registerFilesystemTools(options) {
265
317
  const executionWorkspaceId = target.executionWorkspaceId;
266
318
  const executionContext = await prepareExecutionContext(target, extra._meta, extra.signal, extra.sessionId);
267
319
  if (remoteWorkspaces.has(executionWorkspaceId)) {
268
- return presentSemanticWorkResult(await remoteWorkspaces.rename(executionWorkspaceId, { path, newPath }, hostScopeIdFor(extra._meta, extra.sessionId)), target);
320
+ return presentSemanticWorkResult(await remoteWorkspaces.rename(executionWorkspaceId, { path, newPath }, hostScopeIdFor(extra._meta, extra.sessionId)), target, hostScopeIdFor(extra._meta, extra.sessionId));
269
321
  }
270
- return presentSemanticWorkResult(await coreOperations.rename({ workspaceId: executionWorkspaceId, path, newPath }, executionContext), target);
322
+ return presentSemanticWorkResult(await coreOperations.rename({ workspaceId: executionWorkspaceId, path, newPath }, executionContext), target, hostScopeIdFor(extra._meta, extra.sessionId));
271
323
  });
272
324
  registerAppTool(server, toolNames.delete, {
273
325
  title: "Delete path",
@@ -308,12 +360,12 @@ export function registerFilesystemTools(options) {
308
360
  const executionWorkspaceId = target.executionWorkspaceId;
309
361
  const executionContext = await prepareExecutionContext(target, extra._meta, extra.signal, extra.sessionId);
310
362
  if (remoteWorkspaces.has(executionWorkspaceId)) {
311
- return presentSemanticWorkResult(await remoteWorkspaces.delete(executionWorkspaceId, { path, paths, recursive }, hostScopeIdFor(extra._meta, extra.sessionId)), target);
363
+ return presentSemanticWorkResult(await remoteWorkspaces.delete(executionWorkspaceId, { path, paths, recursive }, hostScopeIdFor(extra._meta, extra.sessionId)), target, hostScopeIdFor(extra._meta, extra.sessionId));
312
364
  }
313
365
  if (path !== undefined) {
314
- return presentSemanticWorkResult(await coreOperations.delete({ workspaceId: executionWorkspaceId, path, recursive }, executionContext), target);
366
+ return presentSemanticWorkResult(await coreOperations.delete({ workspaceId: executionWorkspaceId, path, recursive }, executionContext), target, hostScopeIdFor(extra._meta, extra.sessionId));
315
367
  }
316
- return presentSemanticWorkResult(await nativeBulkMutations.delete({ workspaceId: executionWorkspaceId, paths: paths, recursive }, executionContext), target);
368
+ return presentSemanticWorkResult(await nativeBulkMutations.delete({ workspaceId: executionWorkspaceId, paths: paths, recursive }, executionContext), target, hostScopeIdFor(extra._meta, extra.sessionId));
317
369
  });
318
370
  if (config.toolMode === "codex") {
319
371
  registerAppTool(server, "apply_patch", {
@@ -344,7 +396,7 @@ export function registerFilesystemTools(options) {
344
396
  const executionWorkspaceId = target.executionWorkspaceId;
345
397
  const executionContext = await prepareExecutionContext(target, extra._meta, extra.signal, extra.sessionId);
346
398
  if (remoteWorkspaces.has(executionWorkspaceId)) {
347
- return presentSemanticWorkResult(await remoteWorkspaces.applyPatch(executionWorkspaceId, { patch }, hostScopeIdFor(extra._meta, extra.sessionId)), target);
399
+ return presentSemanticWorkResult(await remoteWorkspaces.applyPatch(executionWorkspaceId, { patch }, hostScopeIdFor(extra._meta, extra.sessionId)), target, hostScopeIdFor(extra._meta, extra.sessionId));
348
400
  }
349
401
  const workspace = workspaces.getWorkspace(executionWorkspaceId);
350
402
  return runActivityToolWithHooks(activityLifecycle, hooks, workspace, hostScopeIdFor(extra._meta, extra.sessionId), activityRequestFor({ workspaceId: executionWorkspaceId, patch }, executionContext), {
@@ -363,6 +415,9 @@ export function registerFilesystemTools(options) {
363
415
  const displayPath = applied.files.length === 1
364
416
  ? applied.files[0]?.path
365
417
  : `${applied.files.length} files`;
418
+ const diagnosticPaths = applied.files
419
+ .filter((file) => file.operation !== "delete")
420
+ .map((file) => file.path);
366
421
  logToolCall(config, {
367
422
  tool: "apply_patch",
368
423
  ...workspaceLogContext(workspace, extra.sessionId),
@@ -370,7 +425,7 @@ export function registerFilesystemTools(options) {
370
425
  success: true,
371
426
  durationMs: Math.round(performance.now() - startedAt),
372
427
  });
373
- return {
428
+ return appendAutomaticMutationDiagnostics({
374
429
  content,
375
430
  _meta: {
376
431
  tool: "apply_patch",
@@ -392,9 +447,9 @@ export function registerFilesystemTools(options) {
392
447
  removals: applied.removals,
393
448
  files: applied.files,
394
449
  },
395
- };
450
+ }, codeIntelligence, workspace.root, diagnosticPaths, extra.signal);
396
451
  },
397
- }, activityRelationFor(executionContext)).then((result) => presentSemanticWorkResult(result, target));
452
+ }, activityRelationFor(executionContext)).then((result) => presentSemanticWorkResult(result, target, hostScopeIdFor(extra._meta, extra.sessionId)));
398
453
  });
399
454
  }
400
455
  }
@@ -0,0 +1,54 @@
1
+ import { CodeIntelligenceError } from "../../../../lsp/code-intelligence-error.js";
2
+ import { textBlock } from "../../core/tool-support.js";
3
+ const AUTO_DIAGNOSTIC_LIMIT = 20;
4
+ export async function appendAutomaticMutationDiagnostics(result, codeIntelligence, workspaceRoot, paths, signal) {
5
+ const summary = await automaticMutationDiagnostics(codeIntelligence, workspaceRoot, paths, signal);
6
+ if (!summary)
7
+ return result;
8
+ return {
9
+ ...result,
10
+ content: [...result.content, textBlock(summary)],
11
+ structuredContent: {
12
+ ...result.structuredContent,
13
+ result: `${result.structuredContent.result}\n\n${summary}`,
14
+ },
15
+ };
16
+ }
17
+ export async function automaticMutationDiagnostics(codeIntelligence, workspaceRoot, paths, signal) {
18
+ const sections = [];
19
+ for (const path of Array.from(new Set(paths))) {
20
+ try {
21
+ const result = await codeIntelligence.run(workspaceRoot, { operation: "diagnostics", path, limit: AUTO_DIAGNOSTIC_LIMIT }, { signal });
22
+ if (result.operation !== "diagnostics" || result.diagnostics.length === 0)
23
+ continue;
24
+ sections.push(formatDiagnostics(result));
25
+ }
26
+ catch (error) {
27
+ if (error instanceof CodeIntelligenceError) {
28
+ if (error.code === "code.language_service_unavailable" || error.code === "code.operation_unsupported")
29
+ continue;
30
+ sections.push(`Automatic diagnostics warning for ${path}: ${error.message}`);
31
+ continue;
32
+ }
33
+ const message = error instanceof Error ? error.message : String(error);
34
+ sections.push(`Automatic diagnostics warning for ${path}: ${message}`);
35
+ }
36
+ }
37
+ if (sections.length === 0)
38
+ return undefined;
39
+ return [
40
+ "Automatic Language Server validation after this file change:",
41
+ ...sections,
42
+ ].join("\n");
43
+ }
44
+ function formatDiagnostics(result) {
45
+ const lines = result.diagnostics.map((diagnostic) => {
46
+ const location = `${result.path}:${diagnostic.range.start.line}:${diagnostic.range.start.column}`;
47
+ const severity = diagnostic.severity ? `[${diagnostic.severity}] ` : "";
48
+ const code = diagnostic.code === undefined ? "" : ` (${diagnostic.code})`;
49
+ return `- ${location} ${severity}${diagnostic.message}${code}`;
50
+ });
51
+ if (result.truncated)
52
+ lines.push(`- … more diagnostics were truncated (showing ${result.returned}${result.total === undefined ? "" : ` of ${result.total}`}).`);
53
+ return [`${result.selectedServer} · ${result.path}`, ...lines].join("\n");
54
+ }
@@ -12,8 +12,9 @@ import { capabilityActivityAuditRequest, capabilityActivityAuditResult } from ".
12
12
  import { activityRelationFor, activityRequestFor, runActivityTool, runActivityToolWithHooks, standardActivityOutcome, } from "../../core/activity-support.js";
13
13
  import { capabilityContextFor, workspaceHookInvocation } from "../../core/capability-support.js";
14
14
  import { assertWorkspaceInstructionsLoadedBeforeSideEffect, contentLineCount, contentText, countDiffStats, formatDiscoveredWorkspaceInstructions, logFailedToolResponse, logToolCall, newFilePatch, textBlock, textSummary, toolResultContent, toolResultIsError, toolResultText, workspaceLogContext, } from "../../core/tool-support.js";
15
+ import { appendAutomaticMutationDiagnostics } from "./mutation-diagnostics.js";
15
16
  export function createOperationRuntime(options) {
16
- const { config, workspaces, activityLifecycle, hooks, processSessions, bashOutputStore, capabilityRegistry, hostScopeIdFor } = options;
17
+ const { config, workspaces, activityLifecycle, hooks, processSessions, bashOutputStore, capabilityRegistry, codeIntelligence, hostScopeIdFor, } = options;
17
18
  const coreOperations = createCoreOperationExecutor({
18
19
  read: async (input, context) => {
19
20
  const { workspaceId, ...readInput } = input;
@@ -127,7 +128,7 @@ export function createOperationRuntime(options) {
127
128
  success: true,
128
129
  durationMs: Math.round(performance.now() - startedAt),
129
130
  });
130
- return {
131
+ return appendAutomaticMutationDiagnostics({
131
132
  ...response,
132
133
  _meta: {
133
134
  tool: toolNames.write,
@@ -144,7 +145,7 @@ export function createOperationRuntime(options) {
144
145
  structuredContent: {
145
146
  result: contentText(response.content),
146
147
  },
147
- };
148
+ }, codeIntelligence, workspace.root, [writeInput.path], context.signal);
148
149
  },
149
150
  }, activityRelationFor(context));
150
151
  },
@@ -188,7 +189,7 @@ export function createOperationRuntime(options) {
188
189
  success: true,
189
190
  durationMs: Math.round(performance.now() - startedAt),
190
191
  });
191
- return {
192
+ return appendAutomaticMutationDiagnostics({
192
193
  content: editContent,
193
194
  _meta: {
194
195
  tool: toolNames.edit,
@@ -206,7 +207,7 @@ export function createOperationRuntime(options) {
206
207
  status: "applied",
207
208
  result: contentText(editContent),
208
209
  },
209
- };
210
+ }, codeIntelligence, workspace.root, [editInput.path], context.signal);
210
211
  },
211
212
  }, activityRelationFor(context));
212
213
  },
@@ -236,7 +237,7 @@ export function createOperationRuntime(options) {
236
237
  success: true,
237
238
  durationMs: Math.round(performance.now() - startedAt),
238
239
  });
239
- return {
240
+ return appendAutomaticMutationDiagnostics({
240
241
  content,
241
242
  _meta: {
242
243
  tool: toolNames.rename,
@@ -253,7 +254,7 @@ export function createOperationRuntime(options) {
253
254
  path,
254
255
  newPath,
255
256
  },
256
- };
257
+ }, codeIntelligence, workspace.root, [newPath], context.signal);
257
258
  }
258
259
  catch (error) {
259
260
  logToolCall(config, {
@@ -9,15 +9,27 @@ import { formatUnavailableSubagentProvider } from "../../../../subagents/provide
9
9
  import { capabilityContextFor } from "../../core/capability-support.js";
10
10
  import { redactSkillDiagnosticPaths } from "../../core/schemas.js";
11
11
  import { logToolCall, workspaceLogContext } from "../../core/tool-support.js";
12
+ export const workspaceTaskUsageInstruction = "Use workspace.tasks proactively for work that spans multiple steps or sessions: create or update Tasks to preserve useful next steps and current state, and read its summary when resuming relevant unfinished work. Do not query it mechanically on every open_workspace call.";
12
13
  export async function presentLocalWorkspaceOpen(options, input, contextData) {
13
14
  const { config, forgerelayVersion: FORGERELAY_VERSION, workspaces, workspaceTasks, reviewCheckpoints, capabilityRegistry, subagentProviders, hooks, rememberWorkspacePanelState, } = options;
14
15
  const { path, workspaceId, mode, baseRef, newWorktree, newWorkspace, context } = input;
15
16
  const { conversationScopeId, protectedWorkspaceIds, startedAt, sessionId } = contextData;
16
- const { workspace, agentsFiles, availableAgentsFiles, hookReports, workspaceReused, includeBootstrapContext, bootstrapContextComponents, contextFingerprint, } = await workspaces.openWorkspace({ path, workspaceId, mode, baseRef, newWorktree, newWorkspace, context }, {
17
+ const { workspace, agentsFiles, availableAgentsFiles, hookReports, workspaceReused, bootstrapContextComponents, contextFingerprint, } = await workspaces.openWorkspace({ path, workspaceId, mode, baseRef, newWorktree, newWorkspace, context }, {
17
18
  conversationScopeId,
18
19
  protectedWorkspaceIds,
19
20
  });
20
21
  workspaceTasks.initializeWorkspace(workspace.id);
22
+ const requestedContext = context ?? "auto";
23
+ const resourceUpdate = workspaceReused && requestedContext === "auto"
24
+ ? workspaces.claimResourceUpdates(workspace.id, conversationScopeId)
25
+ : undefined;
26
+ if (requestedContext === "full" || (!workspaceReused && requestedContext !== "none")) {
27
+ workspaces.acknowledgeResourceUpdates(workspace.id, conversationScopeId);
28
+ }
29
+ const effectiveBootstrapContextComponents = resourceUpdate
30
+ ? bootstrapContextComponents.filter((component) => !resourceUpdate.coveredComponents.includes(component))
31
+ : bootstrapContextComponents;
32
+ const effectiveIncludeBootstrapContext = effectiveBootstrapContextComponents.length > 0 || Boolean(resourceUpdate);
21
33
  const knownWorktrees = await workspaces.listKnownWorktrees(workspace);
22
34
  const staleWorkspaces = await workspaces.listStaleWorkspaces(workspace);
23
35
  const capabilityFingerprint = buildCapabilityFingerprint(config, FORGERELAY_VERSION, {
@@ -59,7 +71,7 @@ export async function presentLocalWorkspaceOpen(options, input, contextData) {
59
71
  const cardAvailableAgentsFiles = availableAgentsFiles.map((file) => ({
60
72
  path: formatAgentsPath(file.path, workspace.root),
61
73
  }));
62
- const bootstrapComponents = new Set(bootstrapContextComponents);
74
+ const bootstrapComponents = new Set(effectiveBootstrapContextComponents);
63
75
  const visibleSkills = bootstrapComponents.has("skills") ? cardSkills : [];
64
76
  const visibleSkillDiagnostics = bootstrapComponents.has("skillDiagnostics")
65
77
  ? redactSkillDiagnosticPaths(workspace.skillDiagnostics)
@@ -72,16 +84,23 @@ export async function presentLocalWorkspaceOpen(options, input, contextData) {
72
84
  ? cardAvailableAgentsFiles
73
85
  : [];
74
86
  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.";
75
- 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.";
87
+ const workspaceManagementInstruction = [
88
+ "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.",
89
+ capabilityCatalog.some((entry) => entry.name === "workspace.tasks")
90
+ ? workspaceTaskUsageInstruction
91
+ : undefined,
92
+ ].filter(Boolean).join(" ");
76
93
  const cardInstruction = config.skillsEnabled
77
94
  ? `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}`
78
95
  : `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}`;
79
96
  const instruction = workspaceReused
80
- ? includeBootstrapContext
97
+ ? effectiveIncludeBootstrapContext
81
98
  ? [
82
99
  `Workspace already exists as ${workspace.id} for this directory.`,
83
100
  "Reuse this workspaceId for subsequent tool calls.",
84
- `Project bootstrap context components included in this response: ${bootstrapContextComponents.join(", ")}. Components not listed are unchanged and are not repeated.`,
101
+ effectiveBootstrapContextComponents.length > 0
102
+ ? `Project bootstrap context components included in this response: ${effectiveBootstrapContextComponents.join(", ")}. Components not listed are unchanged and are not repeated.`
103
+ : "Only Workspace context deltas are included in this response; unchanged bootstrap context is not repeated.",
85
104
  workspaceContextInstruction,
86
105
  workspaceManagementInstruction,
87
106
  ].join("\n\n")
@@ -93,7 +112,7 @@ export async function presentLocalWorkspaceOpen(options, input, contextData) {
93
112
  workspaceManagementInstruction,
94
113
  ].join("\n\n")
95
114
  : workspace.mode === "worktree"
96
- ? "Use this workspaceId for subsequent tool calls. Follow the project instructions, nested instruction files, skills, agent profiles, and diagnostics returned for this isolated worktree."
115
+ ? `Use this workspaceId for subsequent tool calls. Follow the project instructions, nested instruction files, skills, agent profiles, and diagnostics returned for this isolated worktree. ${workspaceManagementInstruction}`
97
116
  : cardInstruction;
98
117
  const resultContent = [
99
118
  {
@@ -138,6 +157,7 @@ export async function presentLocalWorkspaceOpen(options, input, contextData) {
138
157
  : undefined,
139
158
  `ForgeRelay ${capabilityFingerprint.version} capabilities: ${capabilityFingerprint.capabilities.join(", ")}`,
140
159
  instruction,
160
+ resourceUpdate?.text,
141
161
  ].filter(Boolean).join("\n"),
142
162
  },
143
163
  ];
@@ -155,7 +175,7 @@ export async function presentLocalWorkspaceOpen(options, input, contextData) {
155
175
  path: workspace.root,
156
176
  mode: workspace.mode,
157
177
  workspaceReused,
158
- includeBootstrapContext,
178
+ includeBootstrapContext: effectiveIncludeBootstrapContext,
159
179
  sourceRoot: workspace.sourceRoot,
160
180
  worktree: workspace.worktree,
161
181
  worktrees: knownWorktrees,
@@ -183,7 +203,7 @@ export async function presentLocalWorkspaceOpen(options, input, contextData) {
183
203
  content: resultContent,
184
204
  _meta: {
185
205
  tool: "open_workspace",
186
- card: includeBootstrapContext
206
+ card: effectiveIncludeBootstrapContext
187
207
  ? workspaceCard
188
208
  : compactWorkspacePresentation(workspaceCard),
189
209
  },
@@ -4,7 +4,7 @@ import { formatPathForPrompt } from "../../../../workspaces/resources/skills.js"
4
4
  import { compositeCapabilityContext } from "../../core/capability-support.js";
5
5
  import { logToolCall, textBlock } from "../../core/tool-support.js";
6
6
  import { openWorkspaceToolDefinition } from "./workspace-open-schema.js";
7
- import { presentLocalWorkspaceOpen } from "./workspace-open-presentation.js";
7
+ import { presentLocalWorkspaceOpen, workspaceTaskUsageInstruction, } from "./workspace-open-presentation.js";
8
8
  export function registerOpenWorkspaceTool(options) {
9
9
  const { server } = options;
10
10
  registerAppTool(server, "open_workspace", openWorkspaceToolDefinition, (args, extra) => handleOpenWorkspace(options, args, extra));
@@ -405,6 +405,9 @@ async function handleOpenWorkspace(options, input, extra) {
405
405
  compositeCapabilityCatalog.length > 0
406
406
  ? `Composite-owned capabilities: ${compositeCapabilityCatalog.map((entry) => entry.name).join(", ")}. Use these without member because their state belongs to the Composite Workspace itself.`
407
407
  : undefined,
408
+ compositeCapabilityCatalog.some((entry) => entry.name === "workspace.tasks")
409
+ ? workspaceTaskUsageInstruction
410
+ : undefined,
408
411
  composite.members.length > 0
409
412
  ? "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."
410
413
  : undefined,
@@ -170,7 +170,7 @@ export function registerWorkspaceAuxiliaryTools(options) {
170
170
  ...(file !== undefined ? { file } : {}),
171
171
  }, hostScopeIdFor(extra._meta, extra.sessionId));
172
172
  return action === "run" && name !== "workspace.tasks"
173
- ? presentSemanticWorkResult(response, target)
173
+ ? presentSemanticWorkResult(response, target, hostScopeIdFor(extra._meta, extra.sessionId))
174
174
  : presentExecutionResult(response, target);
175
175
  }
176
176
  if (action === "run" && name === "batch.execute") {
@@ -195,7 +195,7 @@ export function registerWorkspaceAuxiliaryTools(options) {
195
195
  success: true,
196
196
  durationMs: Math.round(performance.now() - startedAt),
197
197
  });
198
- return presentSemanticWorkResult(result, target);
198
+ return presentSemanticWorkResult(result, target, hostScopeIdFor(extra._meta, extra.sessionId));
199
199
  }
200
200
  catch (error) {
201
201
  if (extra.signal.aborted)
@@ -225,7 +225,7 @@ export function registerWorkspaceAuxiliaryTools(options) {
225
225
  const response = await coreOperations.capabilityRun({ workspaceId: executionWorkspaceId, name, arguments: capabilityArguments, file }, executionContext);
226
226
  return name === "workspace.tasks"
227
227
  ? presentExecutionResult(response, target)
228
- : presentSemanticWorkResult(response, target);
228
+ : presentSemanticWorkResult(response, target, hostScopeIdFor(extra._meta, extra.sessionId));
229
229
  }
230
230
  const workspace = workspaces.getWorkspace(executionWorkspaceId);
231
231
  return runActivityToolWithHooks(activityLifecycle, hooks, workspace, hostScopeIdFor(extra._meta, extra.sessionId), activityRequestFor({ workspaceId: executionWorkspaceId, name, action, arguments: capabilityArguments, file }, executionContext), {
@@ -24,7 +24,7 @@ export function buildToolDescriptions(config) {
24
24
  ? ""
25
25
  : " Use shell commands for search and directory inspection instead of dedicated MCP search tools.";
26
26
  return {
27
- read: `Read one file or multiple files inside an open workspace or the OS temp directory. Use path for one target or paths for multiple targets; offset/limit apply to every target in a bulk read. Instruction files and advertised capability guides returned by ${toolNames.openWorkspace} are also readable when applicable.${skillCapability} Only advertised entry files and files under already-loaded advertised directories are readable outside the normal roots. Call ${toolNames.openWorkspace} first and pass workspaceId.`,
27
+ read: `Read one file or multiple files. Inside an open workspace, relative paths, Workspace instructions, advertised capability guides, and loaded advertised resources are available as usual.${skillCapability} For read-only inspection before opening a Workspace, omit workspaceId and use absolute paths already inside configured allowedRoots; unscoped reads never activate Workspace instructions, Skills, capabilities, or execution state. Use path for one target or paths for multiple targets; offset/limit apply to every target in a bulk read.`,
28
28
  write: `Create or completely overwrite a file inside an open workspace or the OS temp directory. Workspace paths may be relative; OS temp paths may be absolute. Call ${toolNames.openWorkspace} first and pass workspaceId.`,
29
29
  edit: `Edit one file or multiple files inside an open workspace or the OS temp directory by replacing exact text blocks. Use path for one target or paths for multiple targets; a bulk Edit applies the same edits to every file and preflights all targets before the first mutation. Each oldText must match a unique, non-overlapping region of the original file. Workspace paths may be relative; OS temp paths may be absolute. Call ${toolNames.openWorkspace} first and pass workspaceId.`,
30
30
  rename: `Rename or move one file or directory inside an open workspace or the OS temp directory without overwriting an existing destination. Source and destination must both remain inside the permitted file roots. Call ${toolNames.openWorkspace} first and pass workspaceId.`,
@@ -261,6 +261,7 @@ export function loadConfig(env = process.env) {
261
261
  ? files.config.subagents === true
262
262
  : parseBoolean(productEnv(env, "SUBAGENTS")),
263
263
  languageServers: files.config.languageServers ?? {},
264
+ allowAgentLanguageServerInstall: files.config.allowAgentLanguageServerInstall === true,
264
265
  agentDir: resolve(expandHomePath(productEnv(env, "AGENT_DIR") ?? files.config.agentDir ?? defaultAgentDir())),
265
266
  systemInstructionsPath: parseSystemInstructionsPath(productEnv(env, "SYSTEM_INSTRUCTIONS_PATH") ?? files.config.systemInstructionsPath),
266
267
  hooks: mergeHookConfigs(parseHookConfig(files.config.hooks), parseHookConfig(files.hooks), files.hookFiles),
@@ -1,7 +1,7 @@
1
1
  import { randomBytes, randomUUID } from "node:crypto";
2
2
  import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync, } from "node:fs";
3
3
  import { homedir } from "node:os";
4
- import { dirname, join, resolve } from "node:path";
4
+ import { join, resolve } from "node:path";
5
5
  import { withFileLock } from "../state/lock/file-lock.js";
6
6
  import { expandHomePath } from "../../mcp/filesystem/roots.js";
7
7
  import { mergeHookConfigs, parseHookFile, } from "../../mcp/hooks/hooks.js";
@@ -135,15 +135,6 @@ export async function removeForgeRelayRemote(alias, env = process.env) {
135
135
  return { ...auth, remotes };
136
136
  }, env);
137
137
  }
138
- export function ensureForgeRelayDefaultSkills(env = process.env) {
139
- const targetPath = join(forgerelaySkillsDir(env), "subagent-delegation", "SKILL.md");
140
- if (existsSync(targetPath))
141
- return [];
142
- const sourcePath = new URL("../../../skills/subagent-delegation/SKILL.md", import.meta.url);
143
- mkdirSync(dirname(targetPath), { recursive: true });
144
- writeFileSync(targetPath, readFileSync(sourcePath, "utf8"), { mode: 0o644 });
145
- return [targetPath];
146
- }
147
138
  export function resolveSubagentsFlag(config, env = process.env) {
148
139
  const value = env.FORGERELAY_SUBAGENTS;
149
140
  if (value === undefined)
package/dist/server.js CHANGED
@@ -10,6 +10,7 @@ import { downloadIncomingArtifact, isArtifactDownloadSupportedPlatform } from ".
10
10
  import { ArtifactError } from "./mcp/artifacts/artifact-error.js";
11
11
  import { loadConfig } from "./runtime/config/config.js";
12
12
  import { CodeIntelligenceError } from "./lsp/code-intelligence.js";
13
+ import { installManagedLanguageServers, installedManagedLanguageServers, supportedManagedLanguageServers, } from "./lsp/runtime/managed-language-servers.js";
13
14
  import { HookRunner } from "./mcp/hooks/hooks.js";
14
15
  import { checkHookConfiguration } from "./mcp/hooks/hook-cli.js";
15
16
  import { buildServerInstructions, buildToolDescriptions, toolNames } from "./mcp/server-instructions.js";
@@ -38,7 +39,7 @@ import { createOperationRuntime } from "./mcp/server/operations/runtime/operatio
38
39
  import { registerWorkspaceAuxiliaryTools } from "./mcp/server/workspace/runtime/workspace-tools.js";
39
40
  import { registerOpenWorkspaceTool } from "./mcp/server/workspace/runtime/workspace-open.js";
40
41
  import { redactSkillDiagnosticPaths } from "./mcp/server/core/schemas.js";
41
- import { attachWorkspaceTaskReminder, remapCompositeToolResult, toolResultIsError } from "./mcp/server/core/tool-support.js";
42
+ import { attachWorkspaceContextUpdate, attachWorkspaceTaskReminder, remapCompositeToolResult, toolResultIsError } from "./mcp/server/core/tool-support.js";
42
43
  export function createMcpServer(config, workspaces, reviewCheckpoints, processSessions, subagentProviders, incomingArtifactAdapters, codeIntelligence, activityLifecycle, bashOutputStore, activityQueries, options = {}) {
43
44
  const connectionScopeId = `mcp-connection:${randomUUID()}`;
44
45
  const activityPanelApp = createActivityPanelApp(config, FORGERELAY_VERSION);
@@ -95,10 +96,14 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
95
96
  return undefined;
96
97
  }
97
98
  };
98
- const presentSemanticWorkResult = (result, target) => {
99
- const presented = presentExecutionResult(result, target);
99
+ const presentSemanticWorkResult = (result, target, conversationScopeId) => {
100
+ let presented = presentExecutionResult(result, target);
100
101
  if (toolResultIsError(presented))
101
102
  return presented;
103
+ if (!remoteWorkspaces.has(target.executionWorkspaceId)) {
104
+ const update = workspaces.claimResourceUpdates(target.executionWorkspaceId, conversationScopeId);
105
+ presented = attachWorkspaceContextUpdate(presented, update?.text);
106
+ }
102
107
  const reminderWorkspaceId = taskReminderWorkspaceIdFor(target);
103
108
  return attachWorkspaceTaskReminder(presented, reminderWorkspaceId ? taskReminders.recordWork(reminderWorkspaceId) : undefined);
104
109
  };
@@ -154,10 +159,33 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
154
159
  },
155
160
  codeIntelligence: {
156
161
  available: true,
157
- run: async (input, context, options) => {
162
+ run: async (input, context, runOptions) => {
163
+ if (input.operation === "managed.status") {
164
+ return {
165
+ value: {
166
+ supported: supportedManagedLanguageServers(),
167
+ installed: installedManagedLanguageServers(config.configDir),
168
+ agentInstallAllowed: config.allowAgentLanguageServerInstall,
169
+ },
170
+ };
171
+ }
172
+ if (input.operation === "managed.install") {
173
+ if (!config.allowAgentLanguageServerInstall) {
174
+ throw new CapabilityError("code.managed_install_disabled", "Agent-managed Language Server installation is disabled. Enable it explicitly with forgerelay init --force.");
175
+ }
176
+ const install = options.managedLanguageServerInstaller ?? installManagedLanguageServers;
177
+ const installed = await install(input.servers, config.configDir);
178
+ return {
179
+ value: {
180
+ ...installed,
181
+ availableNow: installedManagedLanguageServers(config.configDir),
182
+ restartRequired: false,
183
+ },
184
+ };
185
+ }
158
186
  try {
159
187
  return {
160
- value: await codeIntelligence.run(requireCapabilityWorkspaceRoot(context), input, { signal: options.signal }),
188
+ value: await codeIntelligence.run(requireCapabilityWorkspaceRoot(context), input, { signal: runOptions.signal }),
161
189
  };
162
190
  }
163
191
  catch (error) {
@@ -291,7 +319,8 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
291
319
  };
292
320
  };
293
321
  const operationRuntime = createOperationRuntime({
294
- config, workspaces, activityLifecycle, hooks, processSessions, bashOutputStore, capabilityRegistry, hostScopeIdFor,
322
+ config, workspaces, activityLifecycle, hooks, processSessions, bashOutputStore,
323
+ capabilityRegistry, codeIntelligence, hostScopeIdFor,
295
324
  });
296
325
  batchExecutor = operationRuntime.batchExecutor;
297
326
  const { coreOperations, nativeBulkMutations } = operationRuntime;
@@ -430,6 +459,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
430
459
  coreOperations,
431
460
  nativeBulkMutations,
432
461
  activityLifecycle,
462
+ codeIntelligence,
433
463
  hooks,
434
464
  toolDescriptions,
435
465
  resolveExecutionTarget,