@akira-tl/forgerelay 1.0.0 → 1.0.1

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,14 @@ All notable ForgeRelay changes are documented here.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [1.0.1] - 2026-09-09
8
+
9
+ ### Fixed
10
+
11
+ - Preserved Capability names/actions and Workspace lifecycle actions in default pretty tool-call logs instead of collapsing multiplexed operations to generic `ok` lines.
12
+ - Completed attributable tool-call logging for Composite member mutations, checkout/Composite close and delete paths, and Composite-owned `workspace.tasks` success/failure results without logging arbitrary capability arguments.
13
+ - Preserved deletion of closed checkout Workspaces through historical aliases while adding lifecycle logging, avoiding a regression in the existing close/reopen/delete contract.
14
+
7
15
  ## [1.0.0] - 2026-09-07
8
16
 
9
17
  ### Changed
@@ -7,6 +7,12 @@ export function workspaceLogContext(workspace, _transportSessionId) {
7
7
  workspace: workspaceLogLabel(workspace.root, workspace.id),
8
8
  };
9
9
  }
10
+ export function compositeWorkspaceLogContext(workspace) {
11
+ return {
12
+ workspaceId: workspace.id,
13
+ workspace: workspaceLogLabel(workspace.name, workspace.id),
14
+ };
15
+ }
10
16
  export function formatDiscoveredWorkspaceInstructions(files, workspaceRoot) {
11
17
  return [
12
18
  "Workspace instructions discovered for this path. Apply them to follow-up work under their directories:",
@@ -2,7 +2,7 @@ import { registerAppTool } from "@modelcontextprotocol/ext-apps/server";
2
2
  import { openAiConversationScopeId } from "../../../request-meta.js";
3
3
  import { formatPathForPrompt } from "../../../../workspaces/resources/skills.js";
4
4
  import { compositeCapabilityContext } from "../../core/capability-support.js";
5
- import { logToolCall, textBlock } from "../../core/tool-support.js";
5
+ import { compositeWorkspaceLogContext, logToolCall, textBlock } from "../../core/tool-support.js";
6
6
  import { openWorkspaceToolDefinition } from "./workspace-open-schema.js";
7
7
  import { presentLocalWorkspaceOpen, workspaceTaskUsageInstruction, } from "./workspace-open-presentation.js";
8
8
  export function registerOpenWorkspaceTool(options) {
@@ -254,6 +254,16 @@ async function handleOpenWorkspace(options, input, extra) {
254
254
  : "This Composite Workspace currently has no members.",
255
255
  "Use the Composite workspaceId as the top-level handle. Work operations on it require an explicit member name; ForgeRelay never infers a member from tool type or purpose.",
256
256
  ].join("\n");
257
+ logToolCall(config, {
258
+ tool: "open_workspace",
259
+ ...compositeWorkspaceLogContext(composite),
260
+ action: `member.${memberAction}`,
261
+ path: memberAction === "update" && member.newName !== undefined
262
+ ? `${member.name} -> ${member.newName}`
263
+ : member.name,
264
+ success: true,
265
+ durationMs: Math.round(performance.now() - startedAt),
266
+ });
257
267
  const response = {
258
268
  content: [textBlock(instruction)],
259
269
  _meta: {
@@ -7,7 +7,7 @@ import { formatAgentsPath } from "../../../../workspaces.js";
7
7
  import { activityRelationFor, activityRequestFor, runActivityToolWithHooks, } from "../../core/activity-support.js";
8
8
  import { capabilityContextFor, compositeCapabilityContext, workspaceHookInvocation, } from "../../core/capability-support.js";
9
9
  import { capabilityErrorOutputSchema, resultOutputSchema } from "../../core/schemas.js";
10
- import { logFailedToolResponse, logToolCall, textBlock, toolResultIsError, workspaceLogContext, } from "../../core/tool-support.js";
10
+ import { compositeWorkspaceLogContext, logFailedToolResponse, logToolCall, textBlock, toolResultIsError, workspaceLogContext, } from "../../core/tool-support.js";
11
11
  const WRITE_TOOL_ANNOTATIONS = {
12
12
  readOnlyHint: false,
13
13
  destructiveHint: true,
@@ -100,6 +100,7 @@ export function registerWorkspaceAuxiliaryTools(options) {
100
100
  }
101
101
  const startedAt = performance.now();
102
102
  const context = compositeCapabilityContext(workspaceId, compositeTaskGuides);
103
+ const compositeLogContext = compositeWorkspaceLogContext(compositeWorkspaces.get(workspaceId));
103
104
  try {
104
105
  if (action === "run") {
105
106
  const execution = await capabilityRegistry.run(name, capabilityArguments ?? {}, context, {
@@ -114,6 +115,7 @@ export function registerWorkspaceAuxiliaryTools(options) {
114
115
  };
115
116
  logToolCall(config, {
116
117
  tool: toolNames.capability,
118
+ ...compositeLogContext,
117
119
  capability: name,
118
120
  action,
119
121
  success: true,
@@ -135,6 +137,7 @@ export function registerWorkspaceAuxiliaryTools(options) {
135
137
  };
136
138
  logToolCall(config, {
137
139
  tool: toolNames.capability,
140
+ ...compositeLogContext,
138
141
  capability: name,
139
142
  action,
140
143
  success: true,
@@ -148,7 +151,7 @@ export function registerWorkspaceAuxiliaryTools(options) {
148
151
  const capabilityError = error instanceof CapabilityError
149
152
  ? error
150
153
  : new CapabilityError("execution_failed", error instanceof Error ? error.message : String(error));
151
- return {
154
+ const result = {
152
155
  content: [textBlock(`${capabilityError.code}: ${capabilityError.message}`)],
153
156
  structuredContent: {
154
157
  name,
@@ -157,6 +160,13 @@ export function registerWorkspaceAuxiliaryTools(options) {
157
160
  },
158
161
  isError: true,
159
162
  };
163
+ logFailedToolResponse(config, {
164
+ tool: toolNames.capability,
165
+ ...compositeLogContext,
166
+ capability: name,
167
+ action,
168
+ }, result.content, startedAt);
169
+ return result;
160
170
  }
161
171
  }
162
172
  const target = resolveExecutionTarget(workspaceId, member);
@@ -323,6 +333,7 @@ export function registerWorkspaceAuxiliaryTools(options) {
323
333
  annotations: WRITE_TOOL_ANNOTATIONS,
324
334
  }, async ({ workspaceId, action = "close", commitMessage }, extra) => {
325
335
  if (compositeWorkspaces.has(workspaceId)) {
336
+ const startedAt = performance.now();
326
337
  if (commitMessage !== undefined) {
327
338
  throw new Error("close_workspace commitMessage is not valid for a Composite Workspace.");
328
339
  }
@@ -345,6 +356,14 @@ export function registerWorkspaceAuxiliaryTools(options) {
345
356
  : "The Composite Workspace had no members.",
346
357
  "Member Workspace handles, managed worktrees, processes, files, and Workspace Relay routes were not closed, finalized, deleted, or otherwise mutated.",
347
358
  ].join("\n");
359
+ logToolCall(config, {
360
+ tool: toolNames.closeWorkspace,
361
+ ...compositeWorkspaceLogContext(composite),
362
+ action,
363
+ path: composite.name,
364
+ success: true,
365
+ durationMs: Math.round(performance.now() - startedAt),
366
+ });
348
367
  return {
349
368
  content: [textBlock(result)],
350
369
  _meta: {
@@ -382,6 +401,7 @@ export function registerWorkspaceAuxiliaryTools(options) {
382
401
  }
383
402
  const session = workspaces.getWorkspaceSession(workspaceId);
384
403
  if (action === "delete" && session.mode === "checkout") {
404
+ const startedAt = performance.now();
385
405
  if (commitMessage !== undefined) {
386
406
  throw new Error("close_workspace commitMessage is not valid with action=delete for a checkout Workspace.");
387
407
  }
@@ -406,6 +426,14 @@ export function registerWorkspaceAuxiliaryTools(options) {
406
426
  activityQueries.deleteWorkspaceHistory(config.stateDir, session.id);
407
427
  await reviewCheckpoints.releaseWorkspace(session.id);
408
428
  const result = `Deleted ForgeRelay Workspace ${session.id}. Physical project files were not removed.`;
429
+ logToolCall(config, {
430
+ tool: toolNames.closeWorkspace,
431
+ ...workspaceLogContext(session, extra.sessionId),
432
+ action: "delete",
433
+ path: session.root,
434
+ success: true,
435
+ durationMs: Math.round(performance.now() - startedAt),
436
+ });
409
437
  return {
410
438
  content: [textBlock(result)],
411
439
  _meta: {
@@ -531,6 +559,7 @@ export function registerWorkspaceAuxiliaryTools(options) {
531
559
  logToolCall(config, {
532
560
  tool: toolNames.closeWorkspace,
533
561
  ...workspaceLogContext(workspace, extra.sessionId),
562
+ action,
534
563
  path: closed.sourceRoot,
535
564
  success: true,
536
565
  durationMs: Math.round(performance.now() - startedAt),
@@ -575,9 +604,18 @@ export function registerWorkspaceAuxiliaryTools(options) {
575
604
  if (processSessions.activeWorkspaceIds().has(checkoutWorkspaceId)) {
576
605
  throw new Error(`Workspace ${checkoutWorkspaceId} still owns a running process. Poll, interrupt, or wait for it before closing this workspace.`);
577
606
  }
607
+ const startedAt = performance.now();
578
608
  workspaces.closeWorkspace(checkoutWorkspaceId);
579
609
  await reviewCheckpoints.releaseWorkspace(checkoutWorkspaceId);
580
610
  const result = `Closed checkout-backed Workspace ${checkoutWorkspaceId}; its ForgeRelay identity was preserved for later reopen. Physical project files were not removed.`;
611
+ logToolCall(config, {
612
+ tool: toolNames.closeWorkspace,
613
+ ...workspaceLogContext(workspace, extra.sessionId),
614
+ action: "close",
615
+ path: workspace.root,
616
+ success: true,
617
+ durationMs: Math.round(performance.now() - startedAt),
618
+ });
581
619
  return {
582
620
  content: [textBlock(result)],
583
621
  _meta: {
@@ -131,6 +131,17 @@ function toolTarget(entry, tool) {
131
131
  if (isShellTool(tool)) {
132
132
  return stringField(entry.commandPreview) ?? stringField(entry.workingDirectory);
133
133
  }
134
+ if (tool === "capability") {
135
+ return [stringField(entry.capability), stringField(entry.action)]
136
+ .filter((value) => value !== undefined)
137
+ .join(" ") || undefined;
138
+ }
139
+ if (tool === "open_workspace" || tool === "close_workspace") {
140
+ return [
141
+ stringField(entry.action),
142
+ stringField(entry.path) ?? stringField(entry.workingDirectory),
143
+ ].filter((value) => value !== undefined).join(" ") || undefined;
144
+ }
134
145
  return stringField(entry.path) ?? stringField(entry.workingDirectory);
135
146
  }
136
147
  function toolResult(entry, tool, options) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akira-tl/forgerelay",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
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",